18 KiB
Executable File
Private pilot deployment: mock-only foundation
Status: application, isolated PostgreSQL sandbox, and opt-in one-shot observer definition. This is not a real-XMR stack, a public node, or approval to accept deposits. The operator already hosts a Monero full node and intends the pilot to reuse it; Compose does not start a daemon or connect to a Wallet-RPC gateway by default. Observer execution requires separately provisioned external services and protected files. Deposit enrollment and withdrawal execution remain unavailable.
Start the isolated profile
Use compose.custody-pilot.yaml with the stable project name wikideck-xmr-pilot. Do not combine it with the existing local custody sandbox, the disposable PostgreSQL fixture, or another pilot project name. Project-scoped volumes hold the pilot's PostgreSQL database, credentials, and node state separately.
Build/start with docker compose -p wikideck-xmr-pilot -f compose.custody-pilot.yaml up -d --build. Open http://127.0.0.1:3423/ and confirm the page identifies PRIVATE PILOT. The service binds to loopback by default; set WIKIDECK_PILOT_PORT only when changing the local port.
The app and browser bundle both select the pilot identity. Startup requires WIKIDECK_PILOT_MODE=private-mock-only; payments are fixed to mock. The market sandbox and PostgreSQL schema are freshly bound to this distinct chain ID. Do not point this stack at existing testnet/mainnet node or custody volumes.
To stop while preserving pilot data, run docker compose -p wikideck-xmr-pilot -f compose.custody-pilot.yaml down without volume removal. Do not use --volumes unless the database, credentials, and node history are intentionally being destroyed.
What this enables—and does not
The independent private chain uses a fixed pilot genesis time and its own deterministic chain/genesis identity. The app can run with the current sandbox features and mock balances while test cards and UI flows are exercised. Testnet and mainnet candidate identities and launch gates remain unchanged.
The default stack does not connect to the operator's Monero daemon, create deposit addresses, scan a wallet, approve credits, construct or relay withdrawals, or settle NFT sales against active consensus. An opt-in one-shot observer service is defined below, but it cannot run without a separately provisioned watch-only Wallet-RPC gateway, external pilot custody database, and protected credentials. Having a full node supplies only the blockchain daemon; it is not Wallet-RPC, a watch-only deposit wallet, or a spending wallet. Do not expose the service publicly or solicit funds.
Pilot custody identity and deposit reservations (schema v14)
Schema v10 added the immutable identity binding with separate fields for Wikideck market network/chain (wikideck-randomx-pilot-1 and pilot chain ID) and Monero network (mainnet). Schema v11 adds the separate pilot reservation table; v12 adds one-time address registration; v13 persists a create-intent state before the Wallet-RPC side effect. A signed beneficiary/wallet request reserves a never-reused subaddress index transactionally, with request replay, same-beneficiary recovery, concurrent index allocation, and conflict protection. Mainnet allocation/withdrawal constraints and v1-v12 migration definitions remain unchanged.
The reservation table has a composite foreign key to the immutable identity row, so direct SQL inserts cannot bind a reservation to a different custodian, Wikideck chain, or Monero network. The focused disposable PostgreSQL lifecycle test verifies index sequencing, replay, concurrent reservations, beneficiary conflict, and denied financial/address access for the restricted role. This implementation still does not create any address.
Use a separate fresh custody database for this identity exercise. The existing pilot Compose database contains mock sandbox custody and is not eligible; importing or reinterpreting its mock journals as Monero liabilities is prohibited. An operator migration is explicitly gated; identity initialization also refuses populated financial, mock, observer, allocation, credit, release or withdrawal tables.
After an operator creates/provisions a distinct empty database and its schema-owner connection, the CLI workflow is:
- With the
pilotprofile selected, runcustody:db migrate-pilot-schemawithWIKIDECK_PILOT_SCHEMA_MIGRATION=explicit-pilot-schema-migration-only. - Run
custody:db initialize-pilot-custody-identity <CUSTODIAN_UUID>withWIKIDECK_PILOT_MODE=private-mock-only,WIKIDECK_PILOT_CUSTODY_IDENTITY=initialize-pilot-custody-v1, andWIKIDECK_PILOT_CUSTODY_OPERATIONS=identity-only-no-funds. - If using the reservation library against that database, create a separate least-privilege role out of band and run
custody:db grant-pilot-deposit-reservation-runtime <ROLE>withWIKIDECK_PILOT_CUSTODY_OPERATIONS=reservation-only-no-addresses. - Before any operator-triggered address exercise, create a separate Wallet-RPC role out of band and run
custody:db grant-pilot-deposit-address-runtime <ROLE>withWIKIDECK_PILOT_CUSTODY_OPERATIONS=operator-triggered-addresses-only.
The schema migration does not grant runtime roles. The reservation role can read schema-version digests and the immutable pilot identity, execute a fixed isolation predicate, and read/insert immutable reservation rows. It cannot read custody metadata, balances, journals, mock custody, observer data, withdrawal intents, or alter reservation history. The isolation predicate uses a fixed-search-path schema-owner function; the role does not receive broad SELECT access just to check that the dedicated database remains unpopulated.
The CLI refuses other custody actions on the pilot profile; mainnet keeps its existing separate controls.
The operator/library-only address path verifies the signed pilot request, checks both network identities and configured wallet-generation/fingerprint/account, and uses the narrow Wallet-RPC client to inspect/create at the exact reserved index. It requires the stable reservation label and independently reads back/validates the returned mainnet subaddress before recording it. A create-intent timestamp is committed before the RPC side effect. If the process loses the response but the address exists at that exact index/label, a retry recovers it without creating another address. If no matching address exists after a create intent, retries stop with an uncertain outcome for manual reconciliation; they do not issue another create call. An unexpected consumed index is likewise held for operator investigation.
There is still no HTTP enrollment route, observer startup, customer credit, withdrawal, or sale activation. The function returns an address only to its operator/library caller. Keep Wallet-RPC off the public Internet and the credentials private. Existing mainnet enrollment remains unchanged. Do not treat an allocated address as approval to accept funds.
Reusing the operator-hosted daemon
The next integration should point the observer/gateway to the existing daemon rather than deploy or synchronize another one. An operator-triggered, read-only get_info probe is now available as a first connectivity check. It checks the expected Monero network, synchronized/local height, and daemon status; it does not open a wallet or provide a financial proof. The probe is opt-in and does not run on app startup.
Create a protected JSON config file outside the repository with network set to mainnet (or stagenet for a rehearsal), url set to the daemon's private literal-IP /json_rpc endpoint with its explicit port, and allowPrivateHttp set to true only when deliberately using HTTP on a trusted private network. For example:
{
"network": "mainnet",
"url": "http://192.168.1.20:18081/json_rpc",
"allowPrivateHttp": true
}
Replace the example address with the operator's actual daemon address. Protect the file with owner-only permissions/ACLs. No credentials belong in this file. The endpoint is deliberately restricted to a private literal IP; public hosts and DNS names are rejected.
From the pilot Compose project, run docker compose -p wikideck-xmr-pilot -f compose.custody-pilot.yaml --profile monero-check run --rm daemon-check; set WIKIDECK_MONERO_DAEMON_CONFIG_HOST to the config file path if it is not at the documented default. This runs from the Compose network, making it a better reachability check than testing only from the host. It emits the network, height, version and result, but not the endpoint. The current probe does not implement Monero's Digest --rpc-login; if the daemon requires it, do not disable auth or publish RPC to make this check work. Put a separately reviewed, authenticated TLS proxy on the private path or defer the check until a secure supported transport is configured.
Before wiring it into observer/gateway services, also verify the daemon's software version and operation policy. Mainnet is required for the real-XMR run; Stagenet rehearsal needs a separate Stagenet daemon endpoint. A daemon that is reachable on the operator's LAN is not automatically reachable from Docker containers.
Keep daemon RPC private to the pilot/operator network, bound to an appropriate private interface, and firewall-allowlisted to the pilot host. Prefer a supported authenticated TLS boundary; do not publish daemon RPC or Wallet-RPC to the Internet. The probe's HTTP option has no credentials and relies on private-network isolation; it is only for an explicitly trusted/firewalled route. Store gateway credentials/certificates in protected secret files, never in source, Compose literals, or browser settings. The observer must use a watch-only wallet/RPC boundary; transaction signing and spend keys remain separate and out of the app/observer. The gateway config now assigns each client-certificate fingerprint explicit roles: observer permits only height/address reads and bounded incoming-transfer scans; address-allocator permits only address reads/validation and one-subaddress creation. Keep the observer certificate separate from the allocator certificate. Neither role can invoke transaction construction, signing, relay, or sweep methods. The eventual flow still needs reviewed credits, actual withdrawal execution, recovery, and active sale settlement, as described in the implementation plan.
This probe does not establish that the node is honest or independently canonical, and synchronized: true is a point-in-time daemon report—not a finality guarantee or credit authorization. No deposit addresses are issued and no financial API is enabled by a successful result.
Pilot-bound observation persistence (schema v14)
Pilot Wallet-RPC output scans are stored in separate pilot tables, not the existing mainnet-custody observation tables. The one-shot observer verifies the configured wallet fingerprint, resumes a bounded durable cursor with overlap, binds recognized outputs only to an exact registered reservation, and records unknown-wallet outputs as unallocated suspense observations. Replays are idempotent; missing, changed, and reappeared outputs produce append-only events for operator reorg review. This pipeline never posts a journal, creates a credit, or changes a balance.
The pilot observer role is separate from reservation/address roles and cannot read custody balances/journals or write credit records. Use custody:db grant-pilot-monero-observer-runtime <ROLE> with WIKIDECK_PILOT_CUSTODY_OPERATIONS=pilot-observation-only-no-credit after schema migration and identity initialization. The Compose profile pilot-observer runs one bounded scan and exits; invoke it with docker compose -p wikideck-xmr-pilot -f compose.custody-pilot.yaml --profile pilot-observer run --rm pilot-observer. It is not part of normal up, publishes no ports, uses a separate egress network, and mounts only explicitly supplied observer database and mTLS files read-only. Compose does not enforce outbound host allowlists: firewall policy must limit access to the configured private gateway and external observer database. Do not reuse the mock pilot app database or its credentials.
The gateway's protected JSON config uses per-certificate policies, for example clientPolicies: [{"fingerprint":"<lowercase SHA-256 of certificate DER>","roles":["observer"]}]. Issue a different client certificate/fingerprint to the address allocator and assign only address-allocator; do not give the observer certificate that role. A combined role list is only for isolated rehearsals that intentionally exercise both capabilities with one disposable identity. Neither policy includes transaction signing, transfer, relay, or sweep methods.
Provide these non-secret identity values in the local Compose environment: WIKIDECK_PILOT_CUSTODIAN_ID, WIKIDECK_MONERO_WALLET_GENERATION_ID, and WIKIDECK_MONERO_WALLET_FINGERPRINT. Point the following host-path variables at protected files outside the repository: WIKIDECK_PILOT_OBSERVER_PG_URL_HOST, WIKIDECK_PILOT_OBSERVER_PG_CA_HOST, WIKIDECK_PILOT_OBSERVER_WALLET_CONFIG_HOST, WIKIDECK_PILOT_OBSERVER_MTLS_CONFIG_HOST, WIKIDECK_PILOT_OBSERVER_CLIENT_CERT_HOST, WIKIDECK_PILOT_OBSERVER_CLIENT_KEY_HOST, and WIKIDECK_PILOT_OBSERVER_GATEWAY_CA_HOST. The mounted Wallet-RPC config must explicitly say network: mainnet and point to the private gateway. The mounted mTLS JSON must reference the container paths /run/secrets/observer-client.crt, /run/secrets/observer-client.key, and /run/secrets/observer-gateway-ca.crt; its URL must match the Wallet-RPC config. The PostgreSQL URL must use the dedicated pilot_monero_observer role on the distinct pilot custody database, not the app runtime, schema owner, review, reservation, or allocator role.
WIKIDECK_PILOT_OBSERVER_MAX_BLOCKS (default 5000; max 100000) bounds each pass; WIKIDECK_PILOT_OBSERVER_OVERLAP (default 100; max 10000) requests a rescan window. Re-run manually to advance the cursor. There is no background observer, public HTTP endpoint, address issuance, or credit side effect. The Compose service definition is only wiring; connectivity is not verified until an operator supplies the private files and runs the one-shot command.
Pilot deposit review preparation (read-only, no credit)
An operator can now prepare a review subject from already persisted pilot observations. This does not run Wallet-RPC, scan the daemon, approve the output, stage a pending liability, create a journal, change balances, or expose an HTTP endpoint. It is not authorization to accept or spend funds. The separate pilot_deposit_review role reads only schema digests, the pilot identity/reservations, and pilot observer state/scans/observations/events; it cannot write pilot data or read financial tables. Do not reuse the observer, address, reservation, app-runtime, or credit role. The grant refuses roles with administrative powers, role memberships, custody object ownership, or database CREATE privilege.
After creating that role out of band, use the schema-owner connection to run custody:db grant-pilot-monero-review-runtime <ROLE> with WIKIDECK_NETWORK=pilot, WIKIDECK_PILOT_MODE=private-mock-only, and WIKIDECK_PILOT_CUSTODY_OPERATIONS=read-only-pilot-review-role-only. The separate database should already have the pilot schema and immutable identity. The grant does not issue addresses or enable crediting.
Configure a separate review-role PostgreSQL connection via protected WIKIDECK_PILOT_REVIEW_PG_URL_FILE and WIKIDECK_PILOT_REVIEW_PG_CA_FILE files; TLS and CA validation are mandatory, and the URL's role must equal WIKIDECK_PILOT_REVIEW_DB_ROLE. Then run the operator-only action custody:db pilot-deposit-prepare-review <WALLET_GENERATION_UUID> <TXID:OUTPUT_PUBLIC_KEY> with WIKIDECK_PILOT_CUSTODY_OPERATIONS=pilot-deposit-review-preparation-only. The default confirmation floor is 10 and can be set with WIKIDECK_PILOT_DEPOSIT_REVIEW_MIN_CONFIRMATIONS (bounded 10–100000). Maximum observation age defaults to 300000 ms and can be set with WIKIDECK_PILOT_DEPOSIT_REVIEW_MAX_OBSERVATION_AGE_MS (bounded 10000–900000). The command fails closed for unallocated/mismatched outputs, low confirmations, future unlocks, stale or inconsistent scans, double-spend flags, missing/reorged/changed history, malformed atomic amounts, and identity mismatch.
The JSON result is an unsigned operator evidence snapshot with an integrity digest—not a credit approval or finality proof. It includes public wallet/allocation identifiers and a Monero address; protect terminal capture and any saved output accordingly. Even a successful preparation proves only what the persisted observer reported at scan time. Current chain canonicality, daemon trust, wallet correctness, and finality still need independent operational checks. No posting/release action is available under the pilot profile, and cash-out/recovery remains a prerequisite before any real deposit can be accepted.
Initial checks
The network manifest reports profile: pilot, network: wikideck-randomx-pilot-1, and launchStatus: private-pilot-mock-only. Verify startup refuses a missing pilot-mode opt-in and any non-mock payment setting. Deployment tests check loopback binding, distinct profile selection, database secret isolation, and absence of an in-stack Monero daemon. A successful mock deployment is only evidence for this first slice, not for daemon connectivity or real-XMR readiness.
The operator-only command is packaged as npm run monero:pilot-address -- /absolute/path/to/signed-request.json (or node --experimental-strip-types scripts/pilot-monero-deposit-address.ts /absolute/path/to/signed-request.json inside the runtime image). It requires WIKIDECK_NETWORK=pilot, WIKIDECK_PILOT_MODE=private-mock-only, WIKIDECK_PILOT_CUSTODY_OPERATIONS=operator-triggered-addresses-only, and WIKIDECK_PILOT_CUSTODIAN_ID matching the signed request. Provide the dedicated address-allocator PostgreSQL connection through WIKIDECK_PILOT_ADDRESS_PG_URL_FILE and WIKIDECK_PILOT_ADDRESS_PG_CA_FILE; provide the protected watch-only wallet generation ID, fingerprint, and account index through WIKIDECK_MONERO_WALLET_GENERATION_ID, WIKIDECK_MONERO_WALLET_FINGERPRINT, and WIKIDECK_MONERO_DEPOSIT_ACCOUNT_INDEX; mount the protected WIKIDECK_MONERO_WALLET_RPC_CONFIG_FILE and WIKIDECK_MONERO_MTLS_CONFIG_FILE files. The config's endpoint URLs must match. The command is not in app startup, has no HTTP route, and emits the registered address only to this explicit operator invocation. It does not credit balances or declare the deposit accepted.