Reviewed release and reorg-hold resolution
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 = 5;
|
||||
export const CUSTODY_PG_VERSION = 7;
|
||||
export const CUSTODY_WRITE_LOCK = '7140824361';
|
||||
export const CUSTODY_SCHEMA_V1 = `
|
||||
CREATE SCHEMA IF NOT EXISTS wk_custody;
|
||||
@@ -254,9 +254,61 @@ REVOKE ALL ON FUNCTION wk_custody.monero_deposit_credit_history_immutable() FROM
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V5_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}`).digest('hex');
|
||||
export const CUSTODY_MIGRATION_V6 = `
|
||||
CREATE TABLE wk_custody.monero_deposit_release_events(
|
||||
release_id uuid PRIMARY KEY,
|
||||
credit_id uuid NOT NULL UNIQUE REFERENCES wk_custody.monero_deposit_credit_events(credit_id),
|
||||
journal_id text NOT NULL UNIQUE REFERENCES wk_custody.journals(id),
|
||||
reviewer_owner text NOT NULL CHECK(reviewer_owner ~ '^[a-f0-9]{64}$'),
|
||||
reviewer_public_key text NOT NULL CHECK(length(reviewer_public_key) BETWEEN 1 AND 256),
|
||||
review_nonce uuid NOT NULL,
|
||||
review_expires_at bigint NOT NULL CHECK(review_expires_at>=0),
|
||||
review_signature text NOT NULL CHECK(length(review_signature) BETWEEN 1 AND 256),
|
||||
review_subject jsonb NOT NULL,
|
||||
review_digest text NOT NULL CHECK(review_digest ~ '^[a-f0-9]{64}$'),
|
||||
released_at bigint NOT NULL CHECK(released_at>=0),
|
||||
UNIQUE(reviewer_owner,review_nonce)
|
||||
);
|
||||
CREATE TABLE wk_custody.monero_deposit_reorg_resolutions(
|
||||
resolution_id uuid PRIMARY KEY,
|
||||
hold_id uuid NOT NULL UNIQUE REFERENCES wk_custody.monero_deposit_reorg_holds(hold_id),
|
||||
credit_id uuid NOT NULL REFERENCES wk_custody.monero_deposit_credit_events(credit_id),
|
||||
release_id uuid NOT NULL REFERENCES wk_custody.monero_deposit_release_events(release_id),
|
||||
resolved_by text NOT NULL CHECK(resolved_by ~ '^[a-f0-9]{64}$'),
|
||||
resolved_at bigint NOT NULL CHECK(resolved_at>=0),
|
||||
disposition text NOT NULL CHECK(disposition='revalidated-for-release'),
|
||||
resolution_kind text NOT NULL CHECK(resolution_kind IN ('release-review','post-release-review')),
|
||||
reviewer_public_key text NOT NULL CHECK(length(reviewer_public_key) BETWEEN 1 AND 256),
|
||||
review_nonce uuid NOT NULL,
|
||||
review_expires_at bigint NOT NULL CHECK(review_expires_at>=0),
|
||||
review_signature text NOT NULL CHECK(length(review_signature) BETWEEN 1 AND 256),
|
||||
review_subject jsonb NOT NULL,
|
||||
review_digest text NOT NULL CHECK(review_digest ~ '^[a-f0-9]{64}$'),
|
||||
UNIQUE(hold_id,release_id),
|
||||
UNIQUE(resolved_by,review_nonce)
|
||||
);
|
||||
CREATE INDEX monero_deposit_resolutions_credit ON wk_custody.monero_deposit_reorg_resolutions(credit_id,hold_id);
|
||||
CREATE FUNCTION wk_custody.monero_deposit_release_history_immutable() RETURNS trigger LANGUAGE plpgsql
|
||||
SET search_path=wk_custody,pg_catalog AS $$
|
||||
BEGIN RAISE EXCEPTION 'Monero deposit release/resolution history is append-only'; END $$;
|
||||
CREATE TRIGGER monero_deposit_release_events_no_update BEFORE UPDATE OR DELETE ON wk_custody.monero_deposit_release_events
|
||||
FOR EACH ROW EXECUTE FUNCTION wk_custody.monero_deposit_release_history_immutable();
|
||||
CREATE TRIGGER monero_deposit_reorg_resolutions_no_update BEFORE UPDATE OR DELETE ON wk_custody.monero_deposit_reorg_resolutions
|
||||
FOR EACH ROW EXECUTE FUNCTION wk_custody.monero_deposit_release_history_immutable();
|
||||
REVOKE ALL ON wk_custody.monero_deposit_release_events,wk_custody.monero_deposit_reorg_resolutions FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION wk_custody.monero_deposit_release_history_immutable() FROM PUBLIC;
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V6_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}`).digest('hex');
|
||||
export const CUSTODY_MIGRATION_V7 = `
|
||||
ALTER TABLE wk_custody.monero_deposit_reorg_resolutions
|
||||
DROP CONSTRAINT monero_deposit_reorg_resolutions_resolved_by_review_nonce_key;
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V7_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}`).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_V5_DIGEST;
|
||||
export const CUSTODY_SCHEMA_DIGEST = CUSTODY_SCHEMA_V7_DIGEST;
|
||||
|
||||
/** Explicit operator migration; the HTTP runtime never creates or upgrades schema. */
|
||||
export async function migrateCustody(pool: Pool): Promise<void> {
|
||||
@@ -289,6 +341,16 @@ export async function migrateCustody(pool: Pool): Promise<void> {
|
||||
if (current === 4) {
|
||||
await c.query(CUSTODY_MIGRATION_V5);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(5,$1)', [CUSTODY_SCHEMA_V5_DIGEST]);
|
||||
current = 5;
|
||||
}
|
||||
if (current === 5) {
|
||||
await c.query(CUSTODY_MIGRATION_V6);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(6,$1)', [CUSTODY_SCHEMA_V6_DIGEST]);
|
||||
current = 6;
|
||||
}
|
||||
if (current === 6) {
|
||||
await c.query(CUSTODY_MIGRATION_V7);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(7,$1)', [CUSTODY_SCHEMA_V7_DIGEST]);
|
||||
}
|
||||
if (current > CUSTODY_PG_VERSION) throw new Error('Custody PostgreSQL schema is newer than this application');
|
||||
await verifyCustodySchema(c);
|
||||
@@ -303,7 +365,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, CUSTODY_SCHEMA_V3_DIGEST,
|
||||
CUSTODY_SCHEMA_V4_DIGEST, CUSTODY_SCHEMA_V5_DIGEST];
|
||||
CUSTODY_SCHEMA_V4_DIGEST, CUSTODY_SCHEMA_V5_DIGEST, CUSTODY_SCHEMA_V6_DIGEST, CUSTODY_SCHEMA_V7_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');
|
||||
@@ -386,11 +448,13 @@ export async function grantMoneroDepositCreditRuntime(pool: Pool, role: string,
|
||||
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA wk_custody FROM ${name};
|
||||
GRANT SELECT ON wk_custody.custody_meta,wk_custody.monero_observer_state,wk_custody.monero_transfer_observations,
|
||||
wk_custody.monero_observation_events,wk_custody.monero_deposit_allocations,
|
||||
wk_custody.monero_deposit_credit_events,wk_custody.monero_deposit_reorg_holds TO ${name};
|
||||
wk_custody.monero_deposit_credit_events,wk_custody.monero_deposit_reorg_holds,
|
||||
wk_custody.monero_deposit_release_events,wk_custody.monero_deposit_reorg_resolutions TO ${name};
|
||||
GRANT SELECT(id,normal) ON wk_custody.accounts TO ${name};
|
||||
GRANT INSERT(id,normal) ON wk_custody.accounts TO ${name};
|
||||
GRANT INSERT(id,kind,created) ON wk_custody.journals TO ${name};
|
||||
GRANT INSERT ON wk_custody.entries,wk_custody.monero_deposit_credit_events,wk_custody.monero_deposit_reorg_holds TO ${name};`);
|
||||
GRANT INSERT ON wk_custody.entries,wk_custody.monero_deposit_credit_events,wk_custody.monero_deposit_reorg_holds,
|
||||
wk_custody.monero_deposit_release_events,wk_custody.monero_deposit_reorg_resolutions TO ${name};`);
|
||||
await c.query('COMMIT');
|
||||
} catch (error) { await c.query('ROLLBACK').catch(() => {}); throw error; }
|
||||
finally { c.release(); }
|
||||
|
||||
Reference in New Issue
Block a user