Strengthen financial reconciliation checks
This commit is contained in:
@@ -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(); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user