NUT-Exchange: Atomic Multi-Asset Exchange - #410
Open
joemphilips wants to merge 17 commits into
Open
Conversation
joemphilips
force-pushed
the
nut-atomic-exchange
branch
from
July 23, 2026 01:52
4b6a0cc to
f579c33
Compare
joemphilips
marked this pull request as ready for review
July 23, 2026 01:54
Author
|
Manual self-review done. Open for discussion |
- Two preparation patterns: a direct two-party swap (both online) and a
coordinator-mediated swap (relay assembles; an offline FOK taker plus online
makers/FAK).
- Remove swap_id entirely: request_digest over canonical participant records is
the sole idempotency/replay key; the request body is {"participants":[...]}.
- Remove expiry: reclaim is available anytime via the refund key, so no safety
property needs a deadline; order validity/staleness is the coordinator's
concern and a keyset's lifecycle bounds the rest.
- refund is the sole owner-authorization key (settlement is keyless/structural);
document why it cannot be omitted (bearer reclaim must be owner-gated).
- Be explicit about participation: only an exact-fill (FOK) order can be
pre-authorized and go offline; FAK and resting makers must be online at match
(blinding-budget lemma).
- error_codes: drop 15002, 15005, 15006; fix 15010 wording.
Refund-anytime made a conditioned proof a revocable offer rather than a commitment, and reintroduced the free cancellation option (retract when price turns against the committer) that this NUT exists to eliminate — structurally the same regression as the adaptor-sig leader's locktime race. - Add a required `expiry` tag to PAY_TO_UNLOCK, checked against the mint clock. - Settlement valid only before `expiry`; refund valid only after (non-overlapping states, no boundary race). This makes the commitment binding for its window. - Validation rule 11 enforces settlement-before-expiry; the refund section requires refund-after-expiry. - Mint info: advertise `max_expiry_seconds`. - error_codes: restore 15005 (settlement after expiry) and 15006 (refund before expiry). Concerns on unit and exact-amount commitment need no change: a keyset id is derived over its unit (NUT-02), so offer_keyset/receive_keyset already bind the asset; the offered amount is committed by the input proof amounts and the received amount by H_recv, linked by per-class conservation.
User-reviewed revision (notes 1-20) plus adversarial-review fixes:
Applied user notes:
- remove redundant 'This NUT transfers...' paragraph (keep provisional-name caveat on status line)
- rewrite 'matched against several counterparties' as a peer-to-peer batch limitation
- delete the smart-contract-vs-HTLC analogy paragraph
- rename 'star' to two-class throughout; rule 8 already permits any N-vs-M shape
- replace FOK/FAK framing with the multi-lot pattern (each lot is its own participant record; submitter includes only matched lots; unused lots refund after their own expiry)
- remove the blinding-budget lemma
- shorten Recovery to two sentences (owner, not submitter, recovers)
- rewrite Refund section; drop 'non-overlap' wording
- remove 'Why the refund key is required' paragraph
- remove Coordinator and relay role section (fold one sentence into Model)
- remove Resting-order optionality section entirely
- remove seven redundant FAQ entries
Adversarial-review fixes (codex-rescue):
- fix multiset-equality bug in PAY_TO_UNLOCK condition prose that rejected valid 1-vs-N and N-vs-M shapes (use set equality)
- define canonical_swap_request as JCS of {inputs, outputs}; specify Proof.witness as a single BIP-340 Schnorr signature over refund_digest
- refund may issue in any active keyset of the same unit, not strictly the same keyset ID (handles keyset rotation)
- enforce max_expiry_seconds at NUT-03 swap time (reject conditions with expiry > now + max_expiry_seconds)
- rule 1: each participant record must have at least one input and one output
- Canonical encodings: four condition tags each appear exactly once; expiry is decimal unix seconds
- amounts are unsigned 64-bit; rule 10 uses checked non-wrapping arithmetic
- clarify uint32_le(len) in H_recv as entry count, not byte length
- tighten premise claim to locktime free option only (no orderbook discussion)
- soften 'indiscriminate betrayal' privacy claim
- fix typos: 'settellable', lowercase 'see'
receive_keyset was authenticated inside H_recv via each BlindedMessage.id. The tag carried zero information not already present in the output bundle. Adversarially reviewed by codex (gpt-5.6-sol, xhigh): receive_keyset is safe to remove; offer_keyset is kept because it is inside the signed Proof.secret and prevents keyset-ID relabeling when verification keys are reused across keysets (which NUT-02 does not forbid). Condition now has 3 tags: offer_keyset, expiry, refund (was 4). Rules 7 and 8 updated to derive the receive keyset from output IDs.
Idempotent retries via request_digest are now an optional mint feature, advertised via idempotent_retries in NUT-06 info. Clients fall back to NUT-09 recovery when not supported, same as NUT-03 swap.
This was referenced Jul 26, 2026
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Jul 26, 2026
These files are needed because CTF-settle depends on NUT-Exchange concepts (PAY_TO_UNLOCK, H_recv, participants request shape).
…fixes Track 1 - Fix P0 refund bugs: - refund preimage excludes Proof.witness (was self-referential) - unified refund domain (Cashu/PAY_TO_UNLOCK/refund) Track 2 - Relax rules 7-8 for change outputs: - allow_change tag authorises outputs in offer keyset - rule 7 revised: outputs may use receive or offer keyset - rule 8 unchanged (still exactly two keysets) Track 3 - FAK support: - alt_outputs tag: finite set of alternative H_recv values - min_output_amount tag: minimum receive floor - one proof, one swap, tick-level granularity Track 4 - Other fixes: - rule 3: three required tags + optional tags (was four) - rule 5: offer_keyset == Proof.id check - rule 12: min_output_amount enforcement - max_alt_outputs in mint info
Synced from nut-ctf-settle. P0-1: decimal-string amounts. P0-2: unique per-proof nonce. P1-8/P1-11 fixes.
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Jul 27, 2026
joemphilips
force-pushed
the
nut-atomic-exchange
branch
from
July 27, 2026 04:50
5b09b8c to
79893ff
Compare
New NUT extending PAY_TO_UNLOCK with binary output pools and a mint-enforced rate covenant. Enables FAK orders where the actual fill amount is determined at match time. Design based on consultation with codex (gpt-5.6-sol, xhigh). Key mechanism: O(log N) BlindedMessages in powers-of-2 denominations, one manifest hash, one cross-multiply policy check. Coordinator selects any subset satisfying the rate inequality; better prices auto-admitted.
… pool fields - intro: order-type-agnostic (FAK only in FAQ) - powers-of-2: mandatory, not recommended (Cashu convention) - pool_manifest/pool_selection: full PoolEntry schema + bitmap example - add NUT-00 link reference
P0-1: rate encoding in raw minor units (rate_n=1, rate_d=1) P0-2: two-class invariant restored (exactly two keysets) P0-3: rate_d > 0, checked u128 arithmetic, reject change > input P0-4: powers-of-2 requires keyset inspection (NUT-01 dependency added) P0-5: nonce contradiction fixed (same data/tags, unique nonce) User notes: - pool_selection: unselected entries discarded, not swapped - PoolEntry: single name, defined once - data size: full manifest for v1, Merkle reserved for future - example: integer cents (10000 cents = 100 USD) - boundary check: 500 sats for 500 cents (exact limit, 499 rejected) - better price: 237 sats for 200 cents (strict inequality) - alt_outputs FAQ removed (undefined)
- Register pool-mode errors 15011-15014 (6p's bogus 13041 -> 15011; 7p-9p now coded; 10p reuses 11005) - Repair worked example (Repair B): rate_n=1 rate_d=10, max_debit=10000; fix selected-entry counts (boundary 11, better-price 12) - Canonicalize PoolEntry amount/index as decimal strings (IEEE-754 safety above 2^53) - Clarify debit_total is gross; rule 10 burns input_fees per class; examples assume zero fees - Define canonical pool_selection bitmap (ceil(n/8) bytes, lowercase hex, reject other lengths) - Require NUT-09 recovery over full manifest before discarding unselected entries (settlement-loss safety) - Allow mixed pool/standard participants; rule 8p derives receive keyset per mode - Forbid base optional tags (alt_outputs/allow_change/min_output_amount) in pool mode; min_receive supersedes rule 12 - Reframe powers-of-2 as wallet construction; mint validates per-entry keys only - Include pool_manifest/pool_selection in idempotent request_digest - Drop redundant 'at least one receive entry selected' (implied by min_receive>=1)
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Jul 27, 2026
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
61 tasks
Coordinator auth (optional, v1): a PAY_TO_UNLOCK proof may carry coordinator_pubkey (BIP-340 x-only key); if any input does, the request must carry coordinator_sig, a BIP-340 signature over tagged_hash(Cashu/PAY_TO_UNLOCK/coordinator, req_canonical). One key per request (v1); coordinator_sig must be absent otherwise; verified before any idempotency-cache hit. Prevents a user double-submitting the same authorization to two coordinators; NUT-07 stays advisory and atomic spent/pending checks remain.
Canonical: pool participant canonical = JCS({inputs,outputs,pool_manifest}) || hex_decode(pool_selection). New error 15015 for coordinator-auth failures.
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Aug 1, 2026
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
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Aug 1, 2026
These files are needed because CTF-settle depends on NUT-Exchange concepts (PAY_TO_UNLOCK, H_recv, participants request shape).
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Aug 1, 2026
joemphilips
added a commit
to joemphilips/nuts
that referenced
this pull request
Aug 1, 2026
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
13 tasks
… PAY_TO_UNLOCK rule
joemphilips
force-pushed
the
nut-atomic-exchange
branch
from
August 14, 2026 03:36
4555166 to
e3de7f6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an optional NUT for an atomic exchange of two existing Cashu asset classes at one mint. Participants lock bearer proofs to exact blinded receive outputs via a new NUT-10
PAY_TO_UNLOCKcondition; the mint validates the conditions, conserves each asset class independently, and commits all input spends and output signatures in one database transaction — or nothing.Context. This branched out of ongoing work on prediction markets with Cashu (see #337). The key observation: we do not need HTLC-based atomic swaps — genuinely troublesome, as anyone who has dealt with Lightning knows — like we usually reach for in Bitcoin, when both assets are managed by a single mint, because Cashu has a different trust model. A Cashu mint already validates proofs, tracks spentness, and issues blind signatures, so it can serve directly as the atomic settlement layer. This removes the interactive claim sequence, the both-wallets-online requirement, and the free-option locktime race of a client-to-client swap.
Custody is enforced by blind signatures plus the condition binding (any submitter/relay cannot steal or redirect); recovery via NUT-09; post-expiry refund authorized by a signed
refundkey. Trust floor matches NUT-11 P2PK plus one atomic commit; no transparency layer.Draft for discussion — final NUT number/name, endpoint, and fee policy are open.