Strengthen financial reconciliation checks

This commit is contained in:
2026-10-20 16:56:13 -04:00
parent 7734d8ae90
commit 447314440b
16 changed files with 519 additions and 17 deletions
+115 -1
View File
@@ -9,7 +9,9 @@ import { spawn } from 'node:child_process';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { DatabaseSync } from 'node:sqlite';
import { PostgresCustody } from '../../server/market/postgres-custody.ts';
import { PostgresCustody, auditPostgres } from '../../server/market/postgres-custody.ts';
import { CustodyIntegrityError } from '../../server/market/custody-integrity.ts';
import { resolveCustodyIncident } from '../../server/market/custody-incident.ts';
import { CustodyMarket } from '../../server/market/custody.ts';
import { migrateCustody, grantCustodyRuntime } from '../../server/market/postgres-schema.ts';
import { importSqliteCustody } from '../../server/market/custody-import.ts';
@@ -266,3 +268,115 @@ test('failed cutover freezes the source, rolls back the entire target, and resum
try { assert.equal((await restored.audit()).assetsAtomicXmr, '1000000000000'); }
finally { await restored.close(); }
});
test('business audit rejects metadata corruption even when every journal and total remains balanced', async t => {
const f = await fixture(t); await f.execute(buyer, { type: 'mock-credit' });
const l = await f.execute(seller, { type: 'list', cardHash: hash, sequence: 0, priceAtomicXmr: '10000', feeBps: 250 }) as CustodyListing;
const o = await f.execute(buyer, { type: 'reserve', listingId: l.id, priceAtomicXmr: l.priceAtomicXmr, feeBps: l.feeBps }) as CustodyOrder;
const w = await f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '500', maxFeeAtomicXmr: '20' }) as CustodyWithdrawal;
const probes: [string, unknown[], string][] = [
["UPDATE wk_custody.orders SET data=jsonb_set(data,'{buyer}',to_jsonb($1::text)) WHERE id=$2", [other.actor, o.id], 'order-terms-mismatch'],
["UPDATE wk_custody.orders SET data=jsonb_set(data,'{sellerAtomicXmr}','\"1\"') WHERE id=$1", [o.id], 'order-allocation-mismatch'],
["UPDATE wk_custody.orders SET data=jsonb_set(data,'{grossAtomicXmr}','\"1.0\"') WHERE id=$1", [o.id], 'order-record-invalid'],
["UPDATE wk_custody.orders SET status='settled',data=jsonb_set(data,'{status}','\"settled\"') WHERE id=$1", [o.id], 'order-reservation-mismatch'],
["DELETE FROM wk_custody.outbox WHERE id=$1", [o.id], 'order-state-mismatch'],
["UPDATE wk_custody.outbox SET data='{}' WHERE id=$1", [o.id], 'order-outbox-invalid'],
["UPDATE wk_custody.listings SET data=jsonb_set(data,'{feeBps}','500') WHERE id=$1", [l.id], 'order-allocation-mismatch'],
["UPDATE wk_custody.withdrawals SET data=jsonb_set(data,'{maxFeeAtomicXmr}','\"21\"') WHERE id=$1", [w.id], 'withdrawal-reservation-mismatch'],
["UPDATE wk_custody.withdrawals SET data=jsonb_set(data,'{actor}',to_jsonb($1::text)) WHERE id=$2", [other.actor, w.id], 'withdrawal-identity-mismatch'],
["UPDATE wk_custody.outbox SET status='fixture-prepared' WHERE id=$1", [w.id], 'withdrawal-outbox-mismatch'],
["INSERT INTO wk_custody.outbox VALUES($1,'withdrawal','blocked','{}')", [randomUUID()], 'orphan-outbox'],
["INSERT INTO wk_custody.accounts(id,normal) VALUES($1,'credit')", [`customer:${buyer.actor}:purchase:${randomUUID()}`], 'orphan-purchase-reserve'],
];
const c = await f.admin.connect();
try {
for (const [sql, args, code] of probes) {
await c.query('BEGIN');
try {
await c.query(sql, args);
const totals = await c.query("SELECT sum(CASE WHEN normal='debit' THEN balance ELSE -balance END)::text AS net FROM wk_custody.accounts");
assert.equal(totals.rows[0].net, '0'); // This would pass the old aggregate-only accounting audit.
await assert.rejects(auditPostgres(c), (e: unknown) => e instanceof CustodyIntegrityError && e.code === code, code);
} finally { await c.query('ROLLBACK'); }
}
} finally { c.release(); }
assert.equal((await f.market.audit()).journals, 3);
});
test('integrity incident blocks new mutations across instances/restarts but preserves private reads and original receipts', async t => {
const f = await fixture(t), second = await PostgresCustody.open(f.runtime, options);
const credit = envelope(f.market.id, buyer, { type: 'mock-credit' }); await f.market.execute(credit);
const w = await f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '100', maxFeeAtomicXmr: '0' }) as CustodyWithdrawal;
try {
await f.admin.query("UPDATE wk_custody.withdrawals SET data=jsonb_set(data,'{amountAtomicXmr}','\"101\"') WHERE id=$1", [w.id]);
await assert.rejects(f.market.audit(), (e: unknown) => e instanceof CustodyIntegrityError && e.code === 'withdrawal-reservation-mismatch');
const status = f.market.diagnostics(); assert.equal(status.integrityHold, true); assert.equal(status.mutationHold, true);
const text = JSON.stringify(status); for (const secret of [buyer.actor, w.id, 'postgresql:', 'password']) assert.equal(text.includes(secret), false);
const marker = await f.admin.query("SELECT value FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'");
assert.equal(JSON.parse(marker.rows[0].value).code, 'withdrawal-reservation-mismatch');
await assert.rejects(second.execute(envelope(second.id, other, { type: 'mock-credit' })), /integrity incident/);
assert.equal((await f.market.execute(credit) as CustodyActivity).availableAtomicXmr, '1000000000000');
assert.equal((await f.activity()).withdrawalReservedAtomicXmr, '100');
await assert.rejects(PostgresCustody.open(f.runtime, options), /unresolved-integrity-incident/);
// Restore this disposable corruption to prove that repair alone does not silently clear the incident.
await f.admin.query('UPDATE wk_custody.withdrawals SET data=$1 WHERE id=$2', [JSON.stringify(w), w.id]);
await assert.rejects(f.market.audit(), /unresolved-integrity-incident/);
assert.equal((await f.activity()).availableAtomicXmr, '999999999900');
const runtimePool = new Pool(f.runtime);
try { await assert.rejects(resolveCustodyIncident(runtimePool, randomUUID()), /Schema-owner/); }
finally { await runtimePool.end(); }
const resolved = await resolveCustodyIncident(f.admin, randomUUID()); assert.ok(resolved.resolutionId);
// Previously detecting instances remain latched even after authorized acknowledgement.
await f.market.audit(); assert.equal(f.market.diagnostics().integrityHold, true);
await assert.rejects(f.execute(other, { type: 'mock-credit' }), /reconciliation/);
const verified = await PostgresCustody.open(f.runtime, options);
try { assert.equal(verified.diagnostics().mutationHold, false); await verified.audit(); }
finally { await verified.close(); }
} finally { await second.close(); }
});
test('targeted mutation checks reject corrupt reservations before posting or cancelling their outbox', async t => {
const f = await fixture(t); await f.execute(buyer, { type: 'mock-credit' });
const w = await f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '500', maxFeeAtomicXmr: '20' }) as CustodyWithdrawal;
await f.admin.query("UPDATE wk_custody.outbox SET data='{}' WHERE id=$1", [w.id]);
await assert.rejects(f.execute(buyer, { type: 'cancel-withdrawal', withdrawalId: w.id }), /integrity check/);
assert.equal((await f.admin.query('SELECT status FROM wk_custody.withdrawals WHERE id=$1', [w.id])).rows[0].status, 'reserved');
assert.equal((await f.admin.query("SELECT count(*)::int AS n FROM wk_custody.journals WHERE kind='withdrawal-cancellation'")).rows[0].n, 0);
assert.equal((await f.activity()).availableAtomicXmr, '999999999480');
assert.equal(f.market.diagnostics().integrityHold, true);
await assert.rejects(resolveCustodyIncident(f.admin, randomUUID()), /integrity check/);
assert.equal((await f.admin.query("SELECT count(*)::int AS n FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'")).rows[0].n, 1);
});
test('business audit pages past 128 records and catches later corrupted output without dropping claims', async t => {
const f = await fixture(t); await f.execute(buyer, { type: 'mock-credit' });
let last: CustodyWithdrawal | undefined;
for (let i = 0; i < 130; i++) {
const w = await f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' }) as CustodyWithdrawal;
if (!last || last.id < w.id) last = w;
}
assert.equal((await f.market.audit()).journals, 131);
await f.admin.query("UPDATE wk_custody.outbox SET data='{}' WHERE id=$1", [last!.id]);
await assert.rejects(f.market.audit(), /withdrawal-outbox-invalid/);
assert.equal((await f.activity()).withdrawalReservedAtomicXmr, '130');
});
test('imported fixture completion and cancellation states reconcile their exact terminal postings', async t => {
const f = await fixture(t); await f.admin.query('DELETE FROM wk_custody.custody_meta');
const dir = await mkdtemp(join(tmpdir(), 'custody-terminal-audit-')); t.after(() => rm(dir, { recursive: true, force: true }));
const source = await CustodyMarket.open(dir, options), id = source.id;
const run = (who: typeof buyer, action: CustodyAction) => source.execute(envelope(id, who, action));
run(buyer, { type: 'mock-credit' });
const l = run(seller, { type: 'list', cardHash: hash, sequence: 0, priceAtomicXmr: '1000', feeBps: 250 }) as CustodyListing;
const o = run(buyer, { type: 'reserve', listingId: l.id, priceAtomicXmr: l.priceAtomicXmr, feeBps: 250 }) as CustodyOrder;
source.settleFixture(o.id, `fixture:${'c'.repeat(64)}`);
const w = run(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1000', maxFeeAtomicXmr: '50' }) as CustodyWithdrawal;
source.prepareWithdrawalFixture(w.id, randomUUID(), `fixture:${'d'.repeat(64)}`, '10'); source.confirmWithdrawalFixture(w.id, `fixture:${'d'.repeat(64)}`);
const cancelled = run(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '100', maxFeeAtomicXmr: '5' }) as CustodyWithdrawal;
run(buyer, { type: 'cancel-withdrawal', withdrawalId: cancelled.id });
const expected = source.audit(); source.close();
await importSqliteCustody(join(dir, 'testnet', 'custody-sandbox.sqlite'), f.admin);
const pg = await PostgresCustody.open(f.runtime, options);
try { assert.deepEqual(await pg.audit(), expected); }
finally { await pg.close(); }
});