Bounded test succeeded; native RandomX miner accepted one block

This commit is contained in:
2026-10-22 21:25:39 -04:00
parent 176d1927a8
commit bd3d36a7e1
12 changed files with 607 additions and 20 deletions
+14 -2
View File
@@ -7,6 +7,8 @@ export const CUSTODY_DB_ACTION_ARGUMENT_COUNTS = {
'grant-pilot-deposit-reservation-runtime': 1,
'grant-pilot-deposit-address-runtime': 1,
'grant-pilot-monero-observer-runtime': 1,
'grant-pilot-monero-review-runtime': 1,
'pilot-deposit-prepare-review': 2,
'grant-runtime': 1,
'grant-monero-observer-runtime': 1,
'grant-monero-deposit-allocator-runtime': 1,
@@ -57,10 +59,11 @@ export function assertMainnetCustodyOperationAcknowledged(action: CustodyDbActio
export function assertPilotCustodyActionAllowed(action: CustodyDbAction, isPilot: boolean): void {
const pilotActions = ['migrate-pilot-schema', 'initialize-pilot-custody-identity',
'grant-pilot-deposit-reservation-runtime', 'grant-pilot-deposit-address-runtime', 'grant-pilot-monero-observer-runtime'];
'grant-pilot-deposit-reservation-runtime', 'grant-pilot-deposit-address-runtime', 'grant-pilot-monero-observer-runtime',
'grant-pilot-monero-review-runtime', 'pilot-deposit-prepare-review'];
if ((isPilot && !pilotActions.includes(action)) || (!isPilot && pilotActions.includes(action))) {
throw new Error(isPilot
? 'Pilot profile permits only pilot schema migration and identity-only initialization through this CLI'
? 'Pilot profile permits only pilot-specific schema, identity, scoped-role, and review-preparation operations through this CLI'
: 'Pilot custody operation requires the pilot profile');
}
}
@@ -83,6 +86,15 @@ export function assertPilotObserverGrantAcknowledged(action: CustodyDbAction, ac
}
}
export function assertPilotDepositReviewAcknowledged(action: CustodyDbAction, acknowledgement: string | undefined): void {
if (action === 'grant-pilot-monero-review-runtime' && acknowledgement !== 'read-only-pilot-review-role-only') {
throw new Error('Pilot review role grant requires the exact read-only-role acknowledgement');
}
if (action === 'pilot-deposit-prepare-review' && acknowledgement !== 'pilot-deposit-review-preparation-only') {
throw new Error('Pilot deposit review preparation requires the exact preparation-only acknowledgement');
}
}
export function assertPilotSchemaMigrationAcknowledged(action: CustodyDbAction, acknowledgement: string | undefined): void {
if (action === 'migrate-pilot-schema' && acknowledgement !== 'explicit-pilot-schema-migration-only') {
throw new Error('Pilot schema migration requires explicit pilot schema-migration acknowledgement');
+3 -3
View File
@@ -26,7 +26,7 @@ function requirePilot(custodianId: string, initializedAt: number): void {
if (!Number.isSafeInteger(initializedAt) || initializedAt < 0) throw new Error('Invalid pilot identity initialization time');
}
function identityFromRow(row: Record<string, unknown>): PilotCustodyIdentity {
export function pilotCustodyIdentityFromRow(row: Record<string, unknown>): PilotCustodyIdentity {
const identity = {
version: Number(row.version), mode: row.mode, policy: row.policy,
marketNetwork: row.market_network, marketChainId: row.market_chain_id,
@@ -78,7 +78,7 @@ export async function initializePilotCustodyIdentity(pool: Pool, custodianId: st
await verifyCustodySchema(client);
const current = await client.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1 FOR UPDATE');
if (current.rowCount) {
const identity = identityFromRow(current.rows[0]);
const identity = pilotCustodyIdentityFromRow(current.rows[0]);
if (identity.version !== expected.version || identity.mode !== expected.mode || identity.policy !== expected.policy ||
identity.marketNetwork !== expected.marketNetwork || identity.marketChainId !== expected.marketChainId ||
identity.moneroNetwork !== expected.moneroNetwork || identity.custodianId !== expected.custodianId) {
@@ -103,7 +103,7 @@ export async function verifyPilotCustodyIdentity(pool: Pool, custodianId: string
requirePilot(custodianId, 0);
const result = await pool.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1');
if (result.rowCount !== 1) throw new Error('Pilot custody identity is not initialized');
const identity = identityFromRow(result.rows[0]);
const identity = pilotCustodyIdentityFromRow(result.rows[0]);
if (identity.custodianId !== custodianId.toLowerCase()) throw new Error('Pilot custody identity does not match configured custodian');
return identity;
}
+265
View File
@@ -0,0 +1,265 @@
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<typeof subjectWithoutDigestSchema> & { 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<string, unknown>[], 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<PilotDepositReviewSubject> {
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();
}
}
+58 -1
View File
@@ -1,4 +1,7 @@
import { lstat, readFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { open } from 'node:fs/promises';
import { X509Certificate } from 'node:crypto';
import type { PoolConfig } from 'pg';
export function custodyStorage(env: NodeJS.ProcessEnv = process.env): 'sqlite' | 'postgres' {
@@ -13,6 +16,60 @@ async function secretFile(path: string | undefined): Promise<string> {
if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) throw new Error('PostgreSQL secret file permissions are too broad');
return (await readFile(path, 'utf8')).trim();
}
function derLength(bytes: Buffer, offset: number): { length: number; next: number } {
if (offset >= bytes.length) throw new Error('Invalid certificate DER length');
const first = bytes[offset];
if (first < 0x80) return { length: first, next: offset + 1 };
const count = first & 0x7f;
if (count === 0 || count > 4 || offset + count >= bytes.length) throw new Error('Invalid certificate DER length');
let length = 0;
for (let i = 1; i <= count; i++) length = length * 256 + bytes[offset + i];
return { length, next: offset + count + 1 };
}
function derElement(bytes: Buffer, offset: number): { tag: number; start: number; end: number; next: number } {
if (offset >= bytes.length) throw new Error('Invalid certificate DER');
const tag = bytes[offset], size = derLength(bytes, offset + 1), end = size.next + size.length;
if (end > bytes.length) throw new Error('Invalid certificate DER');
return { tag, start: size.next, end, next: end };
}
function validateCaCertificate(pem: Buffer): string {
const certificate = new X509Certificate(pem);
const now = Date.now();
if (!Number.isFinite(Date.parse(certificate.validFrom)) || !Number.isFinite(Date.parse(certificate.validTo)) ||
now < Date.parse(certificate.validFrom) || now > Date.parse(certificate.validTo)) {
throw new Error('PostgreSQL CA certificate is not currently valid');
}
if (certificate.subject !== certificate.issuer || !certificate.verify(certificate.publicKey)) {
throw new Error('PostgreSQL CA certificate must be a valid self-signed trust anchor');
}
const key = certificate.publicKey;
if (key.asymmetricKeyType !== 'rsa' || (key.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) {
throw new Error('PostgreSQL CA certificate key must be RSA-2048 or stronger');
}
const der = certificate.raw;
const outer = derElement(der, 0);
if (outer.tag !== 0x30) throw new Error('Invalid PostgreSQL CA certificate DER');
const tbs = derElement(der, outer.start);
const signatureAlgorithm = derElement(der, tbs.next);
if (signatureAlgorithm.tag !== 0x30) throw new Error('Invalid PostgreSQL CA signature algorithm');
const oid = derElement(der, signatureAlgorithm.start);
if (oid.tag !== 0x06) throw new Error('Invalid PostgreSQL CA signature algorithm');
const signatureOid = der.subarray(oid.start, oid.end).toString('hex');
const weakSignatureOids = new Set(['2a864886f70d010104', '2b0e03021a', '2b0e03021b']);
if (weakSignatureOids.has(signatureOid)) throw new Error('PostgreSQL CA certificate uses a weak signature algorithm');
return pem.toString('utf8');
}
async function publicCaFile(path: string | undefined): Promise<string | undefined> {
if (!path) return undefined;
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const info = await file.stat();
if (!info.isFile() || info.size < 1 || info.size > 64 * 1024) throw new Error('Invalid PostgreSQL CA file');
// A CA certificate is public trust material; mode 0644 is expected. It is not a database credential.
const bytes = await file.readFile();
return validateCaCertificate(bytes);
} finally { await file.close(); }
}
/** No URL logging, no TLS options in a connection string overriding verified transport. */
export async function postgresConfig(env: NodeJS.ProcessEnv = process.env): Promise<PoolConfig> {
try {
@@ -33,7 +90,7 @@ export async function postgresConfig(env: NodeJS.ProcessEnv = process.env): Prom
if (insecure && !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) throw new Error('Non-TLS test connections must be loopback');
if (!insecure && (!url.username || !url.password)) throw new Error('PostgreSQL credentials required');
if (url.port && (!/^\d+$/.test(url.port) || Number(url.port) < 1 || Number(url.port) > 65535)) throw new Error('Invalid PostgreSQL port');
const ca = env.WIKIDECK_CUSTODY_PG_CA_FILE ? await secretFile(env.WIKIDECK_CUSTODY_PG_CA_FILE) : undefined;
const ca = await publicCaFile(env.WIKIDECK_CUSTODY_PG_CA_FILE);
return { host: url.hostname.replace(/^\[|\]$/g, ''), port: url.port ? Number(url.port) : 5432,
database: url.pathname.slice(1), user: decodeURIComponent(url.username), password: decodeURIComponent(url.password),
// Infrastructure mode is explicit plaintext on the driver's hop. It does NOT configure or
+36
View File
@@ -964,6 +964,42 @@ export async function grantPilotMoneroObserverRuntime(pool: Pool, role: string,
finally { c.release(); }
}
/** Read-only pilot review role. It can inspect persisted evidence, but not observer or financial state. */
export async function grantPilotMoneroDepositReviewRuntime(pool: Pool, role: string,
addressRole = 'pilot_deposit_address', reservationRole = 'pilot_deposit_reservation',
observerRole = 'pilot_monero_observer', custodyRuntimeRole = 'custody_runtime',
creditRole = 'monero_deposit_credit'): Promise<void> {
if (!/^[a-z][a-z0-9_]{0,62}$/.test(role)) throw new Error('Invalid pilot deposit review role identifier');
if ([addressRole, reservationRole, observerRole, custodyRuntimeRole, creditRole].includes(role)) {
throw new Error('Pilot deposit review role must be separate from observer, allocator, custody, and credit 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 deposit review 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 deposit review 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 deposit review role must not own the custody schema');
const owned = await c.query(`SELECT 1 FROM pg_class rel JOIN pg_namespace n ON n.oid=rel.relnamespace
WHERE n.nspname='wk_custody' AND rel.relowner=(SELECT oid FROM pg_roles WHERE rolname=$1) LIMIT 1`, [role]);
if (owned.rowCount) throw new Error('Pilot deposit review role must not own custody tables or sequences');
const databaseCreate = await c.query('SELECT has_database_privilege($1,current_database(),\'CREATE\') AS allowed', [role]);
if (databaseCreate.rows[0].allowed) throw new Error('Pilot deposit review role must not create database objects');
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,wk_custody.pilot_custody_identity,
wk_custody.pilot_monero_deposit_reservations,wk_custody.pilot_monero_observer_state,
wk_custody.pilot_monero_observation_scans,wk_custody.pilot_monero_observations,
wk_custody.pilot_monero_observation_events 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',