Skip to content

NUT-CTF-settle: Multi-Party Atomic Settlement - #412

Draft
joemphilips wants to merge 27 commits into
cashubtc:mainfrom
joemphilips:nut-ctf-settle
Draft

NUT-CTF-settle: Multi-Party Atomic Settlement#412
joemphilips wants to merge 27 commits into
cashubtc:mainfrom
joemphilips:nut-ctf-settle

Conversation

@joemphilips

@joemphilips joemphilips commented Jul 26, 2026

Copy link
Copy Markdown

This PR adds multi-party settlement for CTF tokens defined in #337. It also depends on #410.
Those 2 PRs should be reviewed/merged first.

Add NUT-CTF (Conditional Token Framework), NUT-CTF-split-merge (split/merge operations), and NUT-CTF-numeric (numeric outcome conditions) specs with test vectors, supplementary material, error codes, and README entries.

Squashed from 9 commits after rebasing onto upstream/main to resolve NUT-28/29 filename conflicts (upstream now uses those numbers for P2BK and Batched Mint).
The docs.gnosis.io/conditionaltokens/ URL returns 404. Replace with the official readthedocs site (rendered from the same gnosis/conditional-tokens-contracts repo) and link the contracts repo alongside as a stable fallback. Add Polymarket as a real-world production example of the same model in CTF.md's Terminology section.
Replace POST /v1/ctf/split and /v1/ctf/merge with a single POST /v1/ctf/convert that performs any payoff-preserving rebalance of conditional positions within one condition at one nesting level. Split, merge, recombine, and negative-risk-style conversion all become special cases.

Core rule: for every outcome o in Omega, out(o) == in(o) - F, valid only if in(o) >= F, where F = ceil(sum input_fee_ppk / 1000). The flat fee is retained non-contingently on every outcome (real collateral the mint keeps regardless of the attested outcome), which bounds free operations.

Collateral is the reserved map key '*' (regular keyset at root, parent conditional keyset when nested). Coverage is computed from registered keyset metadata, not request map keys. Canonical collection encoding fixed (enum: announcements[0] outcome index; numeric: [HI,LO]). Partitions must have >=2 elements with no full-set conditional collection. Issuance invariant: every conditional token backed by >= its face in collateral. Adds error codes 13041/13042/13043; updates CTF.md, CTF-numeric.md, error_codes, suppl, and test vectors accordingly.

Design and post-implementation passes adversarially reviewed via Codex (verdict: sound to merge).
Remove partition registration from the CTF specs and describe condition-time keyset creation, managed default policies, root-only convert scope, and updated numeric behavior.
Add NUT-08-style blank outputs and change signatures for condition registration fees so mints retain only the authoritative required fee.
Use a distinct 13047 error for invalid registration fee change outputs and disambiguate the idempotency cross-reference.
Advertise registration fee schedules by collateral unit and default missing schedules to free registration.
Treat registration_fees entries as the allowlist for CTF collateral units and reject non-advertised units instead of defaulting them to free registration.
Closes a partition-based over-redemption gap. HI = floor(amount*hi_payout_ratio), LO = floor(amount*lo_payout_ratio), and the mint retains amount - HI - LO as dust. HI+LO <= amount always, and because floor is subaddible, splitting or merging proofs before redemption can only reduce a holder's payout, never increase it. Trades exact conservation (a full set can redeem slightly under collateral; dust is mint revenue) for partition-invariant solvency.
Extension of both NUT-Exchange (cashubtc#410) and NUT-CTF-split-merge (cashubtc#337).

Delta document specifying only the differences from both parent NUTs.

P0 fixes from adversarial review (gpt-5.6-sol xhigh):

- corrected H_recv and refund domain separators

- explicit inherited (rules 1-7,9,11) vs replaced (rules 8,10)

- request_digest binds condition_id + parent_collection_id

- active-keyset cutoff inherited from split-merge

- coverage requirement is multi-party-only, not inherited

- removed false NUT-03 COMPLEMENTARY claim

- attestation atomicity scoped to all /v1/ctf/convert commits
- Rule 10: rewrite to explain WHY uncovered outcomes are degenerate

- Rule 4: add DoS rationale for zero-fee rejection

- Rule 5: remove (redundant with exchange rule 11)

- request_digest: add idempotency purpose

- Attestation atomicity: rewrite in plain language

- Liability accounting: add colloquial explanation before formal proof
These files are needed because CTF-settle depends on NUT-Exchange

concepts (PAY_TO_UNLOCK, H_recv, participants request shape).
Dropping offer_keyset was incorrect: collateral keysets are regular

NUT-02 keysets that may share verification keys. Without the tag

inside the signed Proof.secret, a coordinator could relabel Proof.id

to a different keyset sharing the same key. Condition is now identical

to NUT-Exchange (3 tags: offer_keyset, expiry, refund).
P0-1: amounts encoded as decimal strings in canonical form (avoids

  IEEE-754 precision loss above 2^53 in JCS)

P0-2: unique per-proof nonce, shared meaningful fields (allows multiple

  proofs per record without duplicate secrets)

P0-3: resolved by P0-2 (micro-lots with unique nonces, same H_recv)

P1-8: change determined by aggregate conservation, not per-participant

P1-11: fix field name id (was keyset_id) in sorting
Also: clarify CTF rejects optional exchange tags (alt_outputs,

allow_change, min_output_amount), per-proof nonce wording.
Applied to participant_canonical and refund preimage (was only H_recv).

Also: field name fix (id not keyset_id), max_alt_outputs MUST enforce,

min_output_amount canonical grammar, removed duplicate bullet.
Fix P0-4: handle absent final_expiry via max_expiry_seconds fallback
Stopgap before the planned nut-atomic-exchange -> main merge + rebase of this branch.

- Add exchange-partial-fill.md placeholder (latest codex-reviewed version: error codes 15011-15014, Repair B worked example, decimal-string PoolEntry canonicalization, gross-debit fee clarification, canonical pool_selection bitmap, NUT-09 discard safety, mixed pool/standard participants, pool-mode tag grammar)

- Update error_codes.md placeholder to latest (adds 15011-15014 + partial-fill link)

- exchange.md is byte-identical between branches (no-op); README.md and all nine CTF files untouched
CTF convert now supports pool-mode range orders (PAY_TO_UNLOCK with rate_n/rate_d) inherited from NUT-Exchange-partial-fill, replacing the micro-lot pattern.

- Mode-dispatched validation: standard participants inherit base rules 1-7,9,11-12; pool participants inherit 6p/7p/9p + per-participant 8p clauses; the request-global two-keyset clause of 8p and rule 10p are replaced by CTF rule 8 and per-outcome rule 10

- Endpoint binding: manifest hashed under Cashu/ctf/convert/manifest (not base PAY_TO_UNLOCK/manifest), preventing /v1/exchange replay at /v1/ctf/convert

- Rational limit-price encoding: buy at p=a/b -> rate_n=b, rate_d=a; sell reverses; fractional prices representable

- request_digest includes pool_manifest/pool_selection for pool participants; Mint info gains partial_fill + max_pool_entries; max_request_bytes covers manifests

- Reconcile pool-mode change with per-outcome conservation (safety = per-outcome equality, not auth/price); clarify fair-pricing FAQ (mint enforces owner limit, not best execution)

- Re-point exchange-spec references to PR cashubtc#410; remove local exchange.md and exchange-partial-fill.md placeholders; depends on now lists NUT-Exchange and NUT-Exchange-partial-fill
Canonical: condition_id and parent_collection_id are 64 lowercase hex decoded to 32 raw bytes (omitted parent = 32 zero bytes); pool participant canonical = JCS({inputs,outputs,pool_manifest}) || hex_decode(pool_selection). Coordinator auth inherited from NUT-Exchange with digest domain Cashu/ctf/convert/coordinator; coordinator_pubkey permitted in standard and pool mode; one key per request (v1).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant