Files
wikideck/docs/sale-lock-candidate.md
T

79 lines
11 KiB
Markdown
Executable File

# ADR 004 — Custodial NFT sale-lock candidate
Status: **offline executable candidate; NOT activated or independently reviewed**. Evidence: 2026-10-20. Related: [production plan](production-plan.md), [custodial model](custodial-market-design.md), [PostgreSQL storage](postgres-financial-storage.md).
## Purpose and activation boundary
Specify the missing asset-control rules before enabling real custodial purchases. An ordinary signed NFT transfer can survive a website refund deadline; balance reservations alone cannot prevent that late delivery. This candidate couples sale authorization to explicit canonical locks and terminal outcomes.
[Signed schemas/messages](../shared/sale-candidate.ts) and [deterministic state machine](../server/testnet/sale-candidate.ts) are deliberately separate from active consensus. The existing transaction schema rejects all candidate transactions. The candidate domain derives from the existing chain identity, explicit policy and candidate version; it is an offline signature domain, **not a reviewed network manifest/genesis**. No current chain identity, miner, peer endpoint or custody signing capability is changed. A separately gated [disposable PostgreSQL accounting rehearsal](custody-sale-rehearsal.md) now changes fictitious reservations using signed synthetic transcripts; it is not mined-chain integration.
The pure `termsForCustodyOrder()` bridge drafts terms from an actual awaiting-lock reservation and checks exact seller/author/host allocations. It does not sign, persist, relay or debit anything. Tests exercise this bridge against the current custody engine. Public custody orders continue stopping at `awaiting-lock`.
## Signed authorization
Terms bind candidate protocol, network, chain/policy digest, custodian/order IDs, exact card/sequence, seller, buyer, host signing identity, native-XMR gross, hosting rate, fixed author policy, lock-admission deadline and final expiry height. Independent downward commission rounding remains unchanged. No production threshold is invented: maturity/depth/window limits are explicit policy inputs committed by the candidate digest.
Optional `hostFeeAddress` appends a tagged hosting-recipient tuple, preserving historical candidate signature bytes when omitted. Drafting preserves the reserved order's snapshot; the PostgreSQL rehearsal additionally checks the exact listing rate, not just rounded allocation equality. The candidate performs address-shape validation only, with checksum/network validation supplied by custody admission and business audits. This extension is still unactivated and requires the same protocol review as the candidate.
- **Lock:** both seller and buyer sign the complete terms. Host signing identity must differ from both trading identities. Distinct keys are role separation, not proof of independent humans/operators.
- **Completion:** the terms' exact host signs the sale ID plus a reservation digest, attesting that sufficiently confirmed/unlocked funds were reserved under the approved policy. Host cannot select another card/recipient/price. Validators would verify the host's authorization, **not Monero truth**. The reservation digest is an accounting commitment, not a cryptographic proof of backing or solvency.
- **Cancellation:** host and buyer sign the sale ID. Neither can unilaterally cancel early. Seller already consented to this branch in signed lock terms; seller retains card ownership on cancellation.
- **Expiry:** permissionless explicit transaction, without a private key, once the canonical expiry height is reached. Website clocks/RPC errors never cause implicit release.
## State transitions and exact heights
Let $h$ be the block executing a transaction, $L$ the accepted lock height, $A$ the last lock-admission height, $E$ the expiry height, $d_L$ the lock-depth policy and $d_O$ the outcome-depth policy.
| Transition | Validity / result |
| --- | --- |
| Unlocked → locked | Both signatures, mature current seller ownership/sequence, no existing lock, unused custodian/order; $h\le A$, $A+d_L<E$, and bounded remaining window. Ownership unchanged; sequence advances once. |
| Locked → completed | Exact host signature, $h-L\ge d_L$ and $h<E$. Ownership moves only to the terms' buyer; sequence advances again. |
| Locked → cancelled | Host+buyer signatures and $h<E$. Ownership remains seller; sequence advances again. |
| Locked → expired | Explicit permissionless transaction with $h\ge E$. Ownership remains seller; sequence advances again. |
| Free transfer/trade | Every affected NFT must be unlocked, correctly owned/sequenced, mature and signed. A locked card in any trade leg rejects the entire transition. |
Only one terminal branch can succeed on a canonical history. An invalid transaction rejects the entire candidate block, not just that transaction; two conflicting outcomes in one block therefore reject the block. Block assembly/mempool policy will eventually need to filter these conflicts before proposing a block.
Lock and terminal transitions both consume sequence numbers, invalidating ordinary transfers signed before/during a lock. Admission requires room for **two** increments so terminal release cannot overflow. Expired/cancelled order-use tombstones prevent a fresh signature from recycling the same custodian/order ID. A new order can relist at the fresh sequence. A withheld lock authorization cannot be accepted after $A$, closing the never-submitted-lock refund hazard.
Candidate state commitment includes NFT entries, active locks, full signed-term messages, lock/terminal heights, reservation commitments and permanent order-use tombstones. Ownership alone would be an insufficient state commitment. Snapshot restore/replay is deterministic. This uses full cloned state and tombstone retention for an auditable model, **not a production-optimized overlay or storage design**.
## Financial observation and reorg discipline
`saleDecision()` is a pure non-posting recommendation based on a caller-supplied canonical candidate state:
- Keep the gross reserved while a lock could still be admitted, while it is active, or before a terminal outcome reaches $h-\text{outcomeHeight}+1\ge d_O$.
- Recommend seller/commission settlement only after mature canonical completion.
- Recommend buyer release only after mature canonical cancellation/expiry. If no lock was ever canonical, require $h-A\ge d_O$ before recommending release.
- Already posted outcomes never post twice. A conflicting or immature/missing previously posted outcome requires `review`, not another settlement/refund. Existing review remains review pending evidence-backed resolution.
This does not independently prove the supplied canonical state or observe intermediate forks. Integration must consume the existing durable connect/disconnect stream and retain a review hold even if an outcome reappears before polling. Deep reorgs can invalidate previously mature results and external XMR withdrawals cannot be undone. Loss/reserve/compensation procedures remain mandatory.
An already mature terminal record remains usable evidence if the NFT is later gifted/traded; current owner alone must not erase completed sale evidence. Conversely, a gift is not payment evidence. Cancel/expiry can be followed by a freshly signed free transfer at the new sequence; this is ordinary owner authority, not resurrection of the cancelled sale.
## Failure behavior and trust
| Failure | Candidate behavior / remaining risk |
| --- | --- |
| Seller lists elsewhere or withholds lock | Competing canonical transfer/lock wins by ordinary sequence rules; funds remain reserved until safe cancellation/non-admission boundary. No cross-host database claim is treated as a chain lock. |
| Buyer disappears after signing lock | Host may complete only to the signed buyer; otherwise eventual expiry restores seller control. Financial operator still owes customer funds. |
| Host disappears or loses key | Anyone can submit expiry after the height boundary. A stalled chain delays expiry; this does not recover custodial XMR keys. |
| Host lies about funding | Valid host signature can authorize delivery despite absent reserves. This is explicit custody/attestation trust; the protocol cannot enforce the honesty of an internal balance claim. |
| Cancel and complete race | Only canonical sequence/lock state chooses a winner; no refund before terminal evidence matures. Mempool order is not settlement. |
| Reorg after posting or withdrawal | Review/quarantine, never imaginary reversal of XMR. Operator loss handling and funded limits needed. |
| Host key compromise/rotation | Existing locks bind an exact key. Rotation/revocation and emergency authority are NOT implemented; do not silently redirect existing authority. |
## Evidence and next release gates
Initial candidate evidence: 162 offline tests passed, including 13 candidate tests. Current TypeScript/Vite Docker build and **172 offline tests plus 28 PostgreSQL integration tests passed, zero failures/skips**, including 14 candidate tests in [tests/sale-candidate.test.ts](../tests/sale-candidate.test.ts). Coverage includes current-consensus rejection, field/signature and hosting-recipient tampering, exact height boundaries, sequence exhaustion, native transfer/trade lock bypass, competing-market/reused-order locks, atomic conflicting-outcome rejection, reorg financial decisions, deterministic snapshot restore, state commitments, actual custody-order drafting and enumerated adversarial histories through expiry. The [accounting rehearsal](custody-sale-rehearsal.md) records actual PostgreSQL/fictitious-funds postings, a local-ledger event adapter test with fake PoW, and synthetic candidate-terminal disconnect linkage. No protocol guarantee is inferred from a test count.
**This does not close M3.** Required next steps:
1. Independent protocol/consensus review and operator approval of authority, cancellation and numeric policy parameters; final specification and test vectors.
2. Versioned network/manifest and explicit migration/activation strategy. No enabling the candidate under the existing v6 chain identity.
3. Integrate new transaction encodings/state commitments with mint/transfer/trade validation, canonical SQL undo/indexes/events, actual RandomX blocks, peer sync, restart and multi-host fork/partition tests. The current model supplies no PoW, issuance or fork-choice implementation.
4. Production PostgreSQL sale witness/terms storage, financially reserved host-signing workflow, outbox fencing, local-ledger canonical observer/inbox, activated candidate transaction linkage, atomic terminal postings and per-order reorg holds. The disposable signed-transcript rehearsal exercises terminal postings and durable review; a synthetic event-page test links terminal IDs to orders and fences a disconnect. The local-ledger adapter separately reads canonical events but cannot observe candidate IDs while active v6 rejects those transactions. Existing sandbox signatures do not authorize the new sale protocol. No signing before final exact order consent.
5. Publication/review UI, key management/rotation, privacy and public-price disclosure, window/griefing/storage limits and operational recovery. Public terms would reveal sale linkage and amounts; privacy acceptance is a product decision.
6. Monero confirmation/unlock observation, real treasury controls, coordinated financial/node recovery and independently reviewed production release gates. No live wallet, funds or credentials were used in this increment.