Implemented PostgreSQL v3 allocation-registry groundwork
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
|
||||
export const CUSTODY_PG_VERSION = 2;
|
||||
export const CUSTODY_PG_VERSION = 3;
|
||||
export const CUSTODY_WRITE_LOCK = '7140824361';
|
||||
export const CUSTODY_SCHEMA_V1 = `
|
||||
CREATE SCHEMA IF NOT EXISTS wk_custody;
|
||||
@@ -127,6 +127,54 @@ CREATE TRIGGER monero_observation_scans_no_update BEFORE UPDATE OR DELETE ON wk_
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V2_DIGEST = createHash('sha256')
|
||||
.update(`${CUSTODY_SCHEMA_V1}\n-- custody migration v2 --\n${CUSTODY_MIGRATION_V2}`).digest('hex');
|
||||
export const CUSTODY_MIGRATION_V3 = `
|
||||
CREATE TABLE wk_custody.monero_deposit_allocations(
|
||||
allocation_id uuid PRIMARY KEY,
|
||||
request_id uuid NOT NULL UNIQUE,
|
||||
custodian_id uuid NOT NULL,
|
||||
market_network text NOT NULL CHECK(market_network IN ('wikideck-randomx-mainnet-1','wikideck-randomx-testnet-6')),
|
||||
market_chain_id text NOT NULL CHECK(market_chain_id ~ '^[a-f0-9]{64}$'),
|
||||
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}$'),
|
||||
monero_network text NOT NULL CHECK(monero_network='mainnet'),
|
||||
account_index bigint NOT NULL CHECK(account_index BETWEEN 0 AND 4294967295),
|
||||
address_index bigint NOT NULL CHECK(address_index BETWEEN 0 AND 4294967295),
|
||||
status text NOT NULL CHECK(status IN ('reserved','registered')),
|
||||
monero_address text UNIQUE CHECK(monero_address IS NULL OR length(monero_address)=95),
|
||||
request_digest text NOT NULL CHECK(request_digest ~ '^[a-f0-9]{64}$'),
|
||||
reserved_at bigint NOT NULL CHECK(reserved_at>=0),
|
||||
registered_at bigint CHECK(registered_at IS NULL OR registered_at>=reserved_at),
|
||||
CHECK((status='reserved' AND monero_address IS NULL AND registered_at IS NULL) OR
|
||||
(status='registered' AND monero_address IS NOT NULL AND registered_at IS NOT NULL)),
|
||||
UNIQUE(wallet_generation_id,account_index,address_index),
|
||||
UNIQUE(custodian_id,market_network,market_chain_id,beneficiary,wallet_generation_id)
|
||||
);
|
||||
CREATE INDEX monero_deposit_allocations_beneficiary
|
||||
ON wk_custody.monero_deposit_allocations(custodian_id,market_network,market_chain_id,beneficiary);
|
||||
CREATE FUNCTION wk_custody.monero_deposit_allocations_immutable() RETURNS trigger LANGUAGE plpgsql
|
||||
SET search_path=wk_custody,pg_catalog AS $$
|
||||
BEGIN
|
||||
IF TG_OP='DELETE' THEN RAISE EXCEPTION 'Monero deposit allocation history cannot be deleted'; END IF;
|
||||
IF OLD.status<>'reserved' OR NEW.status<>'registered' OR OLD.monero_address IS NOT NULL OR NEW.monero_address IS NULL OR
|
||||
NEW.allocation_id IS DISTINCT FROM OLD.allocation_id OR NEW.request_id IS DISTINCT FROM OLD.request_id OR
|
||||
NEW.custodian_id IS DISTINCT FROM OLD.custodian_id OR NEW.market_network IS DISTINCT FROM OLD.market_network OR
|
||||
NEW.market_chain_id IS DISTINCT FROM OLD.market_chain_id OR NEW.beneficiary IS DISTINCT FROM OLD.beneficiary OR
|
||||
NEW.wallet_generation_id IS DISTINCT FROM OLD.wallet_generation_id OR NEW.wallet_fingerprint IS DISTINCT FROM OLD.wallet_fingerprint OR
|
||||
NEW.monero_network IS DISTINCT FROM OLD.monero_network OR NEW.account_index IS DISTINCT FROM OLD.account_index OR
|
||||
NEW.address_index IS DISTINCT FROM OLD.address_index OR NEW.request_digest IS DISTINCT FROM OLD.request_digest OR
|
||||
NEW.reserved_at IS DISTINCT FROM OLD.reserved_at OR NEW.registered_at IS NULL OR NEW.registered_at<OLD.reserved_at THEN
|
||||
RAISE EXCEPTION 'Monero deposit allocation identity is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER monero_deposit_allocations_immutable BEFORE UPDATE OR DELETE ON wk_custody.monero_deposit_allocations
|
||||
FOR EACH ROW EXECUTE FUNCTION wk_custody.monero_deposit_allocations_immutable();
|
||||
REVOKE ALL ON wk_custody.monero_deposit_allocations FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION wk_custody.monero_deposit_allocations_immutable() FROM PUBLIC;
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V3_DIGEST = createHash('sha256')
|
||||
.update(`${CUSTODY_SCHEMA_V1}\n-- custody migration v2 --\n${CUSTODY_MIGRATION_V2}\n-- custody migration v3 --\n${CUSTODY_MIGRATION_V3}`).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_V2_DIGEST;
|
||||
@@ -147,6 +195,11 @@ export async function migrateCustody(pool: Pool): Promise<void> {
|
||||
if (current === 1) {
|
||||
await c.query(CUSTODY_MIGRATION_V2);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(2,$1)', [CUSTODY_SCHEMA_V2_DIGEST]);
|
||||
current = 2;
|
||||
}
|
||||
if (current === 2) {
|
||||
await c.query(CUSTODY_MIGRATION_V3);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(3,$1)', [CUSTODY_SCHEMA_V3_DIGEST]);
|
||||
}
|
||||
if (current > CUSTODY_PG_VERSION) throw new Error('Custody PostgreSQL schema is newer than this application');
|
||||
await verifyCustodySchema(c);
|
||||
@@ -160,7 +213,7 @@ export async function verifyCustodySchema(c: PoolClient): Promise<void> {
|
||||
}
|
||||
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];
|
||||
const expected = [CUSTODY_SCHEMA_V1_DIGEST, CUSTODY_SCHEMA_V2_DIGEST, CUSTODY_SCHEMA_V3_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');
|
||||
@@ -189,6 +242,7 @@ export async function grantCustodyRuntime(pool: Pool, role: string): Promise<voi
|
||||
GRANT INSERT,UPDATE ON wk_custody.listings,wk_custody.orders,wk_custody.withdrawals,wk_custody.outbox TO ${name};`);
|
||||
await c.query(`REVOKE ALL PRIVILEGES ON wk_custody.monero_observer_state,wk_custody.monero_transfer_observations,
|
||||
wk_custody.monero_observation_events,wk_custody.monero_observation_scans FROM ${name};
|
||||
REVOKE ALL PRIVILEGES ON wk_custody.monero_deposit_allocations FROM ${name};
|
||||
REVOKE ALL PRIVILEGES ON SEQUENCE wk_custody.monero_observation_events_sequence_seq FROM ${name};`);
|
||||
await c.query('COMMIT');
|
||||
} catch (e) { await c.query('ROLLBACK').catch(() => {}); throw e; }
|
||||
|
||||
Reference in New Issue
Block a user