Reviewed release and reorg-hold resolution

This commit is contained in:
2026-10-21 22:24:42 -04:00
parent 9f82f050b4
commit 05aead39ae
8 changed files with 504 additions and 21 deletions
+72 -6
View File
@@ -18,8 +18,10 @@ import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime, CUSTOD
grantMoneroDepositCreditRuntime, CUSTODY_MIGRATION_V2, CUSTODY_SCHEMA_V2_DIGEST } from '../../server/market/postgres-schema.ts';
import { persistMoneroObservationScan } from '../../server/market/monero-observation-store.ts';
import { moneroWalletFingerprint } from '../../server/market/monero-observer.ts';
import { depositCreditApprovalMessage, postReviewedDepositPendingCredit, prepareDepositCreditReview,
reconcileDepositCreditReorgHolds, type DepositCreditPolicy } from '../../server/market/monero-deposit-credit.ts';
import { depositCreditApprovalMessage, depositCreditReleaseApprovalMessage, depositReorgResolutionMessage,
postReviewedDepositPendingCredit, prepareDepositCreditReleaseReview, prepareDepositCreditReview, prepareDepositReorgResolution,
reconcileDepositCreditReorgHolds, releaseReviewedDepositCredit, resolveReleasedDepositReorgHold,
type DepositCreditPolicy } from '../../server/market/monero-deposit-credit.ts';
import { createOrRecoverMoneroDepositSubaddress, registerMoneroDepositSubaddress,
reserveMoneroDepositAllocation } from '../../server/market/monero-deposit-allocation-store.ts';
import { importSqliteCustody } from '../../server/market/custody-import.ts';
@@ -194,7 +196,7 @@ test('explicit PostgreSQL migrations are idempotent and runtime role cannot alte
assert.equal((await f.market.audit()).assetsAtomicXmr, '1000000000000');
});
test('custody schema v1 upgrades additively through v3 without dropping existing metadata', async t => {
test('custody schema v1 upgrades additively through v7 without dropping existing metadata', async t => {
const admin = new Pool(config);
await admin.query('DROP SCHEMA IF EXISTS wk_custody CASCADE');
const client = await admin.connect();
@@ -208,17 +210,21 @@ test('custody schema v1 upgrades additively through v3 without dropping existing
try {
await migrateCustody(admin); await migrateCustody(admin);
const versions = await admin.query('SELECT version FROM wk_custody.schema_versions ORDER BY version');
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5]);
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7]);
assert.equal((await admin.query("SELECT value FROM wk_custody.custody_meta WHERE key='v1-preserved-marker'")).rows[0].value,
'existing-test-data');
for (const table of ['monero_observer_state', 'monero_transfer_observations', 'monero_observation_events', 'monero_observation_scans']) {
assert.ok((await admin.query('SELECT to_regclass($1) AS name', [`wk_custody.${table}`])).rows[0].name);
}
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_deposit_allocations') AS name")).rows[0].name);
for (const table of ['monero_deposit_credit_events', 'monero_deposit_reorg_holds',
'monero_deposit_release_events', 'monero_deposit_reorg_resolutions']) {
assert.ok((await admin.query('SELECT to_regclass($1) AS name', [`wk_custody.${table}`])).rows[0].name);
}
} finally { await admin.end(); }
});
test('custody schema v2 upgrades explicitly to v3 address-allocation registry', async t => {
test('custody schema v2 upgrades explicitly through v7 deposit lifecycle tables', async t => {
const admin = new Pool(config);
await admin.query('DROP SCHEMA IF EXISTS wk_custody CASCADE');
const client = await admin.connect();
@@ -231,8 +237,9 @@ test('custody schema v2 upgrades explicitly to v3 address-allocation registry',
await client.query('COMMIT');
await migrateCustody(admin); await migrateCustody(admin);
const versions = await admin.query('SELECT version FROM wk_custody.schema_versions ORDER BY version');
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5]);
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7]);
assert.equal((await admin.query("SELECT value FROM wk_custody.custody_meta WHERE key='v2-marker'")).rows[0].value, 'preserve');
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_deposit_release_events') AS name")).rows[0].name);
} catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; }
finally { client.release(); await admin.end(); }
});
@@ -506,6 +513,65 @@ test('reviewed Monero outputs post once to pending liability and reorgs create h
observation.amountAtomicXmr);
await assert.rejects(creditRuntime.query('UPDATE wk_custody.monero_deposit_credit_events SET amount_atomic_xmr=1'));
await assert.rejects(creditRuntime.query('DELETE FROM wk_custody.monero_deposit_reorg_holds'));
const releaseSubject = await prepareDepositCreditReleaseReview(creditRuntime, first.creditId, policy, now + 220);
assert.equal(releaseSubject.unresolvedHolds.length, 2, 'release review must include prior disappearance and reappearance evidence');
const releaseNonce = randomUUID(), releaseExpiresAt = now + 280;
const releaseApproval = { subject: releaseSubject, reviewerOwner: host.actor, reviewerPublicKey: host.publicKey,
nonce: releaseNonce, expiresAt: releaseExpiresAt,
signature: host.sign(depositCreditReleaseApprovalMessage(releaseSubject, host.actor, releaseNonce, releaseExpiresAt)) };
const releases = await Promise.all([
releaseReviewedDepositCredit(creditRuntime, releaseApproval, policy, now + 221),
releaseReviewedDepositCredit(secondCreditRuntime, releaseApproval, policy, now + 221),
]);
assert.equal(releases.filter(item => !item.replayed).length, 1);
const released = releases.find(item => !item.replayed)!;
assert.equal(released.amountAtomicXmr, observation.amountAtomicXmr);
const releaseReplay = await releaseReviewedDepositCredit(creditRuntime, releaseApproval, policy, releaseExpiresAt + 1);
assert.equal(releaseReplay.replayed, true, 'exact release retry returns the committed result after signature expiry');
assert.equal((await f.admin.query('SELECT balance::text FROM wk_custody.accounts WHERE id=$1', [pendingId])).rows[0].balance, '0');
assert.equal((await f.admin.query('SELECT balance::text FROM wk_custody.accounts WHERE id=$1', [`customer:${buyer.actor}:available`])).rows[0].balance,
observation.amountAtomicXmr);
assert.equal((await f.admin.query('SELECT count(*)::int AS n FROM wk_custody.monero_deposit_reorg_resolutions WHERE release_id=$1',
[released.releaseId])).rows[0].n, 2);
assert.equal((await f.market.audit()).assetsAtomicXmr, observation.amountAtomicXmr);
const laterMissing = await persistMoneroObservationScan(observer, { scanId: randomUUID(), walletFingerprint: wallet,
expectedScanHeight: 102, scannedAt: now + 300,
page: { ...page, fromHeight: 90, walletHeight: 103, throughHeight: 103, incoming: [] } });
assert.equal(laterMissing.notSeen, 1);
assert.equal(await reconcileDepositCreditReorgHolds(creditRuntime, wallet, now + 310), 1);
await assert.rejects(f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' }),
/reorg review/);
const laterReappeared = await persistMoneroObservationScan(observer, { scanId: randomUUID(), walletFingerprint: wallet,
expectedScanHeight: 103, scannedAt: now + 400,
page: { ...page, fromHeight: 90, walletHeight: 104, throughHeight: 104,
incoming: [{ ...observation, confirmations: 15 }] } });
assert.equal(laterReappeared.reappeared, 1);
assert.equal(await reconcileDepositCreditReorgHolds(creditRuntime, wallet, now + 410), 1);
const unresolved = await f.admin.query(`SELECT hold_id::text FROM wk_custody.monero_deposit_reorg_holds h
WHERE h.credit_id=$1 AND NOT EXISTS(SELECT 1 FROM wk_custody.monero_deposit_reorg_resolutions r WHERE r.hold_id=h.hold_id)
ORDER BY h.observation_event_sequence`, [first.creditId]);
assert.equal(unresolved.rowCount, 2, 'both the later disappearance and reappearance events require explicit resolution');
let resolutionClock = now + 420;
for (const hold of unresolved.rows) {
const resolutionSubject = await prepareDepositReorgResolution(creditRuntime, first.creditId, hold.hold_id, policy, resolutionClock);
const resolutionNonce = randomUUID(), resolutionExpiresAt = resolutionClock + 60;
const resolutionApproval = { subject: resolutionSubject, reviewerOwner: host.actor, reviewerPublicKey: host.publicKey,
nonce: resolutionNonce, expiresAt: resolutionExpiresAt,
signature: host.sign(depositReorgResolutionMessage(resolutionSubject, host.actor, resolutionNonce, resolutionExpiresAt)) };
const resolved = await resolveReleasedDepositReorgHold(creditRuntime, resolutionApproval, policy, resolutionClock + 1);
assert.equal(resolved.replayed, false);
assert.equal((await resolveReleasedDepositReorgHold(creditRuntime, resolutionApproval, policy, resolutionClock + 2)).replayed, true);
resolutionClock += 10;
}
assert.equal((await f.admin.query(`SELECT count(*)::int AS n FROM wk_custody.monero_deposit_reorg_holds h
WHERE h.credit_id=$1 AND NOT EXISTS(SELECT 1 FROM wk_custody.monero_deposit_reorg_resolutions r WHERE r.hold_id=h.hold_id)`,
[first.creditId])).rows[0].n, 0);
await f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' });
assert.equal((await f.admin.query('SELECT count(*)::int AS n FROM wk_custody.journals')).rows[0].n, 3,
'one pending credit, one release, and one authorized sandbox withdrawal are journaled');
assert.equal((await f.market.audit()).assetsAtomicXmr, observation.amountAtomicXmr);
} finally { await custodyRuntime.end(); await secondCreditRuntime.end(); await creditRuntime.end(); await observer.end(); }
});