Durable, beneficiary-bound subaddress index reservation
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
import { z } from 'zod';
|
||||
import { IS_PILOT, NETWORK } from '../../shared/network.ts';
|
||||
import { CHAIN_ID } from '../testnet/consensus.ts';
|
||||
import type { PilotCustodyIdentity } from './pilot-custody-identity.ts';
|
||||
import { assertPilotReservationDatabaseEmpty, verifyPilotCustodyIdentity } from './pilot-custody-identity.ts';
|
||||
import { validatePilotMoneroDepositRequest, type PilotDepositRequestContext } from './pilot-monero-deposit-request.ts';
|
||||
import { CUSTODY_WRITE_LOCK, verifyCustodySchema } from './postgres-schema.ts';
|
||||
|
||||
const MAX_INDEX = 0xffff_ffff;
|
||||
const uuidPattern = /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
||||
const hexPattern = /^[a-f0-9]{64}$/;
|
||||
|
||||
export type PilotDepositReservation = {
|
||||
allocationId: string;
|
||||
requestId: string;
|
||||
custodianId: string;
|
||||
marketNetwork: 'wikideck-randomx-pilot-1';
|
||||
marketChainId: string;
|
||||
moneroNetwork: 'mainnet';
|
||||
beneficiary: string;
|
||||
walletGenerationId: string;
|
||||
walletFingerprint: string;
|
||||
accountIndex: number;
|
||||
addressIndex: number;
|
||||
requestDigest: string;
|
||||
allocationDigest: string;
|
||||
reservedAt: number;
|
||||
};
|
||||
|
||||
type ReservationRow = {
|
||||
allocation_id: string; request_id: string; custodian_id: string;
|
||||
market_network: 'wikideck-randomx-pilot-1'; market_chain_id: string; monero_network: 'mainnet';
|
||||
beneficiary: string; wallet_generation_id: string; wallet_fingerprint: string;
|
||||
account_index: string; address_index: string; request_digest: string; allocation_digest: string; reserved_at: string;
|
||||
};
|
||||
|
||||
export type PilotDepositAllocationIntent = {
|
||||
requestId: string; custodianId: string; marketNetwork: 'wikideck-randomx-pilot-1'; marketChainId: string;
|
||||
moneroNetwork: 'mainnet'; beneficiary: string; walletGenerationId: string; walletFingerprint: string;
|
||||
accountIndex: number; requestDigest: string;
|
||||
};
|
||||
|
||||
function toReservation(row: ReservationRow): PilotDepositReservation {
|
||||
return { allocationId: row.allocation_id, requestId: row.request_id, custodianId: row.custodian_id,
|
||||
marketNetwork: row.market_network, marketChainId: row.market_chain_id, moneroNetwork: row.monero_network,
|
||||
beneficiary: row.beneficiary, walletGenerationId: row.wallet_generation_id, walletFingerprint: row.wallet_fingerprint,
|
||||
accountIndex: Number(row.account_index), addressIndex: Number(row.address_index), requestDigest: row.request_digest,
|
||||
allocationDigest: row.allocation_digest, reservedAt: Number(row.reserved_at) };
|
||||
}
|
||||
|
||||
function validateIdentity(identity: PilotCustodyIdentity, intent: PilotDepositAllocationIntent, reservedAt: number): void {
|
||||
if (!IS_PILOT || NETWORK !== 'wikideck-randomx-pilot-1' || identity.version !== 1 || identity.mode !== 'pilot-custody' ||
|
||||
identity.policy !== 'pilot-custody-v1' || identity.marketNetwork !== NETWORK || identity.marketChainId !== CHAIN_ID ||
|
||||
identity.moneroNetwork !== 'mainnet' || identity.activationStatus !== 'identity-only') {
|
||||
throw new Error('Pilot deposit reservation requires the configured identity-only pilot custody profile');
|
||||
}
|
||||
if (!Number.isSafeInteger(reservedAt) || reservedAt < 0 || !uuidPattern.test(intent.requestId) ||
|
||||
!uuidPattern.test(intent.custodianId) || !uuidPattern.test(intent.walletGenerationId) ||
|
||||
intent.custodianId.toLowerCase() !== identity.custodianId.toLowerCase() ||
|
||||
intent.marketNetwork !== identity.marketNetwork || intent.marketChainId !== identity.marketChainId ||
|
||||
intent.moneroNetwork !== identity.moneroNetwork || !hexPattern.test(intent.beneficiary) ||
|
||||
!hexPattern.test(intent.walletFingerprint) || !hexPattern.test(intent.requestDigest) ||
|
||||
!Number.isSafeInteger(intent.accountIndex) || intent.accountIndex < 0 || intent.accountIndex > MAX_INDEX) {
|
||||
throw new Error('Invalid pilot deposit allocation intent');
|
||||
}
|
||||
}
|
||||
|
||||
function allocationDigest(intent: PilotDepositAllocationIntent): string {
|
||||
return createHash('sha256').update(JSON.stringify([
|
||||
'wikideck-pilot-monero-deposit-allocation-v1', intent.custodianId, intent.marketNetwork,
|
||||
intent.marketChainId, intent.moneroNetwork, intent.beneficiary, intent.walletGenerationId,
|
||||
intent.walletFingerprint, intent.accountIndex,
|
||||
])).digest('hex');
|
||||
}
|
||||
|
||||
async function assertIdentityRecord(client: PoolClient, identity: PilotCustodyIdentity): Promise<void> {
|
||||
const result = await client.query(`SELECT version,mode,policy,market_network,market_chain_id,monero_network,
|
||||
custodian_id,activation_status FROM wk_custody.pilot_custody_identity WHERE singleton=1`);
|
||||
if (!result.rowCount) throw new Error('Pilot custody identity is not initialized');
|
||||
const row = result.rows[0];
|
||||
if (Number(row.version) !== identity.version || row.mode !== identity.mode || row.policy !== identity.policy ||
|
||||
row.market_network !== identity.marketNetwork || row.market_chain_id !== identity.marketChainId ||
|
||||
row.monero_network !== identity.moneroNetwork || row.custodian_id !== identity.custodianId ||
|
||||
row.activation_status !== identity.activationStatus) {
|
||||
throw new Error('Pilot custody identity changed or does not match the database binding');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a never-reused Monero address index for a signed, validated pilot request.
|
||||
* This does not call Wallet-RPC, create/register an address, or permit crediting funds.
|
||||
*/
|
||||
export async function reservePilotMoneroDepositIndex(pool: Pool, identity: PilotCustodyIdentity,
|
||||
intentInput: PilotDepositAllocationIntent, reservedAt = Date.now()): Promise<{ reservation: PilotDepositReservation; replayed: boolean }> {
|
||||
const intent = { ...intentInput, custodianId: intentInput.custodianId.toLowerCase() };
|
||||
validateIdentity(identity, intent, reservedAt);
|
||||
const allocationHash = allocationDigest(intent);
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'; SET LOCAL synchronous_commit=on");
|
||||
await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
|
||||
await verifyCustodySchema(client);
|
||||
await assertPilotReservationDatabaseEmpty(client);
|
||||
await assertIdentityRecord(client, identity);
|
||||
// Serialize index allocation across app processes using the same watch-only wallet/account.
|
||||
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1,0))', [
|
||||
`wikideck-pilot-monero-reservation-v1:${intent.walletGenerationId}:${intent.accountIndex}`,
|
||||
]);
|
||||
|
||||
const byRequest = await client.query<ReservationRow>(`SELECT * FROM wk_custody.pilot_monero_deposit_reservations
|
||||
WHERE request_id=$1`, [intent.requestId]);
|
||||
if (byRequest.rowCount) {
|
||||
const row = byRequest.rows[0];
|
||||
if (row.request_digest !== intent.requestDigest || row.allocation_digest !== allocationHash) {
|
||||
throw new Error('Pilot deposit request ID was reused with different signed terms');
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
return { reservation: toReservation(row), replayed: true };
|
||||
}
|
||||
|
||||
const byBeneficiary = await client.query<ReservationRow>(`SELECT * FROM wk_custody.pilot_monero_deposit_reservations
|
||||
WHERE custodian_id=$1 AND market_network=$2 AND market_chain_id=$3 AND beneficiary=$4
|
||||
AND wallet_generation_id=$5`, [intent.custodianId, intent.marketNetwork, intent.marketChainId,
|
||||
intent.beneficiary, intent.walletGenerationId]);
|
||||
if (byBeneficiary.rowCount) {
|
||||
const row = byBeneficiary.rows[0];
|
||||
if (row.allocation_digest !== allocationHash) {
|
||||
throw new Error('Beneficiary already has a different pilot reservation for this wallet generation');
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
return { reservation: toReservation(row), replayed: true };
|
||||
}
|
||||
|
||||
const next = await client.query<{ next_index: string }>(`SELECT (COALESCE(MAX(address_index),-1)+1)::text AS next_index
|
||||
FROM wk_custody.pilot_monero_deposit_reservations WHERE wallet_generation_id=$1 AND account_index=$2`,
|
||||
[intent.walletGenerationId, intent.accountIndex]);
|
||||
const index = BigInt(next.rows[0].next_index);
|
||||
if (index > BigInt(MAX_INDEX)) throw new Error('Pilot Monero subaddress index space is exhausted for this account');
|
||||
const inserted = await client.query<ReservationRow>(`INSERT INTO wk_custody.pilot_monero_deposit_reservations(
|
||||
allocation_id,request_id,custodian_id,market_network,market_chain_id,monero_network,beneficiary,
|
||||
wallet_generation_id,wallet_fingerprint,account_index,address_index,request_digest,allocation_digest,reserved_at)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, [randomUUID(), intent.requestId,
|
||||
intent.custodianId, intent.marketNetwork, intent.marketChainId, intent.moneroNetwork, intent.beneficiary,
|
||||
intent.walletGenerationId, intent.walletFingerprint, intent.accountIndex, index.toString(), intent.requestDigest,
|
||||
allocationHash, reservedAt]);
|
||||
await client.query('COMMIT');
|
||||
return { reservation: toReservation(inserted.rows[0]), replayed: false };
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
if (typeof error === 'object' && error !== null && 'code' in error && error.code === '23505') {
|
||||
throw new Error('Pilot deposit reservation conflicts with an existing request, beneficiary, or wallet index');
|
||||
}
|
||||
throw error;
|
||||
} finally { client.release(); }
|
||||
}
|
||||
|
||||
/** Full signed-request path: verify durable identity, cryptographically validate, then reserve an index. */
|
||||
export async function reservePilotMoneroDepositRequest(pool: Pool,
|
||||
context: Omit<PilotDepositRequestContext, 'identity'>,
|
||||
rawRequest: unknown, reservedAt = Date.now()) {
|
||||
const custodianId = z.object({ custodianId: z.string().uuid() }).passthrough().parse(rawRequest).custodianId;
|
||||
const identity = await verifyPilotCustodyIdentity(pool, custodianId);
|
||||
const intent = validatePilotMoneroDepositRequest({ ...context, identity }, rawRequest);
|
||||
return reservePilotMoneroDepositIndex(pool, identity, intent, reservedAt);
|
||||
}
|
||||
Reference in New Issue
Block a user