89 lines
16 KiB
Markdown
Executable File
89 lines
16 KiB
Markdown
Executable File
# Private pilot deployment: mock-only foundation
|
||
|
||
Status: **application and isolated PostgreSQL sandbox only**. This is not a real-XMR stack, a public node, or an approval to accept deposits. The operator already hosts a Monero full node on the network and intends the pilot to reuse it; this Compose profile neither starts nor connects to that daemon yet. Wallet-RPC, observer, deposit enrollment, and withdrawal execution are not included.
|
||
|
||
## Start the isolated profile
|
||
|
||
Use [compose.custody-pilot.yaml](../compose.custody-pilot.yaml) with the stable project name `wikideck-xmr-pilot`. Do not combine it with [the existing local custody sandbox](../compose.custody.yaml), 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.
|
||
|
||
This stack does **not** connect to the operator's Monero daemon, create real Monero deposit addresses, scan a wallet, approve real credits, construct or relay withdrawals, or settle NFT sales against active consensus. Having a full node supplies only the blockchain daemon; it is not a Wallet-RPC service, watch-only deposit wallet, or spending wallet. Deposit enrollment and withdrawal request flags remain rejected outside the mainnet profile, and mainnet remains separately gated. 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:
|
||
|
||
1. With the `pilot` profile selected, run `custody:db migrate-pilot-schema` with `WIKIDECK_PILOT_SCHEMA_MIGRATION=explicit-pilot-schema-migration-only`.
|
||
2. Run `custody:db initialize-pilot-custody-identity <CUSTODIAN_UUID>` with `WIKIDECK_PILOT_MODE=private-mock-only`, `WIKIDECK_PILOT_CUSTODY_IDENTITY=initialize-pilot-custody-v1`, and `WIKIDECK_PILOT_CUSTODY_OPERATIONS=identity-only-no-funds`.
|
||
3. 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>` with `WIKIDECK_PILOT_CUSTODY_OPERATIONS=reservation-only-no-addresses`.
|
||
4. 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>` with `WIKIDECK_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:
|
||
|
||
```json
|
||
{
|
||
"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 any later gateway credentials/certificates in protected secret files, never in source, Compose literals, or browser settings. The observer should use a watch-only wallet/RPC boundary; transaction signing and spend keys remain separate and out of the app/observer. The eventual flow still needs a private Wallet-RPC gateway, observer, schema/role integration, reviewed credits, actual withdrawal execution, recovery, and active sale settlement, as described in [the implementation plan](real-xmr-pilot-plan.md).
|
||
|
||
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. Run one operator-controlled scan with `npm run monero:pilot-observe`; it requires the protected pilot observer PostgreSQL URL/CA, Mainnet Wallet-RPC config, matching mTLS gateway config, and wallet generation/fingerprint settings. `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. No background service or HTTP endpoint is enabled.
|
||
|
||
### 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.
|