Verified incoming transfers against Monero Wallet PRC

This commit is contained in:
2026-10-21 21:34:46 -04:00
parent af646dcc32
commit 9f82f050b4
13 changed files with 605 additions and 17 deletions
+65
View File
@@ -1,10 +1,13 @@
import { z } from 'zod';
import { createHash } from 'node:crypto';
import type { PoolClient, QueryResult } from 'pg';
import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts';
import { parseAtomicXmr } from '../../shared/xmr-amount.ts';
import { allocateXmrSale } from '../../shared/xmr-pricing.ts';
import { validateXmrAddress } from './xmr.ts';
import { replayReservedSale } from './sale-rehearsal.ts';
import { address, verifySignature } from '../testnet/consensus.ts';
import { depositCreditApprovalMessage, type DepositCreditReviewSubject } from './monero-deposit-credit.ts';
/** Codes only: never attach customer IDs, payloads, SQL errors or addresses to operational diagnostics. */
export class CustodyIntegrityError extends Error {
@@ -169,6 +172,67 @@ async function checkWithdrawals(client: PoolClient, rows: WithdrawalRow[]) {
c(available, (BigInt(w.maxFeeAtomicXmr) - BigInt(w.feeAtomicXmr!)).toString())] : null);
}
}
type DepositCreditRow = { credit_id: string; wallet_fingerprint: string; observation_id: string; allocation_id: string;
beneficiary: string; amount_atomic_xmr: string; journal_id: string; last_observation_event_sequence: string;
observation_digest: string; reviewer_owner: string; reviewer_public_key: string; review_nonce: string;
review_expires_at: string; review_signature: string; review_subject: DepositCreditReviewSubject; credited_at: string;
review_digest: string; minimum_confirmations: number; balance: string | null; normal: string | null };
async function checkDepositCredits(client: PoolClient, budget: () => void) {
let after: string | null = null;
for (;;) {
budget();
const result: QueryResult<DepositCreditRow> = await client.query<DepositCreditRow>(`SELECT c.credit_id::text,c.wallet_fingerprint,c.observation_id,
c.allocation_id::text,c.beneficiary,c.amount_atomic_xmr::text,c.journal_id,c.last_observation_event_sequence::text,
c.observation_digest,c.reviewer_owner,c.reviewer_public_key,c.review_nonce,c.review_expires_at::text,
c.review_signature,c.review_subject,c.review_digest,c.credited_at::text,c.minimum_confirmations,
a.balance::text,a.normal FROM wk_custody.monero_deposit_credit_events c
LEFT JOIN wk_custody.accounts a ON a.id='customer:'||c.beneficiary||':deposit-pending:'||c.credit_id::text
WHERE ($1::uuid IS NULL OR c.credit_id>$1::uuid) ORDER BY c.credit_id LIMIT 128`, [after]);
const postings = await journals(client, result.rows.map(row => row.journal_id));
for (const row of result.rows) {
let approvalIsValid = false;
let subjectIsValid = false;
try {
const message = depositCreditApprovalMessage(row.review_subject, row.reviewer_owner,
row.review_nonce, Number(row.review_expires_at));
approvalIsValid = address(row.reviewer_public_key) === row.reviewer_owner &&
createHash('sha256').update(message).digest('hex') === row.review_digest &&
Number(row.review_expires_at) >= Number(row.credited_at) &&
verifySignature(row.reviewer_public_key, row.reviewer_owner, message, row.review_signature);
subjectIsValid = row.observation_id === `${row.review_subject.observationId.split(':')[0]}:${row.review_subject.outputPublicKey}` &&
row.review_subject.walletFingerprint === row.wallet_fingerprint &&
row.review_subject.observationDigest === row.observation_digest &&
row.review_subject.observationId === row.observation_id &&
row.review_subject.amountAtomicXmr === row.amount_atomic_xmr && row.review_subject.beneficiary === row.beneficiary &&
row.review_subject.allocationId === row.allocation_id &&
row.review_subject.observationEventSequence === Number(row.last_observation_event_sequence) &&
row.review_subject.minimumConfirmations === row.minimum_confirmations;
} catch { approvalIsValid = false; }
if (!subjectIsValid || !approvalIsValid) fail('deposit-credit-observation-mismatch');
if (row.normal !== 'credit' || row.balance !== row.amount_atomic_xmr) fail('deposit-pending-liability-mismatch');
expectJournal(postings, row.journal_id, 'monero-deposit-pending-credit', [
d('treasury:monero:pending', row.amount_atomic_xmr),
c(`customer:${row.beneficiary}:deposit-pending:${row.credit_id}`, row.amount_atomic_xmr),
]);
}
if (result.rows.length < 128) break;
after = result.rows.at(-1)!.credit_id;
}
const orphanAccount = await client.query(`SELECT 1 FROM wk_custody.accounts a
WHERE a.id LIKE 'customer:%:deposit-pending:%' AND NOT EXISTS (
SELECT 1 FROM wk_custody.monero_deposit_credit_events c
WHERE a.id='customer:'||c.beneficiary||':deposit-pending:'||c.credit_id::text) LIMIT 1`);
if (orphanAccount.rowCount) fail('orphan-deposit-pending-liability');
const orphanJournal = await client.query(`SELECT 1 FROM wk_custody.journals j WHERE j.kind='monero-deposit-pending-credit'
AND NOT EXISTS(SELECT 1 FROM wk_custody.monero_deposit_credit_events c WHERE c.journal_id=j.id) LIMIT 1`);
if (orphanJournal.rowCount) fail('orphan-deposit-credit-journal');
const invalidHold = await client.query(`SELECT 1 FROM wk_custody.monero_deposit_reorg_holds h
JOIN wk_custody.monero_deposit_credit_events c ON c.credit_id=h.credit_id
WHERE h.wallet_fingerprint<>c.wallet_fingerprint OR h.observation_id<>c.observation_id OR
h.observation_event_sequence<=c.last_observation_event_sequence LIMIT 1`);
if (invalidHold.rowCount) fail('deposit-reorg-hold-mismatch');
}
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
@@ -212,6 +276,7 @@ export async function auditBusinessLiabilities(client: PoolClient): Promise<void
await validateWallets(wallets);
if (result.rows.length < 128) break; after = result.rows.at(-1)!.id;
}
await checkDepositCredits(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:%'