Added pilot-bound Monero observation persistence and the one-shot monero:pilot-observe command
This commit is contained in:
@@ -16,11 +16,14 @@ import { resolveCustodyIncident } from '../../server/market/custody-incident.ts'
|
||||
import { CustodyMarket } from '../../server/market/custody.ts';
|
||||
import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime, CUSTODY_SCHEMA_V1, CUSTODY_SCHEMA_V1_DIGEST,
|
||||
grantMoneroDepositCreditRuntime, grantMoneroDepositAllocatorRuntime, grantMoneroWithdrawalRuntime,
|
||||
grantPilotDepositReservationRuntime, grantPilotDepositAddressRuntime, CUSTODY_MIGRATION_V2,
|
||||
grantPilotDepositReservationRuntime, grantPilotDepositAddressRuntime, grantPilotMoneroObserverRuntime,
|
||||
CUSTODY_MIGRATION_V2,
|
||||
CUSTODY_SCHEMA_V2_DIGEST } from '../../server/market/postgres-schema.ts';
|
||||
import { parseMainnetCustodianId } from '../../server/market/mainnet-custody-identity.ts';
|
||||
import { initializePilotCustodyIdentity, parsePilotCustodianId } from '../../server/market/pilot-custody-identity.ts';
|
||||
import { createOrRecoverPilotMoneroDepositAddress, reservePilotMoneroDepositRequest } from '../../server/market/pilot-monero-deposit-allocation.ts';
|
||||
import { createOrRecoverPilotMoneroDepositAddress, reservePilotMoneroDepositRequest,
|
||||
reservePilotMoneroDepositIndex } from '../../server/market/pilot-monero-deposit-allocation.ts';
|
||||
import { persistPilotMoneroObservationScan } from '../../server/market/pilot-monero-observation-store.ts';
|
||||
import { readMainnetCustodyBalance, reserveMainnetWithdrawal, cancelMainnetWithdrawal } from '../../server/market/mainnet-withdrawal.ts';
|
||||
import { mainnetCustodyBalanceMessage, mainnetWithdrawalCancellationMessage, mainnetWithdrawalMessage,
|
||||
type MainnetCustodyBalanceRequestUnsigned, type MainnetWithdrawalRequestUnsigned,
|
||||
@@ -208,7 +211,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 v13 without dropping existing metadata', async t => {
|
||||
test('custody schema v1 upgrades additively through v14 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();
|
||||
@@ -222,7 +225,7 @@ test('custody schema v1 upgrades additively through v13 without dropping existin
|
||||
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, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
|
||||
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']) {
|
||||
@@ -231,6 +234,7 @@ test('custody schema v1 upgrades additively through v13 without dropping existin
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_deposit_allocations') AS name")).rows[0].name);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.pilot_custody_identity') AS name")).rows[0].name);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.pilot_monero_deposit_reservations') AS name")).rows[0].name);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.pilot_monero_observation_events') 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);
|
||||
@@ -238,7 +242,7 @@ test('custody schema v1 upgrades additively through v13 without dropping existin
|
||||
} finally { await admin.end(); }
|
||||
});
|
||||
|
||||
test('custody schema v2 upgrades explicitly through v13 deposit and withdrawal lifecycle tables', async t => {
|
||||
test('custody schema v2 upgrades explicitly through v14 deposit and withdrawal lifecycle tables', async t => {
|
||||
const admin = new Pool(config);
|
||||
await admin.query('DROP SCHEMA IF EXISTS wk_custody CASCADE');
|
||||
const client = await admin.connect();
|
||||
@@ -251,7 +255,7 @@ test('custody schema v2 upgrades explicitly through v13 deposit and withdrawal l
|
||||
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, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
|
||||
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);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_withdrawal_intents') AS name")).rows[0].name);
|
||||
@@ -507,6 +511,165 @@ test('pilot Wallet-RPC address creation recovers a lost response and registers o
|
||||
}
|
||||
});
|
||||
|
||||
test('pilot observation persistence classifies allocated and suspense outputs and records reorg evidence only', async t => {
|
||||
const admin = new Pool(config), database = `pilot_observations_${randomUUID().replaceAll('-', '')}`;
|
||||
const role = `pilot_observer_${randomUUID().replaceAll('-', '')}`;
|
||||
await admin.query(`CREATE DATABASE "${database}"`);
|
||||
const owner = new Pool({ ...config, database }), run = promisify(execFile);
|
||||
const custodianId = randomUUID(), generation = randomUUID(), fingerprint = 'c'.repeat(64), now = 1_800_000_000_000;
|
||||
const allocatedAddress = await syntheticSubaddress(21), suspenseAddress = await syntheticSubaddress(22);
|
||||
const code = `import { Pool } from 'pg';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { CHAIN_ID } from './server/testnet/consensus.ts';
|
||||
import { initializePilotCustodyIdentity } from './server/market/pilot-custody-identity.ts';
|
||||
import { reservePilotMoneroDepositIndex } from './server/market/pilot-monero-deposit-allocation.ts';
|
||||
import { persistPilotMoneroObservationScan } from './server/market/pilot-monero-observation-store.ts';
|
||||
const config={host:'127.0.0.1',database:process.env.DB,user:'postgres',ssl:false,max:4};
|
||||
const owner=new Pool(config), observer=new Pool({...config,user:process.env.OBSERVER_ROLE});
|
||||
const now=Number(process.env.NOW), walletGenerationId=process.env.GENERATION, walletFingerprint=process.env.FINGERPRINT;
|
||||
try {
|
||||
const identity=await initializePilotCustodyIdentity(owner,process.env.CUSTODIAN,now-100);
|
||||
const intent={requestId:randomUUID(),custodianId:identity.custodianId,marketNetwork:identity.marketNetwork,
|
||||
marketChainId:CHAIN_ID,moneroNetwork:'mainnet',beneficiary:'a'.repeat(64),walletGenerationId,walletFingerprint,
|
||||
accountIndex:0,requestDigest:'b'.repeat(64)};
|
||||
const allocation=(await reservePilotMoneroDepositIndex(owner,identity,intent,now-50)).reservation;
|
||||
await owner.query("UPDATE wk_custody.pilot_monero_deposit_reservations SET status='creating',create_started_at=$2 WHERE allocation_id=$1",
|
||||
[allocation.allocationId,now-40]);
|
||||
await owner.query("UPDATE wk_custody.pilot_monero_deposit_reservations SET status='registered',monero_address=$2,registered_at=$3 WHERE allocation_id=$1",
|
||||
[allocation.allocationId,process.env.ALLOCATED_ADDRESS,now-30]);
|
||||
const assigned={observationId:'d'.repeat(64)+':'+'e'.repeat(64),transactionId:'d'.repeat(64),address:process.env.ALLOCATED_ADDRESS,
|
||||
amountAtomicXmr:'9007199254740993',height:100,confirmations:11,timestamp:now-1000,unlockTime:'0',accountIndex:0,addressIndex:0,
|
||||
outputPublicKey:'e'.repeat(64),globalOutputIndex:50,locked:false,doubleSpendSeen:false};
|
||||
const unallocated={...assigned,observationId:'f'.repeat(64)+':'+'1'.repeat(64),transactionId:'f'.repeat(64),
|
||||
address:process.env.SUSPENSE_ADDRESS,amountAtomicXmr:'7',height:101,confirmations:10,addressIndex:1,
|
||||
outputPublicKey:'1'.repeat(64),globalOutputIndex:51};
|
||||
const persist=(scanId,expectedScanHeight,fromHeight,throughHeight,walletHeight,incoming)=>persistPilotMoneroObservationScan(observer,
|
||||
{scanId,walletGenerationId,walletFingerprint,expectedScanHeight,scannedAt:now+walletHeight,
|
||||
page:{network:'mainnet',walletHeight,fromHeight,throughHeight,primaryAddress:process.env.PRIMARY_ADDRESS,incoming}});
|
||||
const firstId=randomUUID(),first=await persist(firstId,0,0,110,110,[assigned,unallocated]);
|
||||
const replay=await persist(firstId,0,0,110,110,[assigned,unallocated]);
|
||||
const missing=await persist(randomUUID(),110,100,120,120,[]);
|
||||
const reappeared=await persist(randomUUID(),120,100,130,130,[{...assigned,confirmations:31}]);
|
||||
const rows=await observer.query('SELECT classification,currently_present,amount_atomic_xmr::text AS amount FROM wk_custody.pilot_monero_observations ORDER BY classification');
|
||||
const events=await observer.query('SELECT event_type,count(*)::int AS n FROM wk_custody.pilot_monero_observation_events GROUP BY event_type ORDER BY event_type');
|
||||
const journals=await owner.query('SELECT count(*)::int AS n FROM wk_custody.journals');
|
||||
const credits=await owner.query('SELECT count(*)::int AS n FROM wk_custody.monero_deposit_credit_events');
|
||||
console.log(JSON.stringify({first,replay,missing,reappeared,rows:rows.rows,events:events.rows,journals:journals.rows[0].n,credits:credits.rows[0].n}));
|
||||
} finally { await observer.end(); await owner.end(); }`;
|
||||
try {
|
||||
await migrateCustody(owner);
|
||||
await admin.query(`CREATE ROLE "${role}" LOGIN`);
|
||||
await grantPilotMoneroObserverRuntime(owner, role);
|
||||
const result = JSON.parse((await run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], {
|
||||
env: { ...process.env, WIKIDECK_NETWORK: 'pilot', WIKIDECK_PILOT_MODE: 'private-mock-only', DB: database,
|
||||
OBSERVER_ROLE: role, CUSTODIAN: custodianId, GENERATION: generation, FINGERPRINT: fingerprint,
|
||||
NOW: String(now), ALLOCATED_ADDRESS: allocatedAddress, SUSPENSE_ADDRESS: suspenseAddress,
|
||||
PRIMARY_ADDRESS: ADDITIONAL_MARKET_FEE.address },
|
||||
})).stdout.trim());
|
||||
assert.equal(result.first.observed, 2); assert.equal(result.first.assigned, 1); assert.equal(result.first.unallocated, 1);
|
||||
assert.equal(result.replay.replayed, true);
|
||||
assert.equal(result.missing.notSeen, 2);
|
||||
assert.equal(result.reappeared.reappeared, 1);
|
||||
assert.deepEqual(result.rows.map((row: { classification: string; currently_present: boolean; amount: string }) =>
|
||||
[row.classification,row.currently_present,row.amount]), [['assigned',true,'9007199254740993'],['unallocated',false,'7']]);
|
||||
assert.deepEqual(result.events.map((row: { event_type: string; n: number }) => [row.event_type,row.n]),
|
||||
[['not-seen',2],['reappeared',1],['seen',2]]);
|
||||
assert.equal(result.journals, 0); assert.equal(result.credits, 0);
|
||||
assert.equal((await owner.query("SELECT has_table_privilege($1,'wk_custody.accounts','SELECT') AS allowed", [role])).rows[0].allowed, false);
|
||||
const observer = new Pool({ ...config, database, user: role });
|
||||
try {
|
||||
await assert.rejects(observer.query('DELETE FROM wk_custody.pilot_monero_observation_events'));
|
||||
await assert.rejects(observer.query("UPDATE wk_custody.pilot_monero_observations SET monero_address=$1", [suspenseAddress]));
|
||||
await assert.rejects(observer.query('SELECT * FROM wk_custody.journals'));
|
||||
} finally { await observer.end(); }
|
||||
} finally {
|
||||
await owner.end();
|
||||
await admin.query(`DROP DATABASE IF EXISTS "${database}" WITH (FORCE)`);
|
||||
await admin.query(`DROP ROLE IF EXISTS "${role}"`);
|
||||
await admin.end();
|
||||
}
|
||||
});
|
||||
|
||||
test('pilot observations classify reserved-wallet outputs, persist replay/reorg evidence, and never post credits', async t => {
|
||||
const admin = new Pool(config), database = `pilot_observations_${randomUUID().replaceAll('-', '')}`;
|
||||
const role = `pilot_observer_${randomUUID().replaceAll('-', '')}`;
|
||||
await admin.query(`CREATE DATABASE "${database}"`);
|
||||
const owner = new Pool({ ...config, database }), run = promisify(execFile);
|
||||
const custodianId = randomUUID(), generation = randomUUID(), fingerprint = 'c'.repeat(64), now = 1_800_000_000_000;
|
||||
const allocatedAddress = await syntheticSubaddress(21), suspenseAddress = await syntheticSubaddress(22);
|
||||
const primaryAddress = ADDITIONAL_MARKET_FEE.address;
|
||||
const code = `import { Pool } from 'pg';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { CHAIN_ID } from './server/testnet/consensus.ts';
|
||||
import { initializePilotCustodyIdentity } from './server/market/pilot-custody-identity.ts';
|
||||
import { reservePilotMoneroDepositIndex } from './server/market/pilot-monero-deposit-allocation.ts';
|
||||
import { persistPilotMoneroObservationScan } from './server/market/pilot-monero-observation-store.ts';
|
||||
const config={host:'127.0.0.1',database:process.env.DB,user:'postgres',ssl:false,max:4};
|
||||
const owner=new Pool(config), observer=new Pool({...config,user:process.env.OBSERVER_ROLE});
|
||||
const now=Number(process.env.NOW), walletGenerationId=process.env.GENERATION, walletFingerprint=process.env.FINGERPRINT;
|
||||
try {
|
||||
const identity=await initializePilotCustodyIdentity(owner,process.env.CUSTODIAN,now-100);
|
||||
const intent={requestId:randomUUID(),custodianId:identity.custodianId,marketNetwork:identity.marketNetwork,
|
||||
marketChainId:CHAIN_ID,moneroNetwork:'mainnet',beneficiary:'a'.repeat(64),walletGenerationId,walletFingerprint,
|
||||
accountIndex:0,requestDigest:'b'.repeat(64)};
|
||||
const allocation=(await reservePilotMoneroDepositIndex(owner,identity,intent,now-50)).reservation;
|
||||
await owner.query("UPDATE wk_custody.pilot_monero_deposit_reservations SET status='creating',create_started_at=$2 WHERE allocation_id=$1",
|
||||
[allocation.allocationId,now-40]);
|
||||
await owner.query("UPDATE wk_custody.pilot_monero_deposit_reservations SET status='registered',monero_address=$2,registered_at=$3 WHERE allocation_id=$1",
|
||||
[allocation.allocationId,process.env.ALLOCATED_ADDRESS,now-30]);
|
||||
const assigned={observationId:'d'.repeat(64)+':'+'e'.repeat(64),transactionId:'d'.repeat(64),address:process.env.ALLOCATED_ADDRESS,
|
||||
amountAtomicXmr:'9007199254740993',height:100,confirmations:11,timestamp:now-1000,unlockTime:'0',accountIndex:0,addressIndex:0,
|
||||
outputPublicKey:'e'.repeat(64),globalOutputIndex:50,locked:false,doubleSpendSeen:false};
|
||||
const unallocated={...assigned,observationId:'f'.repeat(64)+':'+'1'.repeat(64),transactionId:'f'.repeat(64),
|
||||
address:process.env.SUSPENSE_ADDRESS,amountAtomicXmr:'7',height:101,confirmations:10,addressIndex:1,
|
||||
outputPublicKey:'1'.repeat(64),globalOutputIndex:51};
|
||||
const persist=(scanId,expectedScanHeight,fromHeight,throughHeight,walletHeight,incoming)=>persistPilotMoneroObservationScan(observer,
|
||||
{scanId,walletGenerationId,walletFingerprint,expectedScanHeight,scannedAt:now+walletHeight,
|
||||
page:{network:'mainnet',walletHeight,fromHeight,throughHeight,primaryAddress:process.env.PRIMARY_ADDRESS,incoming}});
|
||||
const firstId=randomUUID(),first=await persist(firstId,0,0,110,110,[assigned,unallocated]);
|
||||
const replay=await persist(firstId,0,0,110,110,[assigned,unallocated]);
|
||||
const missing=await persist(randomUUID(),110,100,120,120,[]);
|
||||
const reappeared=await persist(randomUUID(),120,100,130,130,[{...assigned,confirmations:31}]);
|
||||
const rows=await observer.query('SELECT classification,currently_present,amount_atomic_xmr::text AS amount FROM wk_custody.pilot_monero_observations ORDER BY classification');
|
||||
const events=await observer.query('SELECT event_type,count(*)::int AS n FROM wk_custody.pilot_monero_observation_events GROUP BY event_type ORDER BY event_type');
|
||||
const journals=await owner.query('SELECT count(*)::int AS n FROM wk_custody.journals');
|
||||
const credits=await owner.query('SELECT count(*)::int AS n FROM wk_custody.monero_deposit_credit_events');
|
||||
console.log(JSON.stringify({first,replay,missing,reappeared,rows:rows.rows,events:events.rows,journals:journals.rows[0].n,credits:credits.rows[0].n}));
|
||||
} finally { await observer.end(); await owner.end(); }`;
|
||||
try {
|
||||
await migrateCustody(owner);
|
||||
await admin.query(`CREATE ROLE "${role}" LOGIN`);
|
||||
await grantPilotMoneroObserverRuntime(owner, role);
|
||||
const result = JSON.parse((await run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], {
|
||||
env: { ...process.env, WIKIDECK_NETWORK: 'pilot', WIKIDECK_PILOT_MODE: 'private-mock-only', DB: database,
|
||||
OBSERVER_ROLE: role, CUSTODIAN: custodianId, GENERATION: generation, FINGERPRINT: fingerprint,
|
||||
NOW: String(now), ALLOCATED_ADDRESS: allocatedAddress, SUSPENSE_ADDRESS: suspenseAddress,
|
||||
PRIMARY_ADDRESS: primaryAddress },
|
||||
})).stdout.trim());
|
||||
assert.equal(result.first.observed, 2); assert.equal(result.first.assigned, 1); assert.equal(result.first.unallocated, 1);
|
||||
assert.equal(result.replay.replayed, true);
|
||||
assert.equal(result.missing.notSeen, 2);
|
||||
assert.equal(result.reappeared.reappeared, 1);
|
||||
assert.deepEqual(result.rows.map((row: { classification: string; currently_present: boolean; amount: string }) =>
|
||||
[row.classification,row.currently_present,row.amount]), [['assigned',true,'9007199254740993'],['unallocated',false,'7']]);
|
||||
assert.deepEqual(result.events.map((row: { event_type: string; n: number }) => [row.event_type,row.n]),
|
||||
[['not-seen',2],['reappeared',1],['seen',2]]);
|
||||
assert.equal(result.journals, 0); assert.equal(result.credits, 0);
|
||||
assert.equal((await owner.query("SELECT has_table_privilege($1,'wk_custody.accounts','SELECT') AS allowed", [role])).rows[0].allowed, false);
|
||||
const observer = new Pool({ ...config, database, user: role });
|
||||
try {
|
||||
await assert.rejects(observer.query('DELETE FROM wk_custody.pilot_monero_observation_events'));
|
||||
await assert.rejects(observer.query("UPDATE wk_custody.pilot_monero_observations SET monero_address=$1", [suspenseAddress]));
|
||||
await assert.rejects(observer.query('SELECT * FROM wk_custody.journals'));
|
||||
} finally { await observer.end(); }
|
||||
} finally {
|
||||
await owner.end();
|
||||
await admin.query(`DROP DATABASE IF EXISTS "${database}" WITH (FORCE)`);
|
||||
await admin.query(`DROP ROLE IF EXISTS "${role}"`);
|
||||
await admin.end();
|
||||
}
|
||||
});
|
||||
|
||||
test('mainnet custody identity bootstrap is one-time, chain-bound, and does not open withdrawals', async t => {
|
||||
assert.equal(parseMainnetCustodianId('AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA'), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa');
|
||||
assert.throws(() => parseMainnetCustodianId('not-a-uuid'), /Invalid mainnet custodian UUID/);
|
||||
|
||||
Reference in New Issue
Block a user