Durable, beneficiary-bound subaddress index reservation

This commit is contained in:
2026-10-22 17:26:13 -04:00
parent 84e6e08209
commit 2ecb42c019
9 changed files with 416 additions and 42 deletions
+98 -3
View File
@@ -1,7 +1,7 @@
import { createHash } from 'node:crypto';
import type { Pool, PoolClient } from 'pg';
export const CUSTODY_PG_VERSION = 10;
export const CUSTODY_PG_VERSION = 11;
export const CUSTODY_WRITE_LOCK = '7140824361';
export const MONERO_OBSERVER_WORKER_LOCK = '7140824362';
export const CUSTODY_SCHEMA_V1 = `
@@ -383,7 +383,8 @@ CREATE TABLE wk_custody.pilot_custody_identity(
monero_network text NOT NULL CHECK(monero_network='mainnet'),
custodian_id uuid NOT NULL UNIQUE,
activation_status text NOT NULL CHECK(activation_status='identity-only'),
initialized_at bigint NOT NULL CHECK(initialized_at>=0)
initialized_at bigint NOT NULL CHECK(initialized_at>=0),
UNIQUE(custodian_id,market_network,market_chain_id,monero_network)
);
CREATE FUNCTION wk_custody.pilot_custody_identity_immutable() RETURNS trigger LANGUAGE plpgsql
SET search_path=wk_custody,pg_catalog AS $$
@@ -395,6 +396,65 @@ REVOKE ALL ON FUNCTION wk_custody.pilot_custody_identity_immutable() FROM PUBLIC
`;
export const CUSTODY_SCHEMA_V10_DIGEST = createHash('sha256')
.update(`${CUSTODY_SCHEMA_V1}\n-- custody migration v2 --\n${CUSTODY_MIGRATION_V2}\n-- custody migration v3 --\n${CUSTODY_MIGRATION_V3}\n-- custody migration v4 --\n${CUSTODY_MIGRATION_V4}\n-- custody migration v5 --\n${CUSTODY_MIGRATION_V5}\n-- custody migration v6 --\n${CUSTODY_MIGRATION_V6}\n-- custody migration v7 --\n${CUSTODY_MIGRATION_V7}\n-- custody migration v8 --\n${CUSTODY_MIGRATION_V8}\n-- custody migration v9 --\n${CUSTODY_MIGRATION_V9}\n-- custody migration v10 --\n${CUSTODY_MIGRATION_V10}`).digest('hex');
export const CUSTODY_MIGRATION_V11 = `
CREATE TABLE wk_custody.pilot_monero_deposit_reservations(
allocation_id uuid PRIMARY KEY,
request_id uuid NOT NULL UNIQUE,
custodian_id uuid NOT NULL,
market_network text NOT NULL CHECK(market_network='wikideck-randomx-pilot-1'),
market_chain_id text NOT NULL CHECK(market_chain_id ~ '^[a-f0-9]{64}$'),
monero_network text NOT NULL CHECK(monero_network='mainnet'),
beneficiary text NOT NULL CHECK(beneficiary ~ '^[a-f0-9]{64}$'),
wallet_generation_id uuid NOT NULL,
wallet_fingerprint text NOT NULL CHECK(wallet_fingerprint ~ '^[a-f0-9]{64}$'),
account_index bigint NOT NULL CHECK(account_index BETWEEN 0 AND 4294967295),
address_index bigint NOT NULL CHECK(address_index BETWEEN 0 AND 4294967295),
request_digest text NOT NULL CHECK(request_digest ~ '^[a-f0-9]{64}$'),
allocation_digest text NOT NULL CHECK(allocation_digest ~ '^[a-f0-9]{64}$'),
reserved_at bigint NOT NULL CHECK(reserved_at>=0),
UNIQUE(wallet_generation_id,account_index,address_index),
UNIQUE(custodian_id,market_network,market_chain_id,beneficiary,wallet_generation_id),
FOREIGN KEY(custodian_id,market_network,market_chain_id,monero_network)
REFERENCES wk_custody.pilot_custody_identity(custodian_id,market_network,market_chain_id,monero_network)
);
CREATE INDEX pilot_deposit_reservations_wallet_index
ON wk_custody.pilot_monero_deposit_reservations(wallet_generation_id,account_index,address_index);
CREATE FUNCTION wk_custody.pilot_monero_deposit_reservations_immutable() RETURNS trigger LANGUAGE plpgsql
SET search_path=wk_custody,pg_catalog AS $$
BEGIN RAISE EXCEPTION 'Pilot Monero deposit reservation history is immutable'; END $$;
CREATE TRIGGER pilot_monero_deposit_reservations_immutable
BEFORE UPDATE OR DELETE ON wk_custody.pilot_monero_deposit_reservations
FOR EACH ROW EXECUTE FUNCTION wk_custody.pilot_monero_deposit_reservations_immutable();
REVOKE ALL ON wk_custody.pilot_monero_deposit_reservations FROM PUBLIC;
REVOKE ALL ON FUNCTION wk_custody.pilot_monero_deposit_reservations_immutable() FROM PUBLIC;
CREATE FUNCTION wk_custody.pilot_reservation_database_isolated() RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER
SET search_path=wk_custody,pg_catalog AS $$
SELECT NOT (
EXISTS(SELECT 1 FROM wk_custody.custody_meta) OR
EXISTS(SELECT 1 FROM wk_custody.accounts) OR
EXISTS(SELECT 1 FROM wk_custody.journals) OR
EXISTS(SELECT 1 FROM wk_custody.entries) OR
EXISTS(SELECT 1 FROM wk_custody.listings) OR
EXISTS(SELECT 1 FROM wk_custody.orders) OR
EXISTS(SELECT 1 FROM wk_custody.withdrawals) OR
EXISTS(SELECT 1 FROM wk_custody.receipts) OR
EXISTS(SELECT 1 FROM wk_custody.outbox) OR
EXISTS(SELECT 1 FROM wk_custody.credits) OR
EXISTS(SELECT 1 FROM wk_custody.monero_observer_state) OR
EXISTS(SELECT 1 FROM wk_custody.monero_transfer_observations) OR
EXISTS(SELECT 1 FROM wk_custody.monero_observation_scans) OR
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_allocations) OR
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_credit_events) OR
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_reorg_holds) OR
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_release_events) OR
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_reorg_resolutions) OR
EXISTS(SELECT 1 FROM wk_custody.monero_withdrawal_intents)
)
$$;
REVOKE ALL ON FUNCTION wk_custody.pilot_reservation_database_isolated() FROM PUBLIC;
`;
export const CUSTODY_SCHEMA_V11_DIGEST = createHash('sha256')
.update(`${CUSTODY_SCHEMA_V1}\n-- custody migration v2 --\n${CUSTODY_MIGRATION_V2}\n-- custody migration v3 --\n${CUSTODY_MIGRATION_V3}\n-- custody migration v4 --\n${CUSTODY_MIGRATION_V4}\n-- custody migration v5 --\n${CUSTODY_MIGRATION_V5}\n-- custody migration v6 --\n${CUSTODY_MIGRATION_V6}\n-- custody migration v7 --\n${CUSTODY_MIGRATION_V7}\n-- custody migration v8 --\n${CUSTODY_MIGRATION_V8}\n-- custody migration v9 --\n${CUSTODY_MIGRATION_V9}\n-- custody migration v10 --\n${CUSTODY_MIGRATION_V10}\n-- custody migration v11 --\n${CUSTODY_MIGRATION_V11}`).digest('hex');
/** Retained alias for callers that need the initial schema definition in disposable fixtures. */
export const CUSTODY_SCHEMA = CUSTODY_SCHEMA_V1;
export const CUSTODY_SCHEMA_DIGEST = CUSTODY_SCHEMA_V10_DIGEST;
@@ -455,6 +515,11 @@ export async function migrateCustody(pool: Pool): Promise<void> {
if (current === 9) {
await c.query(CUSTODY_MIGRATION_V10);
await c.query('INSERT INTO wk_custody.schema_versions VALUES(10,$1)', [CUSTODY_SCHEMA_V10_DIGEST]);
current = 10;
}
if (current === 10) {
await c.query(CUSTODY_MIGRATION_V11);
await c.query('INSERT INTO wk_custody.schema_versions VALUES(11,$1)', [CUSTODY_SCHEMA_V11_DIGEST]);
}
if (current > CUSTODY_PG_VERSION) throw new Error('Custody PostgreSQL schema is newer than this application');
await verifyCustodySchema(c);
@@ -470,7 +535,7 @@ async function verifyCustodySchemaHistory(c: PoolClient): Promise<number> {
const rows = await c.query('SELECT version,digest FROM wk_custody.schema_versions ORDER BY version');
const expected = [CUSTODY_SCHEMA_V1_DIGEST, CUSTODY_SCHEMA_V2_DIGEST, CUSTODY_SCHEMA_V3_DIGEST,
CUSTODY_SCHEMA_V4_DIGEST, CUSTODY_SCHEMA_V5_DIGEST, CUSTODY_SCHEMA_V6_DIGEST, CUSTODY_SCHEMA_V7_DIGEST,
CUSTODY_SCHEMA_V8_DIGEST, CUSTODY_SCHEMA_V9_DIGEST, CUSTODY_SCHEMA_V10_DIGEST];
CUSTODY_SCHEMA_V8_DIGEST, CUSTODY_SCHEMA_V9_DIGEST, CUSTODY_SCHEMA_V10_DIGEST, CUSTODY_SCHEMA_V11_DIGEST];
if (!rows.rowCount || rows.rowCount > expected.length || rows.rows.some((row, index) =>
Number(row.version) !== index + 1 || row.digest !== expected[index])) {
throw new Error('Custody PostgreSQL schema version/digest history mismatch; explicit migration required');
@@ -598,6 +663,36 @@ export async function grantMoneroDepositAllocatorRuntime(pool: Pool, role: strin
finally { c.release(); }
}
/** Pilot reservation-only role. It can allocate durable indexes but cannot register addresses or access financial tables. */
export async function grantPilotDepositReservationRuntime(pool: Pool, role: string,
custodyRuntimeRole = 'custody_runtime', observerRole = 'monero_observer',
creditRole = 'monero_deposit_credit', mainnetAllocatorRole = 'monero_deposit_allocator'): Promise<void> {
if (!/^[a-z][a-z0-9_]{0,62}$/.test(role)) throw new Error('Invalid pilot deposit reservation role identifier');
if ([custodyRuntimeRole, observerRole, creditRole, mainnetAllocatorRole].includes(role)) {
throw new Error('Pilot deposit reservation role must be separate from other custody roles');
}
const name = `"${role}"`;
const c = await pool.connect();
try {
await c.query('BEGIN'); await verifyCustodySchema(c);
const r = await c.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolreplication,rolbypassrls FROM pg_roles WHERE rolname=$1', [role]);
if (!r.rowCount || Object.values(r.rows[0]).some(Boolean)) throw new Error('Pilot reservation role must exist without administrative privileges');
const memberships = await c.query('SELECT 1 FROM pg_auth_members WHERE member=(SELECT oid FROM pg_roles WHERE rolname=$1) LIMIT 1', [role]);
if (memberships.rowCount) throw new Error('Pilot reservation role must not inherit privileges through role membership');
const owner = await c.query("SELECT pg_get_userbyid(nspowner) AS owner FROM pg_namespace WHERE nspname='wk_custody'");
if (owner.rows[0].owner === role) throw new Error('Pilot reservation role must not own the custody schema');
await c.query(`GRANT USAGE ON SCHEMA wk_custody TO ${name};
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA wk_custody FROM ${name};
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA wk_custody FROM ${name};
GRANT SELECT ON wk_custody.schema_versions TO ${name};
GRANT SELECT ON wk_custody.pilot_custody_identity TO ${name};
GRANT SELECT,INSERT ON wk_custody.pilot_monero_deposit_reservations TO ${name};
GRANT EXECUTE ON FUNCTION wk_custody.pilot_reservation_database_isolated() TO ${name};`);
await c.query('COMMIT');
} catch (error) { await c.query('ROLLBACK').catch(() => {}); throw error; }
finally { c.release(); }
}
/** Mainnet customer withdrawal-intent role. It can reserve/cancel liabilities but has no signing or relay authority. */
export async function grantMoneroWithdrawalRuntime(pool: Pool, role: string,
custodyRuntimeRole = 'custody_runtime', observerRole = 'monero_observer',