Ledger-side credit and withdrawal-intent paths

This commit is contained in:
2026-10-22 13:28:55 -04:00
parent 4fea176877
commit 52559f65d7
17 changed files with 842 additions and 40 deletions
+92 -1
View File
@@ -10,6 +10,7 @@ import { address, verifySignature } from '../testnet/consensus.ts';
import { depositCreditApprovalMessage, depositCreditReleaseApprovalMessage,
depositReorgResolutionMessage, type DepositCreditReleaseSubject, type DepositCreditReviewSubject,
type DepositReorgResolutionSubject } from './monero-deposit-credit.ts';
import { mainnetWithdrawalCancellationMessage, mainnetWithdrawalMessage } from '../../shared/monero-withdrawal.ts';
/** Codes only: never attach customer IDs, payloads, SQL errors or addresses to operational diagnostics. */
export class CustodyIntegrityError extends Error {
@@ -319,6 +320,89 @@ async function checkDepositCredits(client: PoolClient, budget: () => void) {
h.observation_event_sequence<=c.last_observation_event_sequence LIMIT 1`);
if (invalidHold.rowCount) fail('deposit-reorg-hold-mismatch');
}
async function checkMainnetWithdrawalIntents(client: PoolClient, budget: () => void): Promise<void> {
type WithdrawalIntentAuditRow = { request_id: string; custodian_id: string; market_network: string; market_chain_id: string;
beneficiary: string; destination: string; amount_atomic_xmr: string; max_fee_atomic_xmr: string; public_key: string;
expires_at: string; request_signature: string; request_digest: string; status: string; created_at: string;
cancel_request_id: string | null; cancel_public_key: string | null; cancel_expires_at: string | null;
cancel_signature: string | null; cancelled_at: string | null };
let after: string | null = null;
const identityResult = await client.query("SELECT value FROM wk_custody.custody_meta WHERE key='identity'");
if (!identityResult.rowCount) {
if ((await client.query('SELECT 1 FROM wk_custody.monero_withdrawal_intents LIMIT 1')).rowCount) fail('mainnet-withdrawal-identity-missing');
return;
}
const identity = JSON.parse(identityResult.rows[0].value) as Record<string, unknown>;
if (identity.mode !== 'mainnet-custody' || identity.network !== 'wikideck-randomx-mainnet-1' || identity.policy !== 'custody-mainnet-v1') {
if ((await client.query('SELECT 1 FROM wk_custody.monero_withdrawal_intents LIMIT 1')).rowCount) fail('mainnet-withdrawal-identity-mismatch');
return;
}
for (;;) {
budget();
const result: QueryResult<WithdrawalIntentAuditRow> = await client.query<WithdrawalIntentAuditRow>(`SELECT request_id::text,custodian_id::text,market_network,market_chain_id,beneficiary,
destination,amount_atomic_xmr::text,max_fee_atomic_xmr::text,public_key,expires_at::text,request_signature,
request_digest,status,created_at::text,cancel_request_id::text,cancel_public_key,cancel_expires_at::text,
cancel_signature,cancelled_at::text FROM wk_custody.monero_withdrawal_intents
WHERE ($1::uuid IS NULL OR request_id>$1::uuid) ORDER BY request_id LIMIT 128`, [after]);
const postings = await journals(client, result.rows.flatMap(row => [
`monero-withdrawal-reserve:${row.request_id}`, `monero-withdrawal-cancel:${row.request_id}`]));
for (const row of result.rows) {
let destination: string;
try { destination = await validateXmrAddress(row.destination); }
catch { fail('mainnet-withdrawal-destination-invalid'); }
const total = (BigInt(row.amount_atomic_xmr) + BigInt(row.max_fee_atomic_xmr)).toString();
const aggregate = await client.query(`SELECT
COALESCE((SELECT sum(amount_atomic_xmr+max_fee_atomic_xmr) FROM wk_custody.monero_withdrawal_intents
WHERE beneficiary=$1 AND status='reserved'),0)::text AS reserved,
COALESCE((SELECT sum(c.amount_atomic_xmr) FROM wk_custody.monero_deposit_credit_events c
JOIN wk_custody.monero_deposit_release_events r ON r.credit_id=c.credit_id WHERE c.beneficiary=$1),0)::text AS credited`,
[row.beneficiary]);
if (BigInt(aggregate.rows[0].reserved) > BigInt(aggregate.rows[0].credited)) {
fail('mainnet-withdrawal-exceeds-reviewed-credit');
}
const holdAccount = `customer:${row.beneficiary}:monero-withdrawal:${row.request_id}`;
const availableAccount = `customer:${row.beneficiary}:available`;
const hold = await client.query('SELECT balance::text,normal FROM wk_custody.accounts WHERE id=$1', [holdAccount]);
const active = row.status === 'reserved';
if (!['reserved','cancelled'].includes(row.status) || (active && (hold.rowCount !== 1 || hold.rows[0].normal !== 'credit' || hold.rows[0].balance !== total)) ||
(!active && hold.rowCount === 1 && (hold.rows[0].normal !== 'credit' || hold.rows[0].balance !== '0'))) fail('mainnet-withdrawal-reservation-mismatch');
try {
const request = { version: 1 as const, network: 'wikideck-randomx-mainnet-1' as const,
chainId: row.market_chain_id, custodianId: row.custodian_id, beneficiary: row.beneficiary,
publicKey: row.public_key, requestId: row.request_id, destination, amountAtomicXmr: row.amount_atomic_xmr,
maxFeeAtomicXmr: row.max_fee_atomic_xmr, expiresAt: Number(row.expires_at), signature: row.request_signature };
const { signature: _signature, ...unsigned } = request;
const message = mainnetWithdrawalMessage(unsigned);
if (row.market_network !== 'wikideck-randomx-mainnet-1' || row.custodian_id !== identity.id ||
address(row.public_key) !== row.beneficiary || createHash('sha256').update(message).digest('hex') !== row.request_digest ||
!verifySignature(row.public_key, row.beneficiary, message, row.request_signature)) fail('mainnet-withdrawal-authorization-invalid');
} catch { fail('mainnet-withdrawal-authorization-invalid'); }
expectJournal(postings, `monero-withdrawal-reserve:${row.request_id}`, 'monero-withdrawal-reservation', [
d(availableAccount, total), c(holdAccount, total)]);
if (active) {
if (row.cancel_request_id || row.cancel_signature || row.cancelled_at) fail('mainnet-withdrawal-cancellation-invalid');
expectJournal(postings, `monero-withdrawal-cancel:${row.request_id}`, 'monero-withdrawal-cancellation', null);
} else {
try {
const cancellation = { version: 1 as const, network: 'wikideck-randomx-mainnet-1' as const,
chainId: row.market_chain_id, custodianId: row.custodian_id, beneficiary: row.beneficiary,
publicKey: row.cancel_public_key!, cancellationId: row.cancel_request_id!,
withdrawalRequestId: row.request_id, expiresAt: Number(row.cancel_expires_at), signature: row.cancel_signature! };
const { signature: cancelSignature, ...unsigned } = cancellation;
const message = mainnetWithdrawalCancellationMessage(unsigned);
if (address(cancellation.publicKey) !== row.beneficiary ||
!verifySignature(cancellation.publicKey, row.beneficiary, message, cancelSignature)) fail('mainnet-withdrawal-cancellation-invalid');
} catch { fail('mainnet-withdrawal-cancellation-invalid'); }
expectJournal(postings, `monero-withdrawal-cancel:${row.request_id}`, 'monero-withdrawal-cancellation', [
d(holdAccount, total), c(availableAccount, total)]);
}
}
if (result.rows.length < 128) break;
after = result.rows.at(-1)!.request_id;
}
}
const withdrawalSelect = `SELECT w.id::text,w.actor,w.status,w.data,a.balance::text,a.normal,
b.kind AS box_kind,b.status AS box_status,b.data AS box_data FROM wk_custody.withdrawals w
LEFT JOIN wk_custody.accounts a ON a.id='customer:'||w.actor||':withdrawal:'||w.id::text
@@ -363,12 +447,16 @@ export async function auditBusinessLiabilities(client: PoolClient): Promise<void
if (result.rows.length < 128) break; after = result.rows.at(-1)!.id;
}
await checkDepositCredits(client, budget);
await checkMainnetWithdrawalIntents(client, budget);
// Check both directions: a correct order cannot excuse orphan reserves, tasks or terminal postings.
const checks: [string, string][] = [
['orphan-purchase-reserve', `SELECT 1 FROM wk_custody.accounts a WHERE a.id LIKE 'customer:%:purchase:%'
AND NOT EXISTS(SELECT 1 FROM wk_custody.orders o WHERE a.id='customer:'||o.buyer||':purchase:'||o.id::text) LIMIT 1`],
['orphan-withdrawal-reserve', `SELECT 1 FROM wk_custody.accounts a WHERE a.id LIKE 'customer:%:withdrawal:%'
AND NOT EXISTS(SELECT 1 FROM wk_custody.withdrawals w WHERE a.id='customer:'||w.actor||':withdrawal:'||w.id::text) LIMIT 1`],
['orphan-monero-withdrawal-reserve', `SELECT 1 FROM wk_custody.accounts a WHERE a.id LIKE 'customer:%:monero-withdrawal:%'
AND NOT EXISTS(SELECT 1 FROM wk_custody.monero_withdrawal_intents w
WHERE a.id='customer:'||w.beneficiary||':monero-withdrawal:'||w.request_id::text) LIMIT 1`],
['orphan-outbox', `SELECT 1 FROM wk_custody.outbox b WHERE NOT EXISTS(SELECT 1 FROM wk_custody.orders o WHERE o.id=b.id AND b.kind='nft-lock')
AND NOT EXISTS(SELECT 1 FROM wk_custody.withdrawals w WHERE w.id=b.id AND b.kind='withdrawal') LIMIT 1`],
['listing-order-mismatch', `SELECT 1 FROM wk_custody.listings l LEFT JOIN wk_custody.orders o ON o.data->>'listingId'=l.id::text
@@ -379,7 +467,10 @@ export async function auditBusinessLiabilities(client: PoolClient): Promise<void
(j.kind IN ('purchase-reservation','purchase-cancellation','mock-sale-settlement') AND NOT EXISTS
(SELECT 1 FROM wk_custody.orders o WHERE j.id IN ('reserve:'||o.id::text,'cancel:'||o.id::text,'settle:'||o.id::text))) OR
(j.kind IN ('withdrawal-reservation','withdrawal-cancellation','mock-withdrawal') AND NOT EXISTS
(SELECT 1 FROM wk_custody.withdrawals w WHERE j.id IN ('withdraw:'||w.id::text,'withdraw-cancel:'||w.id::text,'withdraw-confirm:'||w.id::text))) LIMIT 1`],
(SELECT 1 FROM wk_custody.withdrawals w WHERE j.id IN ('withdraw:'||w.id::text,'withdraw-cancel:'||w.id::text,'withdraw-confirm:'||w.id::text))) OR
(j.kind IN ('monero-withdrawal-reservation','monero-withdrawal-cancellation') AND NOT EXISTS
(SELECT 1 FROM wk_custody.monero_withdrawal_intents w WHERE j.id IN
('monero-withdrawal-reserve:'||w.request_id::text,'monero-withdrawal-cancel:'||w.request_id::text))) LIMIT 1`],
['duplicate-prepared-attempt', `SELECT 1 FROM wk_custody.withdrawals WHERE status IN ('prepared','complete')
GROUP BY data->>'attemptId' HAVING count(*)>1 LIMIT 1`],
['duplicate-prepared-transaction', `SELECT 1 FROM wk_custody.withdrawals WHERE status IN ('prepared','complete')