import { createHash } from 'node:crypto'; import type { Pool, PoolClient } from 'pg'; import { z } from 'zod'; import { XMR_RPC_UINT64_MAX } from '../../shared/xmr-amount.ts'; import { CHAIN_ID } from '../testnet/consensus.ts'; import { IS_PILOT, NETWORK } from '../../shared/network.ts'; import { verifyCustodySchema } from './postgres-schema.ts'; import { pilotCustodyIdentityFromRow } from './pilot-custody-identity.ts'; import { validateXmrSubaddress } from './xmr.ts'; const HEX64 = z.string().regex(/^[a-f0-9]{64}$/); const UUID = z.string().uuid(); const OBSERVATION_ID = /^[a-f0-9]{64}:[a-f0-9]{64}$/; const UINT64 = /^(0|[1-9]\d{0,19})$/; const MAX_EVENT_HISTORY = 256; const MAX_BLOCK_UNLOCK = 500_000_000n; export type PilotDepositReviewPolicy = { minimumConfirmations: number; maximumObservationAgeMs: number; }; const subjectWithoutDigestSchema = z.object({ policyVersion: z.literal('pilot-monero-deposit-review-v1'), preparationOnly: z.literal(true), crediting: z.literal('disabled'), marketNetwork: z.literal('wikideck-randomx-pilot-1'), marketChainId: HEX64, moneroNetwork: z.literal('mainnet'), custodianId: UUID, activationStatus: z.literal('identity-only'), walletGenerationId: UUID, walletFingerprint: HEX64, allocationId: UUID, beneficiary: HEX64, accountIndex: z.number().int().nonnegative().max(0xffff_ffff).safe(), addressIndex: z.number().int().nonnegative().max(0xffff_ffff).safe(), moneroAddress: z.string().length(95), observationId: z.string().regex(OBSERVATION_ID), transactionId: HEX64, outputPublicKey: HEX64, amountAtomicXmr: z.string().regex(/^[1-9]\d{0,19}$/), blockHeight: z.number().int().positive().safe(), walletHeight: z.number().int().positive().safe(), confirmations: z.number().int().positive().safe(), minimumConfirmations: z.number().int().min(10).max(100_000).safe(), unlockTime: z.string().regex(UINT64), locked: z.literal(false), doubleSpendSeen: z.literal(false), currentlyPresent: z.literal(true), globalOutputIndex: z.number().int().nonnegative().safe(), observationDigest: HEX64, scanId: UUID, scanFromHeight: z.number().int().nonnegative().safe(), scanThroughHeight: z.number().int().nonnegative().safe(), observerScanHeight: z.number().int().nonnegative().safe(), scanCompletedAt: z.number().int().nonnegative().safe(), observerUpdatedAt: z.number().int().nonnegative().safe(), lastObservedAt: z.number().int().nonnegative().safe(), scanFreshnessMs: z.number().int().nonnegative().safe(), maximumObservationAgeMs: z.number().int().min(10_000).max(15 * 60_000).safe(), eventHistorySequence: z.number().int().nonnegative().safe(), eventHistoryCount: z.number().int().positive().max(MAX_EVENT_HISTORY).safe(), eventHistoryDigest: HEX64, preparedAt: z.number().int().nonnegative().safe(), }).strict(); export type PilotDepositReviewSubject = z.infer & { subjectDigest: string }; const reviewSubjectSchema = subjectWithoutDigestSchema.extend({ subjectDigest: HEX64 }).strict(); const inputSchema = z.object({ walletGenerationId: UUID, observationId: z.string().regex(OBSERVATION_ID), }).strict(); function digest(value: string): string { return createHash('sha256').update(value).digest('hex'); } function safeInteger(value: unknown, name: string, minimum = 0): number { if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'bigint') { throw new Error(`Invalid pilot review ${name}`); } const number = Number(value); if (!Number.isSafeInteger(number) || number < minimum) throw new Error(`Invalid pilot review ${name}`); return number; } function validatePolicy(policy: PilotDepositReviewPolicy): void { if (!Number.isSafeInteger(policy.minimumConfirmations) || policy.minimumConfirmations < 10 || policy.minimumConfirmations > 100_000 || !Number.isSafeInteger(policy.maximumObservationAgeMs) || policy.maximumObservationAgeMs < 10_000 || policy.maximumObservationAgeMs > 15 * 60_000) { throw new Error('Invalid pilot deposit review policy'); } } function assertUnlockMature(unlockTime: string, walletHeight: number, scanCompletedAt: number): void { if (!UINT64.test(unlockTime)) throw new Error('Pilot deposit unlock time is malformed'); const unlock = BigInt(unlockTime); if (unlock === 0n) return; const mature = unlock < MAX_BLOCK_UNLOCK ? BigInt(walletHeight) >= unlock : BigInt(Math.floor(scanCompletedAt / 1000)) >= unlock; if (!mature) throw new Error('Pilot deposit output has a future unlock time'); } function validateEvents(rows: Record[], observationDigest: string) { if (!rows.length || rows.length > MAX_EVENT_HISTORY) { throw new Error('Pilot observation event history is missing or exceeds the review bound'); } const events = rows.map(row => { const sequence = safeInteger(row.sequence, 'event sequence'); const observedAt = safeInteger(row.observed_at, 'event timestamp'); const walletHeight = safeInteger(row.wallet_height, 'event wallet height'); const payloadDigest = HEX64.parse(row.payload_digest); const eventType = z.enum(['seen', 'changed', 'reappeared', 'not-seen']).parse(row.event_type); const scanId = UUID.parse(row.scan_id); return { sequence, observedAt, walletHeight, payloadDigest, eventType, scanId }; }); if (events.some(event => event.eventType !== 'seen' || event.payloadDigest !== observationDigest)) { throw new Error('Pilot observation history contains change, disappearance, or reappearance evidence and needs separate review'); } const last = events.at(-1)!; const historyDigest = digest(JSON.stringify(events.map(event => [event.sequence, event.eventType, event.payloadDigest, event.walletHeight, event.observedAt, event.scanId]))); return { sequence: last.sequence, count: events.length, digest: historyDigest }; } /** * Build an operator-facing pilot deposit review snapshot from persisted observations only. * This function is read-only and does not approve, stage, post, release, or credit funds. */ export async function preparePilotMoneroDepositReview(pool: Pool, inputValue: unknown, policy: PilotDepositReviewPolicy, now = Date.now()): Promise { if (!IS_PILOT || NETWORK !== 'wikideck-randomx-pilot-1') { throw new Error('Pilot deposit review preparation requires the pilot profile'); } const input = inputSchema.parse(inputValue); validatePolicy(policy); if (!Number.isSafeInteger(now) || now < 0) throw new Error('Invalid pilot review time'); const client: PoolClient = await pool.connect(); try { await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'"); await verifyCustodySchema(client); const identityResult = await client.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1'); if (identityResult.rowCount !== 1) throw new Error('Pilot custody identity is not initialized'); const identity = pilotCustodyIdentityFromRow(identityResult.rows[0]); if (identity.marketChainId !== CHAIN_ID || identity.marketNetwork !== NETWORK || identity.moneroNetwork !== 'mainnet' || identity.activationStatus !== 'identity-only') { throw new Error('Pilot custody identity does not permit review preparation'); } const observations = await client.query(`SELECT o.wallet_generation_id::text,o.observation_id,o.allocation_id::text,o.classification,o.txid,o.monero_address, o.amount_atomic_xmr::text AS amount_atomic_xmr,o.block_height::text AS block_height, o.wallet_height::text AS wallet_height,o.confirmations::text AS confirmations,o.unlock_time, o.account_index::text AS account_index,o.address_index::text AS address_index,o.output_public_key, o.global_output_index::text AS global_output_index,o.locked,o.double_spend_seen,o.payload_digest, o.currently_present,o.last_seen_scan_id::text AS scan_id,o.last_observed_at::text AS last_observed_at, s.wallet_fingerprint,s.network,s.scan_height::text AS observer_scan_height,s.updated_at::text AS observer_updated_at, sc.from_height::text AS scan_from_height,sc.through_height::text AS scan_through_height, sc.completed_at::text AS scan_completed_at FROM wk_custody.pilot_monero_observations o JOIN wk_custody.pilot_monero_observer_state s USING(wallet_generation_id) JOIN wk_custody.pilot_monero_observation_scans sc ON sc.scan_id=o.last_seen_scan_id AND sc.wallet_generation_id=o.wallet_generation_id WHERE o.wallet_generation_id=$1 AND o.observation_id=$2`, [input.walletGenerationId, input.observationId]); if (observations.rowCount !== 1) throw new Error('Pilot deposit observation was not found for this wallet generation'); const row = observations.rows[0]; if (row.wallet_generation_id !== input.walletGenerationId || row.observation_id !== input.observationId || row.network !== identity.moneroNetwork || row.classification !== 'assigned' || row.allocation_id === null || row.currently_present !== true || row.double_spend_seen !== false || row.locked !== false) { throw new Error('Pilot deposit observation is not an assigned, present, unlocked, non-double-spent output'); } const txid = HEX64.parse(row.txid); const outputPublicKey = HEX64.parse(row.output_public_key); if (row.observation_id !== `${txid}:${outputPublicKey}`) throw new Error('Pilot observation ID does not match its output identity'); const amountAtomicXmr = z.string().regex(/^[1-9]\d{0,19}$/).parse(row.amount_atomic_xmr); const amount = BigInt(amountAtomicXmr); if (amount > XMR_RPC_UINT64_MAX) throw new Error('Pilot deposit amount exceeds the Monero RPC uint64 range'); const blockHeight = safeInteger(row.block_height, 'block height', 1); const walletHeight = safeInteger(row.wallet_height, 'wallet height', 1); const confirmations = safeInteger(row.confirmations, 'confirmation count', 1); const observerScanHeight = safeInteger(row.observer_scan_height, 'observer scan height'); const scanFromHeight = safeInteger(row.scan_from_height, 'scan start height'); const scanThroughHeight = safeInteger(row.scan_through_height, 'scan end height'); const scanCompletedAt = safeInteger(row.scan_completed_at, 'scan completion time'); const observerUpdatedAt = safeInteger(row.observer_updated_at, 'observer update time'); const lastObservedAt = safeInteger(row.last_observed_at, 'last observation time'); const accountIndex = safeInteger(row.account_index, 'account index'); const addressIndex = safeInteger(row.address_index, 'address index'); const globalOutputIndex = safeInteger(row.global_output_index, 'global output index'); if (confirmations !== walletHeight - blockHeight + 1 || confirmations < policy.minimumConfirmations) { throw new Error('Pilot deposit output is below the configured confirmation floor'); } if (observerScanHeight < walletHeight || scanThroughHeight < walletHeight || scanFromHeight > blockHeight || scanThroughHeight < blockHeight || lastObservedAt !== scanCompletedAt || observerUpdatedAt < scanCompletedAt) { throw new Error('Pilot observation scan is not caught up or its persisted scan context is inconsistent'); } const scanFreshnessMs = now - observerUpdatedAt; if (scanFreshnessMs < 0 || now - scanCompletedAt < 0 || now - lastObservedAt < 0 || scanFreshnessMs > policy.maximumObservationAgeMs || now - scanCompletedAt > policy.maximumObservationAgeMs || now - lastObservedAt > policy.maximumObservationAgeMs) { throw new Error('Pilot Monero observation scan is stale or has a future timestamp'); } assertUnlockMature(row.unlock_time, walletHeight, scanCompletedAt); const allocations = await client.query(`SELECT allocation_id::text,custodian_id::text,market_network,market_chain_id, monero_network,beneficiary,wallet_generation_id::text,wallet_fingerprint,account_index::text AS account_index, address_index::text AS address_index,status,monero_address FROM wk_custody.pilot_monero_deposit_reservations WHERE allocation_id=$1`, [row.allocation_id]); if (allocations.rowCount !== 1) throw new Error('Pilot assigned output has no unique durable reservation'); const allocation = allocations.rows[0]; const beneficiary = HEX64.parse(allocation.beneficiary); if (allocation.custodian_id !== identity.custodianId || allocation.market_network !== identity.marketNetwork || allocation.market_chain_id !== identity.marketChainId || allocation.monero_network !== identity.moneroNetwork || allocation.wallet_generation_id !== input.walletGenerationId || allocation.wallet_fingerprint !== row.wallet_fingerprint || safeInteger(allocation.account_index, 'reserved account index') !== accountIndex || safeInteger(allocation.address_index, 'reserved address index') !== addressIndex || allocation.status !== 'registered' || allocation.monero_address !== row.monero_address) { throw new Error('Pilot output does not exactly match its immutable registered reservation and identity'); } const moneroAddress = await validateXmrSubaddress(z.string().length(95).parse(row.monero_address)); const walletFingerprint = HEX64.parse(row.wallet_fingerprint); const observationDigest = HEX64.parse(row.payload_digest); const scanId = UUID.parse(row.scan_id); const history = await client.query(`SELECT sequence::text,event_type,payload_digest,wallet_height::text AS wallet_height, observed_at::text AS observed_at,scan_id::text FROM wk_custody.pilot_monero_observation_events WHERE wallet_generation_id=$1 AND observation_id=$2 ORDER BY sequence LIMIT $3`, [input.walletGenerationId, input.observationId, MAX_EVENT_HISTORY + 1]); if (history.rows.length > MAX_EVENT_HISTORY) throw new Error('Pilot observation event history exceeds the review bound'); const eventHistory = validateEvents(history.rows, observationDigest); const unsigned = subjectWithoutDigestSchema.parse({ policyVersion: 'pilot-monero-deposit-review-v1', preparationOnly: true, crediting: 'disabled', marketNetwork: identity.marketNetwork, marketChainId: identity.marketChainId, moneroNetwork: identity.moneroNetwork, custodianId: identity.custodianId, activationStatus: identity.activationStatus, walletGenerationId: input.walletGenerationId, walletFingerprint, allocationId: allocation.allocation_id, beneficiary, accountIndex, addressIndex, moneroAddress, observationId: input.observationId, transactionId: txid, outputPublicKey, amountAtomicXmr, blockHeight, walletHeight, confirmations, minimumConfirmations: policy.minimumConfirmations, unlockTime: z.string().regex(UINT64).parse(row.unlock_time), locked: false, doubleSpendSeen: false, currentlyPresent: true, globalOutputIndex, observationDigest, scanId, scanFromHeight, scanThroughHeight, observerScanHeight, scanCompletedAt, observerUpdatedAt, lastObservedAt, scanFreshnessMs, maximumObservationAgeMs: policy.maximumObservationAgeMs, eventHistorySequence: eventHistory.sequence, eventHistoryCount: eventHistory.count, eventHistoryDigest: eventHistory.digest, preparedAt: now, }); const subject = reviewSubjectSchema.parse({ ...unsigned, subjectDigest: digest(JSON.stringify(unsigned)) }); await client.query('COMMIT'); return subject; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } }