Skip to content

OTC: bonded offers with verifiable supply — design doc + contrib/otc tooling - #87

Closed
numair wants to merge 2 commits into
mainfrom
claude/otc-escrow-supply-validation-52l46r
Closed

OTC: bonded offers with verifiable supply — design doc + contrib/otc tooling#87
numair wants to merge 2 commits into
mainfrom
claude/otc-escrow-supply-validation-52l46r

Conversation

@numair

@numair numair commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Targets the v0.33.2 release window.

Motivation

OTC desks have been quoting BTX size they cannot deliver (phantom supply) to suppress price: fabricated balances, the same coins shown to several venues at once, borrowed coins shown then returned, and unverifiable "it's in the shielded pool" claims. This PR ships (1) a design document for trustless-as-possible OTC escrow with proof-of-offered-supply, and (2) working Phase-1 reference tooling implementing it, so that an offer without a valid proof bundle can be treated by the market as no supply.

No consensus, policy, or node code changes — everything is built from descriptor leaves, opcodes, and wallet RPCs already live on main (P2MR MAST, CLTV refund() leaves, multi_pq/ctv_multi_pq, HTLC leaves, buildhtlcclaim/buildhtlcrefund, BIP-322 signmessage).

What's included

  • doc/btx-otc-escrow-supply-validation.md — the design: Bonded Offer Vaults (tier A+ CTV covenant / tier A venue co-sign / tier B soft bond), OP_RETURN "BTXOTC1"||sha256(canonical terms) funding-tx commitment that makes one UTXO ↔ one offer cryptographically exclusive (kills double-pledging), HTLC atomic settlement for crypto legs, bounded-arbiter CTV escrow and per-offer-key CSFS oracles for fiat legs, threat model, and roadmap.
  • contrib/otc/btx_otc.py — SDK + CLI: terms canonicalization/hashing, bond descriptor builders and a strict fail-closed parser, create_offer (funds the vault + commitment in one send), walletless verify_offer (descriptor shape, UTXO existence/confirmations/amount, funding-tx commitment without -txindex, expiry, BIP-322 attestation), watch_offer, bond refund and HTLC settlement wrappers over the audited wallet RPCs. python3 btx_otc.py selftest runs offline unit checks.
  • contrib/otc/README.md — usage, tier table, and an honest trust-model statement.
  • test/functional/wallet_otc_offer.py — end-to-end regtest coverage (registered in test_runner.py).
  • Drive-by fix: wallet_htlc_atomicswap.py failed on txindex-less nodes at its preimage-reveal check (getrawtransaction after the tx left the mempool); now scoped to the confirming block. Same fix pattern used in the new test.

Validation

  • I listed the tests that cover this change.
  • src/libbitcoinpqc/** untouched — fuzz smoke not applicable.

Ran locally on a fresh Release build (-DBUILD_GUI=OFF -DBUILD_TESTS=OFF):

  • test/functional/wallet_otc_offer.py --descriptorspasses: offer creation; positive verification (incl. BIP-322 attestation over a p2mr address); rejection of tampered terms, double-pledged outpoints, fabricated outpoints, under-confirmed bonds, undeclared descriptor spend paths, duplicate outpoints; expiry handling; bond refund via the refund() leaf; watcher spent-detection; stage-2 HTLC claim with on-chain preimage reveal.
  • test/functional/wallet_htlc_atomicswap.py --descriptorspasses (was failing before the drive-by fix on nodes without -txindex).
  • python3 contrib/otc/btx_otc.py selftest — passes (canonicalization, descriptor round-trips, fail-closed parsing, commitment framing).
  • Manual CLI smoke on a live regtest node: create → mine → verify --min-conf 1 --require-attestation returns ok: true with all seven checks green, wallet-less.
  • flake8 --select=F,E9 clean on both new Python files.

Notes for reviewers

  • The verifier fails closed on any descriptor shape outside the three documented tiers — an unclassifiable leaf could be a hidden early-exit path.
  • Funding-tx lookup needs no -txindex: the block is derived from the bond UTXO's own confirmation depth (getblockhash(height − confs + 1)).
  • Tier A/A+ turnkey settle-spend builders (venue co-sign PSBT flow, CTV template helper) and a message-pinned CSFS leaf are scoped as Phase-1 follow-ups in the design doc §9; tier B plus stage-2 HTLC settlement is fully working today.
  • contrib/otc carries the same UNAUDITED caveat as contrib/wbtx.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XyH9DxN4gxxXvBGQEoPkT3


Generated by Claude Code

claude added 2 commits July 13, 2026 05:57
Design for trustless OTC settlement and proof-of-offered-supply to
counter phantom-supply OTC markets:

- Bonded Offer Vaults: P2MR MAST vaults (venue co-sign or CTV-committed)
  with refund-after-expiry leaves, so offered coins provably exist,
  cannot be pulled early, and cannot back two offers (OP_RETURN
  terms-hash binding to the funding outpoint).
- Settlement: HTLC atomic swaps for crypto legs (existing
  buildhtlcclaim/buildhtlcrefund + wBTX Model B), bounded-arbiter CTV
  escrow and per-offer-key CSFS oracles for fiat legs, classic 2-of-3
  as the zero-new-code fallback.
- Supply validation: offer-bundle JSON + verification algorithm over
  gettxout/scantxoutset, bond tiers, desk-level reserves proofs, macro
  supply audit via gettxoutsetinfo + shielded turnstile, and an explicit
  market rule that unverifiable shielded claims count as zero.
- Roadmap: Phase 0 usable today with shipped RPCs; Phase 1 adds
  otc_createoffer/otc_verifyoffer/otc_settle tooling, a CTV template
  helper, and a message-pinned CSFS leaf. No consensus changes anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyH9DxN4gxxXvBGQEoPkT3
Implements the Phase-1 tooling of doc/btx-otc-escrow-supply-validation.md
so OTC quotes can be backed by on-chain proof instead of screenshots:

- contrib/otc/btx_otc.py: SDK + CLI for bonded offers. Offer terms are
  canonicalized and hashed; the bond funding tx commits to that hash via
  an OP_RETURN "BTXOTC1"||sha256(terms) output, so one UTXO can never
  verifiably back two offers. Bond descriptors cover three tiers (B soft
  bond, A venue co-sign, A+ CTV covenant), all built from existing node
  descriptor leaves. verify_offer() re-checks everything against a local
  node and fails closed on unknown descriptor shapes; funding txs are
  fetched without -txindex by deriving the block from the UTXO's
  confirmation depth. Settlement wrappers reuse the audited
  buildhtlcclaim/buildhtlcrefund wallet RPCs (bond refunds ride the same
  refund-leaf path). Includes an offline selftest and a btx-cli-backed
  CLI (create/verify/watch/refund-bond/hash-terms).
- test/functional/wallet_otc_offer.py: end-to-end regtest coverage:
  offer creation, walletless verification, rejection of tampered terms,
  double-pledged and fabricated outpoints, under-confirmed bonds and
  undeclared spend paths, bond refund after expiry, watcher spent
  detection, and stage-2 HTLC settlement with on-chain preimage reveal.
  BIP-322 attestation over p2mr addresses is exercised too. Registered
  in test_runner.py.
- wallet_htlc_atomicswap.py: fix pre-existing failure on txindex-less
  nodes (getrawtransaction now scoped to the confirming block).
- contrib/README.md, design doc: index the new tooling.

No consensus, policy, or node changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyH9DxN4gxxXvBGQEoPkT3
@visitor-code

Copy link
Copy Markdown
Contributor

Independently verified Steps 1–2 (the fund-verification half) end-to-end on stock v0.32.12 — a throwaway regtest with a wallet-less / txindex-less verifier reimplementing the seven checks. All green: offer creation, positive verify (incl. a BIP-322 attestation over a p2mr address), and rejection of tampered terms, double-pledged outpoints, fabricated outpoints, under-confirmed bonds, early-pull (non-final), and post-expiry refund via the refund() leaf. Worth stating explicitly in the PR: this half needs no 0.33.x — every RPC and descriptor leaf it relies on (refund()/htlc() sugar, multi_pq, exportpqkey, buildhtlcclaim/buildhtlcrefund, addpqmultisigaddress, BIP-322 verifymessage, and 39-byte OP_RETURN standardness) is already live on 0.32.12.

Three footguns worth folding in:

1. create_offer defaults to no explicit fee → silent bond death (HIGH). create_offer(..., fee_rate=None) only sets options["fee_rate"] when it's non-None, so the default path lets the wallet pick the fee. PQ bond-funding txs are large — a trivial 1-in/3-out bond is ~7,784 bytes (ML-DSA-44 sigs are 2,420 B each). In the probe, the wallet's default send fee underpaid min-relay by 123 sats (min relay fee not met, 7784 < 7907) and the tx was silently evicted from the mempool: the wallet still reported it as sent, 0 confs forever, no error surfaced. A seller would advertise a bond that never confirms. Suggest create_offer require an explicit fee rate (or default to a safe floor) instead of falling through to wallet default — the design note's "fees: be generous" line is load-bearing, not advice.

2. btx-cli arg-conversion table is missing buildhtlcclaim/buildhtlcrefund. Positional numeric/object params (prevout, locktime, fee) arrive as strings and the server rejects them (Wrong type passed). Raw JSON-RPC over the cookie works, but a one-line vRPCConvertParams entry would fix the CLI path.

3. buildhtlcrefund help-text mismatch. Its help says the descriptor "must be of the form mr(<internal>,{htlc,refund})" but it accepts a bare mr(pk,{refund()}) bond — which looks intended (and the verifier depends on it). Docstring fix.

Happy to share the verifier + regtest notes as a follow-up if useful.

numair commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Accepted for the BTX 0.33.2 release. The complete reviewed change set from this PR has been incorporated into the 0.33.2 release integration, so this PR is being closed as accepted rather than merged independently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants