diff --git a/error_codes.md b/error_codes.md index a9f6aec2..7742d47c 100644 --- a/error_codes.md +++ b/error_codes.md @@ -1,43 +1,56 @@ # NUT Errors -| Code | Description | Relevant nuts | -| ----- | ----------------------------------------------- | ------------------------------------------------------ | -| 10001 | Proof verification failed | [NUT-03][03], [NUT-05][05] | -| 11001 | Proofs already spent | [NUT-03][03], [NUT-05][05] | -| 11002 | Proofs are pending | [NUT-03][03], [NUT-05][05] | -| 11003 | Outputs already signed | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11004 | Outputs are pending | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11005 | Transaction is not balanced (inputs != outputs) | [NUT-02][02], [NUT-03][03], [NUT-05][05] | -| 11006 | Amount outside of limit range | [NUT-04][04], [NUT-05][05] | -| 11007 | Duplicate inputs provided | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11008 | Duplicate outputs provided | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11009 | Inputs/Outputs of multiple units | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11010 | Inputs and outputs not of same unit | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11011 | Amountless invoice is not supported | [NUT-05][05] | -| 11012 | Amount in request does not equal invoice | [NUT-05][05] | -| 11013 | Unit in request is not supported | [NUT-04][04], [NUT-05][05] | -| 11014 | Max inputs exceeded | [NUT-03][03], [NUT-05][05] | -| 11015 | Max outputs exceeded | [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 11016 | Duplicate quote IDs provided | [NUT-29][29] | -| 11017 | Max batch size exceeded | [NUT-29][29] | -| 12001 | Keyset is not known | [NUT-02][02], [NUT-04][04] | -| 12002 | Keyset is inactive, cannot sign messages | [NUT-02][02], [NUT-03][03], [NUT-04][04] | -| 12003 | Keyset has expired | [NUT-02][02], [NUT-03][03], [NUT-04][04], [NUT-05][05] | -| 20001 | Quote request is not paid | [NUT-04][04] | -| 20002 | Quote has already been issued | [NUT-04][04] | -| 20003 | Minting is disabled | [NUT-04][04] | -| 20004 | Lightning payment failed | [NUT-05][05] | -| 20005 | Quote is pending | [NUT-04][04], [NUT-05][05], [NUT-29][29] | -| 20006 | Invoice already paid | [NUT-05][05] | -| 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | -| 20008 | Signature for mint request invalid | [NUT-20][20] | -| 20009 | Pubkey required for mint quote | [NUT-20][20] | -| 30001 | Endpoint requires clear auth | [NUT-21][21] | -| 30002 | Clear authentication failed | [NUT-21][21] | -| 31001 | Endpoint requires blind auth | [NUT-22][22] | -| 31002 | Blind authentication failed | [NUT-22][22] | -| 31003 | Maximum BAT mint amount exceeded | [NUT-22][22] | -| 31004 | BAT mint rate limit exceeded | [NUT-22][22] | +| Code | Description | Relevant nuts | +| ----- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| 10001 | Proof verification failed | [NUT-03][03], [NUT-05][05] | +| 11001 | Proofs already spent | [NUT-03][03], [NUT-05][05] | +| 11002 | Proofs are pending | [NUT-03][03], [NUT-05][05] | +| 11003 | Outputs already signed | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11004 | Outputs are pending | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11005 | Transaction is not balanced (inputs != outputs) | [NUT-02][02], [NUT-03][03], [NUT-05][05] | +| 11006 | Amount outside of limit range | [NUT-04][04], [NUT-05][05] | +| 11007 | Duplicate inputs provided | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11008 | Duplicate outputs provided | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11009 | Inputs/Outputs of multiple units | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11010 | Inputs and outputs not of same unit | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11011 | Amountless invoice is not supported | [NUT-05][05] | +| 11012 | Amount in request does not equal invoice | [NUT-05][05] | +| 11013 | Unit in request is not supported | [NUT-04][04], [NUT-05][05] | +| 11014 | Max inputs exceeded | [NUT-03][03], [NUT-05][05] | +| 11015 | Max outputs exceeded | [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 11016 | Duplicate quote IDs provided | [NUT-29][29] | +| 11017 | Max batch size exceeded | [NUT-29][29] | +| 12001 | Keyset is not known | [NUT-02][02], [NUT-04][04] | +| 12002 | Keyset is inactive, cannot sign messages | [NUT-02][02], [NUT-03][03], [NUT-04][04] | +| 12003 | Keyset has expired | [NUT-02][02], [NUT-03][03], [NUT-04][04], [NUT-05][05] | +| 20001 | Quote request is not paid | [NUT-04][04] | +| 20002 | Quote has already been issued | [NUT-04][04] | +| 20003 | Minting is disabled | [NUT-04][04] | +| 20004 | Lightning payment failed | [NUT-05][05] | +| 20005 | Quote is pending | [NUT-04][04], [NUT-05][05], [NUT-29][29] | +| 20006 | Invoice already paid | [NUT-05][05] | +| 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | +| 20008 | Signature for mint request invalid | [NUT-20][20] | +| 20009 | Pubkey required for mint quote | [NUT-20][20] | +| 30001 | Endpoint requires clear auth | [NUT-21][21] | +| 30002 | Clear authentication failed | [NUT-21][21] | +| 31001 | Endpoint requires blind auth | [NUT-22][22] | +| 31002 | Blind authentication failed | [NUT-22][22] | +| 31003 | Maximum BAT mint amount exceeded | [NUT-22][22] | +| 31004 | BAT mint rate limit exceeded | [NUT-22][22] | +| 15001 | Unsupported or malformed `PAY_TO_UNLOCK` condition | [NUT-Exchange][exchange] | +| 15003 | Receive-output commitment (`H_recv`) mismatch | [NUT-Exchange][exchange] | +| 15004 | Offer/receive keyset relationship violated | [NUT-Exchange][exchange] | +| 15005 | Exchange settlement submitted after `expiry` | [NUT-Exchange][exchange] | +| 15006 | Refund submitted before `expiry` | [NUT-Exchange][exchange] | +| 15007 | Refund signature missing or invalid | [NUT-Exchange][exchange] | +| 15009 | Exchange request exceeds advertised limits | [NUT-Exchange][exchange] | +| 15010 | Conflicting request or reused input (idempotency) | [NUT-Exchange][exchange] | +| 15011 | Pool manifest hash (`H_manifest`) mismatch | [NUT-Exchange-partial-fill][partial-fill] | +| 15012 | Pool selection does not match `outputs` | [NUT-Exchange-partial-fill][partial-fill] | +| 15013 | Pool role/keyset or two-class consistency violated | [NUT-Exchange-partial-fill][partial-fill] | +| 15014 | Pool policy violation (rate/min/max/overflow) | [NUT-Exchange-partial-fill][partial-fill] | +| 15015 | Coordinator authorization failed (key disagreement / missing, invalid, or unexpected `coordinator_sig`) | [NUT-Exchange][exchange] | [00]: 00.md [01]: 01.md @@ -56,3 +69,5 @@ [21]: 21.md [22]: 22.md [29]: 29.md +[exchange]: exchange.md +[partial-fill]: exchange-partial-fill.md diff --git a/exchange-partial-fill.md b/exchange-partial-fill.md new file mode 100644 index 00000000..35c14326 --- /dev/null +++ b/exchange-partial-fill.md @@ -0,0 +1,250 @@ +# NUT-Exchange-partial-fill: Partial-Fill Authorization + +`draft` + +`optional` + +`depends on: NUT-Exchange, NUT-01, NUT-02, NUT-03, NUT-06, NUT-09, NUT-10, NUT-11, NUT-12` + +--- + +This NUT extends [NUT-Exchange][exchange]'s `PAY_TO_UNLOCK` condition with a **pool-based authorization mode**: the owner locks a single input and authorizes a **range** of possible output bundles, with the actual output selection determined at match time. + +In standard mode (NUT-Exchange), `data` is `H_recv` — a hash of one exact output bundle. In pool mode (this NUT), `data` is a manifest hash over a small set of pre-generated output entries in binary denominations, plus a numeric rate policy that the mint enforces. The coordinator selects any subset satisfying the policy; the mint signs only the selected entries. + +Readers should be familiar with [NUT-Exchange][exchange] (`PAY_TO_UNLOCK`, conservation rules, recovery, refund, the `participants` request shape). + +## Condition + +Pool mode is signaled by the presence of `rate_n` and `rate_d` tags. When absent, the condition uses standard mode (exact `H_recv` match, as defined in [NUT-Exchange][exchange]). In pool mode the base optional tags `alt_outputs`, `allow_change`, and `min_output_amount` MUST be absent: `alt_outputs` is incompatible with rule 6p's exact `H_manifest`, change is authorized by the manifest's change role (rule 8p), and `min_receive` supersedes `min_output_amount` (base rule 12). +The base `coordinator_pubkey` tag ([NUT-Exchange][exchange]) is permitted in pool +mode unchanged. + +```json +[ + "PAY_TO_UNLOCK", + { + "nonce": "", + "data": "", + "tags": [ + ["offer_keyset", ""], + ["expiry", ""], + ["refund", ""], + ["rate_n", ""], + ["rate_d", ""], + ["min_receive", ""], + ["max_debit", ""] + ] + } +] +``` + +**Required tags** (same as NUT-Exchange): `offer_keyset`, `expiry`, `refund`. + +**Pool-mode tags** (presence signals pool mode; all four required together, each appearing exactly once as a minimal unsigned decimal string with no leading zeros): + +- `rate_n` / `rate_d`: minimum receive rate in integer keyset minor units: at least `rate_n` receive-keyset units for every `rate_d` offer-keyset units debited. `rate_d` MUST be greater than zero. The mint enforces `receive_total × rate_d ≥ debit_total × rate_n` using checked `u128` cross-multiplication, with no division or rounding. Implementations MUST NOT convert either total to display units before this comparison. For a sat receive keyset and a USD offer keyset whose amount `1` is one cent, one sat per cent is encoded as `rate_n = 1, rate_d = 1`. The inequality admits every better price. +- `min_receive`: minimum total receive-keyset output amount. Prevents dust fills. MUST be positive. +- `max_debit`: maximum total debit (`input_total − change_total`). Caps spending and MUST be no greater than the participant's input total. The mint MUST reject a request whose selected `change_total` exceeds `input_total`; it MUST NOT perform a wrapping subtraction. + +Every proof contributed by one participant MUST carry the same `data` and the same tags (`offer_keyset`, `expiry`, `refund`, `rate_n`, `rate_d`, `min_receive`, `max_debit`, and optionally `coordinator_pubkey`), while each proof MUST use a unique `nonce`, following the per-proof nonce rule in [NUT-Exchange][exchange]. + +## Output pools + +The owner generates two pools of `BlindedMessage` entries: + +- **Receive pool:** entries in the receive keyset, at powers-of-2 denominations (the recommended wallet construction; see below). +- **Change pool:** entries in the offer keyset, at powers-of-2 denominations covering the possible change range. + +Each entry has a `role` (`receive` or `change`), `amount`, `id` (keyset), and `B_` (blinded point). The owner retains every entry's secret and blinding factor. + +For each participant, every `receive` entry MUST have the same `id`, called that participant's receive keyset. Every `change` entry MUST have `id` equal to the condition's `offer_keyset`. Both roles MUST be present, and the receive keyset MUST differ from `offer_keyset`; therefore each participant's complete manifest contains exactly those two keyset IDs. Across the complete exchange request, the union of every participant's `offer_keyset` and derived receive keyset MUST contain exactly two distinct keyset IDs. + +The recommended wallet construction places entries at powers-of-two denominations (`1, 2, 4, 8, ..., 2^k`), because `⌈log₂(R+1)⌉` such receive entries can represent every integer from `0` through `R` by subset sum, and likewise `⌈log₂(C+1)⌉` change entries cover `0` through `C`, keeping the pool at `O(log R + log C)` entries. Cashu does not guarantee that a keyset publishes those denominations, so a wallet MUST inspect the [NUT-01][01] key maps and MUST NOT create a pool unless each keyset is active for issuance and publishes a signing key for every amount placed in that keyset's pool. The denomination sequence is a wallet-side construction: the mint authenticates only the committed manifest and verifies per entry that `(id, amount)` identifies a published signing key in an active keyset (rule 8p); it does not require any particular denomination shape. + +## Manifest hash + +The `data` field is `H_manifest`, computed over the complete ordered `pool_manifest`. `PoolEntry` is the only name for an entry in that array and has exactly these fields: + +```json +{ + "index": "", + "role": "receive|change", + "amount": "", + "id": "", + "B_": "" +} +``` + +`amount` is an unsigned 64-bit integer in the minor unit of the entry's keyset ([NUT-01][01]); `index` is the entry's zero-based position. In the canonical encoding below, both `amount` and `index` are serialized as minimal decimal strings (no leading zeros), exactly as base NUT-Exchange serializes `amount`, to avoid IEEE-754 precision loss above 2^53 ([RFC 8785][rfc8785] §3.1). `pool_manifest` MUST contain all `receive` entries first and all `change` entries second. Each entry's `index` MUST equal its zero-based position in `pool_manifest`; indices are unique and contiguous from `0` through `len(pool_manifest) − 1`. + +Each `PoolEntry` is encoded using the [RFC 8785][rfc8785] JCS defined by [NUT-Exchange][exchange], with `amount` and `index` as minimal decimal strings (object keys in lexicographic order: `B_`, `amount`, `id`, `index`, `role`): + +``` +manifest_canonical = JCS(pool_manifest[0]) || JCS(pool_manifest[1]) || ... || JCS(pool_manifest[n-1]) +H_manifest = tagged_hash("Cashu/PAY_TO_UNLOCK/manifest", manifest_canonical) +``` + +## Request format + +Pool-mode participants include the full manifest and a selection bitmap: + +```json +{ + "participants": [ + { + "inputs": "", + "outputs": "", + "pool_manifest": "", + "pool_selection": "" + } + ] +} +``` + +- `outputs`: the selected entries only — these are the `BlindedMessage` values the mint will sign. MUST be a subset of `pool_manifest`, in manifest index order. +- `pool_manifest`: the complete ordered array of `PoolEntry` values defined in [Manifest hash](#manifest-hash). The mint computes `H_manifest` from this array and verifies it against the condition's `data` field. This authenticates every candidate entry as part of the owner-created pool. +- `pool_selection`: a hex-encoded bitmap selecting which manifest entries to sign. The bitmap is exactly `⌈len(pool_manifest) / 8⌉` bytes, encoded as lowercase hex (even-length, no `0x` prefix); a string of any other byte length MUST be rejected. Bit `i` (0-indexed from the least-significant bit of the first byte) corresponds to `pool_manifest[i]`. Bit `1` = selected (include in `outputs`), bit `0` = skipped. Unused trailing bits (bit index `≥ len(pool_manifest)`) MUST be zero. The selected entries, in index order, MUST exactly match the `outputs` array. + +`pool_selection` tells the mint which authenticated candidate outputs to sign. An entry whose bit is zero is an unsigned candidate, not ecash: the mint MUST NOT sign or return it, and signing every manifest entry would violate the per-class conservation rule. The full-manifest form reveals to the mint that all listed `B_` values belong to one authorization, but an unselected value never becomes a proof. The owner MUST NOT discard any unselected entry's secret or blinding factor, or reuse its `B_`, until it has a definitive settlement outcome for the authorization: if no settlement response was received, the owner MUST first determine whether the mint spent the inputs and, if so, recover the actually-signed signatures via [NUT-09][09] by submitting every manifest entry's `BlindedMessage` (only entries the mint did not sign may then be discarded). After successful settlement, refund, or post-expiry recovery, a later transaction requiring that denomination MUST generate a fresh secret, blinding factor, and `B_`. + +Example: a five-entry manifest has receive entries at manifest indices 0, 1, and 2 and change entries at manifest indices 3 and 4. Selecting manifest entries 0, 2, and 4 produces the bitmap `0b00010101` = `0x15`; therefore `pool_selection = "15"`, and `outputs` contains `pool_manifest[0]`, `pool_manifest[2]`, and `pool_manifest[4]`, in that order. + +A single `/v1/exchange` request MAY mix pool-mode and standard-mode participants. Standard-mode participants omit `pool_manifest` and `pool_selection` and are validated under base [NUT-Exchange][exchange] rules; rule 8p derives each participant's receive keyset per mode — from the manifest for pool mode, from the outputs for standard mode. + +Version 1 uses the full manifest. Pool size is logarithmic in the representable receive and change ranges, and every request remains subject to both `max_pool_entries` per participant and NUT-Exchange's `max_request_bytes` for the complete request. The mint MUST reject a request exceeding either limit, and a wallet MUST NOT create a pool-mode authorization that cannot fit both advertised limits. For idempotent retries, a pool-mode participant's canonical record is `JCS({"inputs": ..., "outputs": ..., "pool_manifest": ...}) || hex_decode(pool_selection)` (same JCS rules as base NUT-Exchange; `pool_manifest` in index order), appended to `req_canonical`, so two requests differing only in manifest or selection cannot alias to one cached response. `hex_decode(pool_selection)` is the raw bitmap (`⌈len(pool_manifest)/8⌉` bytes); its length is derivable from the manifest, so no length prefix is needed. Merkle roots and inclusion proofs are not valid version-1 request forms. A future Merkle form MUST use a separately advertised version or mode that defines the leaf encoding, global index binding, tree construction and domain separation, proof encoding, and proof-size limits. + +Response: same as NUT-Exchange — `{signatures: [...]}`, one `BlindSignature` array per participant. Pool-mode participants receive signatures for their selected entries only. + +## Mint validation + +Pool-mode participants require additional validation beyond [NUT-Exchange][exchange] rules 1–5, 9, 11. Rule 3's tag grammar is extended for pool mode: the four pool tags are required and the base optional tags `alt_outputs`, `allow_change`, and `min_output_amount` are forbidden (error 15001); rule 12 (`min_output_amount`) is superseded by rule 9p's `min_receive`: + +6p. **Manifest hash:** hash `pool_manifest` canonically → MUST equal condition `data`. Otherwise reject (error 15011). + +7p. **Selection consistency:** the entries indicated by `pool_selection` MUST exactly match the `outputs` array (same `B_`, `amount`, `id` values in the same order), and `pool_selection` MUST be the canonical bitmap encoding from [Request format](#request-format). Otherwise reject (error 15012). + +8p. **Role/keyset and two-class consistency:** validate the entire `pool_manifest`, not only selected entries. For each pool-mode participant, all `receive` entries MUST share one `id`; all `change` entries MUST use the condition's `offer_keyset`; both roles MUST be present; and the derived receive keyset MUST differ from `offer_keyset`. No other keyset ID may occur in that manifest. Each participant's receive keyset is derived per mode — from the manifest for pool-mode participants, from the outputs under base rule 7 for standard-mode participants. Across all participants, exactly two distinct keyset IDs MUST occur among all conditions' `offer_keyset` values and all derived receive keysets. For every manifest entry, the mint MUST verify that `id` is a known, active keyset (else errors 12001/12002) and that the keyset publishes a signing key for `amount`. Violations of the role, per-mode, or two-class rules are rejected with error 15013. + +9p. **Policy:** parse `rate_n`, `rate_d`, `min_receive`, and `max_debit` as unsigned `u128` values. Reject if parsing fails, if `rate_d = 0`, or if `min_receive = 0`. Using checked `u128` addition, compute `input_total` from the participant's inputs, `receive_total` from selected receive entries, and `change_total` from selected change entries. Reject if any conversion or sum overflows, if `max_debit > input_total`, or if `change_total > input_total`. Only after those checks, compute `debit_total = input_total − change_total` with checked subtraction. Compute both rate products with checked multiplication and reject if either product overflows. Then enforce (reject with error 15014 on any failure): + +- `receive_total × rate_d ≥ debit_total × rate_n` (rate covenant) +- `receive_total ≥ min_receive` (minimum fill, which also requires at least one selected `receive` entry) +- `debit_total ≤ max_debit` (spending cap) + +`debit_total` is gross of fees. Rule 10p burns `input_fees_c` from each offer class `c`, so the value actually delivered to the opposing side is `debit_total − input_fees_offer` in a two-party exchange and, in general, `Σ(debit_total over participants offering c) − input_fees_c` per class `c`. The rate covenant and spending cap use the gross `debit_total`; the worked examples below assume zero input fees. + +10p. **Conservation:** standard per-class conservation from [NUT-Exchange][exchange] rule 10, applied with change entries in the offer keyset (rejection uses error 11005). The mint signs only `outputs`; `pool_manifest` entries are authentication material, not outputs to sign. + +NUT-Exchange rule 7's per-participant single-keyset output constraint is replaced by rule 8p for pool-mode participants, because selected pool outputs may use both that participant's receive keyset and offer keyset for change; standard-mode participants still follow base rule 7. NUT-Exchange rule 8's exactly-two-keysets invariant is not relaxed; rule 8p applies it to the receive keysets derived per mode (manifest or outputs) and to the request as a whole. + +## Example + +Alice wants to swap **10000 cents (100 USD) for 1000 sats** at a limit of 10 cents per sat. Here `input_total = 10000` cents is the value locked in the authorization and `max_debit = 10000` permits the full input to be spent, so a complete 1000-sat fill at Alice's boundary rate (10 cents/sat) returns 0 cents as change; any partial or better-price fill returns the unspent portion as change. + +### Preparation + +**Receive pool** (sats keyset, 10 entries): amounts 1, 2, 4, 8, 16, 32, 64, 128, 256, 512. Any subset sums to 0–1023. + +**Change pool** (USD keyset; amounts are cents, 14 entries): amounts 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192. These entries can represent every integer change amount from 0 through 16383 cents. + +**Manifest:** 24 entries. `H_manifest` is computed from the canonical `PoolEntry` encodings defined above. + +**Policy:** `rate_n = 1, rate_d = 10` (receive at least 1 sat per 10 cents debited, i.e., pay at most 10 cents per sat), `min_receive = 1` sat, `max_debit = 10000` cents. + +**Lock:** one or more `PAY_TO_UNLOCK` proofs totaling 10000 cents, all carrying the same manifest and policy tags and unique per-proof nonces. + +### Settlement — boundary fill of 500 sats + +Counterparty offers 500 sats for 5000 cents (the boundary rate of 10 cents/sat). + +Coordinator selects: + +- Receive: {256, 128, 64, 32, 16, 4} = 500 sats (6 entries) +- Change: {4096, 512, 256, 128, 8} = 5000 cents (5 entries) + +Mint checks: + +- `H_manifest` matches condition `data` ✓ +- Selected entries exactly match the bitmap and `outputs` ✓ +- `input_total = 10000`, `change_total = 5000`, so `debit_total = 10000 − 5000 = 5000` cents ✓ +- Rate boundary: `500 × 10 ≥ 5000 × 1` → `5000 ≥ 5000` ✓ +- `receive_total = 500 ≥ min_receive = 1` ✓ +- `debit_total = 5000 ≤ max_debit = 10000` ✓ +- Conservation (zero input fees): 10000 cents in = 5000 cents change + 5000 cents to the counterparty; 500 sats in = 500 sats out ✓ + +Equality is accepted: this is exactly Alice's limit of 10 cents per sat. If the counterparty instead offered 499 sats for the same 5000-cent debit, the mint would evaluate `499 × 10 ≥ 5000 × 1` → `4990 ≥ 5000`, which is false, and MUST reject the request. + +The mint signs Alice's 11 selected `BlindedMessage` values (6 receive + 5 change). Alice unblinds them to obtain 500 sats and 5000 cents of change. + +### Settlement — better-price partial fill of 237 sats + +Counterparty offers 237 sats for 2000 cents (20.00 USD). + +Coordinator selects: + +- Receive: {128, 64, 32, 8, 4, 1} = 237 sats (6 entries) +- Change: {4096, 2048, 1024, 512, 256, 64} = 8000 cents (6 entries) + +Mint checks: + +- `input_total = 10000`, `change_total = 8000`, so `debit_total = 2000` cents ✓ +- Rate: `237 × 10 ≥ 2000 × 1` → `2370 ≥ 2000` ✓ +- `receive_total = 237 ≥ min_receive = 1` ✓ +- `debit_total = 2000 ≤ max_debit = 10000` ✓ +- Conservation (zero input fees): 10000 cents in = 8000 cents change + 2000 cents to the counterparty; 237 sats in = 237 sats out ✓ + +The strict inequality admits this better price: Alice pays `2000 / 237` ≈ 8.44 cents per sat, less than her maximum of 10 cents per sat. The mint signs Alice's 12 selected `BlindedMessage` values (6 receive + 6 change), and Alice unblinds them to obtain 237 sats and 8000 cents of change. + +## Recovery and refund + +Identical to [NUT-Exchange][exchange]. Pool-mode proofs carry the same `expiry` and `refund` tags. After `expiry`, the owner refunds via NUT-03 swap with a `refund` signature. In the refund path the exchange never committed, so unused pool entries were never signed and may be discarded once the refund is complete; the discard safety rule in [Request format](#request-format) governs the settlement-loss case. + +## Mint info + +```json +{ + "exchange": { + "supported": true, + "version": 1, + "max_participants": "", + "max_inputs": "", + "max_outputs": "", + "max_request_bytes": "", + "idempotent_retries": "", + "max_expiry_seconds": "", + "partial_fill": true, + "max_pool_entries": "" + } +} +``` + +`max_pool_entries` bounds the total number of manifest entries per participant, while `max_request_bytes` bounds the complete serialized request, including every participant's manifest. A wallet MUST satisfy both limits and a mint MUST enforce both limits before signing or mutating state. + +## FAQ + +**Why powers-of-2 denominations?** +`⌈log₂(N)⌉` entries cover every integer from 0 to `N−1` via subset sum. For a 1000-unit range: 10 entries. This keeps wallet cost logarithmic. + +**Can the coordinator steal outputs?** +No. Every selected entry must be in Alice's authenticated manifest (verified by `H_manifest`). The coordinator can only select entries Alice pre-generated with her own blinding factors. It cannot inject its own `BlindedMessage` values. + +**Does the mint enforce best execution?** +No. The rate covenant enforces the owner's limit price. Any subset satisfying the covenant is valid. The coordinator may select the least favorable valid pair. Best execution is an off-mint concern. + +## References + +- [NUT-01](01.md) · [NUT-02](02.md) · [NUT-03](03.md) · [NUT-06](06.md) · + [NUT-09](09.md) · [NUT-10](10.md) · [NUT-11](11.md) · [NUT-12](12.md) · + [NUT-Exchange][exchange] + +[01]: 01.md +[02]: 02.md +[03]: 03.md +[06]: 06.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[exchange]: https://github.com/cashubtc/nuts/pull/410 +[rfc8785]: https://www.rfc-editor.org/rfc/rfc8785.html diff --git a/exchange.md b/exchange.md new file mode 100644 index 00000000..591b30d4 --- /dev/null +++ b/exchange.md @@ -0,0 +1,530 @@ +# NUT-Exchange: Atomic Multi-Asset Exchange + +`draft` (final NUT name, number, and NUT-10 `kind` are provisional) + +`optional` + +`depends on: NUT-02, NUT-03, NUT-06, NUT-07, NUT-09, NUT-10, NUT-11, NUT-12` + +--- + +This NUT defines an atomic exchange of two existing Cashu asset classes at one +mint. Two or more participants each contribute bearer proofs of one asset class +and commit to blinded receive outputs of the other class. The mint spends every +input and signs every output in a single database transaction, or changes +nothing. + +## Premise + +### The problem + +A client-to-client atomic swap of Cashu tokens (for example an HTLC or +adaptor-signature swap) requires both wallets to remain available while they +exchange keys, commitments, signatures, mint requests, and claims. It exposes a +free-option locktime race: the party holding the swap secret (the leader) picks +when to trigger settlement inside the locktime window — completing only if price +has moved in its favor, otherwise letting the swap lapse — a free American option +on the locked rate. And a peer-to-peer swap cannot batch several independent +swaps into one mint transaction: each must be settled separately. + +A Cashu mint already validates proof signatures, maintains spentness, and issues +blind signatures. This NUT uses that existing authority as the atomic settlement +layer, removing the interactive claim sequence and the locktime free option it +creates. + +### Model + +Each participant first locks its bearer proofs to a receive-output commitment +via a [NUT-10][10] `PAY_TO_UNLOCK` condition. The participants' conditioned +proofs and public receive descriptors are assembled into one settlement request +and submitted to the mint. The mint validates every condition and conserves +each asset class independently, then commits all input spends and all output +signatures in one transaction — or changes nothing. + +Two preparation patterns are supported: a **direct two-party swap** where both +participants are online, and a **coordinator-mediated swap** where a relay +assembles matched participants' material (see [Preparation](#preparation)). A +coordinator or relay is **optional** and has no on-mint authority: any holder of +all the valid authorizations may submit a request. + +### Trust boundary and anonymity + +The mint is trusted for the same things [NUT-11][11] P2PK already trusts it +(rejecting invalid spends, maintaining spentness, blind signing) **plus one +atomic database commit**. No transparency or accountability layer is defined +here. Two mitigations bound the added trust: + +1. **A violation is transcript-checkable.** Any party holding the full transcript + (inputs, conditions, output commitments, signatures, expiry) can prove the mint + accepted an exchange that violates a condition or a conservation rule. This + is incidental verifiability, not a published audit log. +2. **Protocol fields carry no stable owner identity.** Fresh per-authorization + `nonce`, refund key, proof secret, and output secret prevent the mint from + performing _identity-selective_ betrayal from protocol fields alone. The mint + still sees asset classes, amounts, timing, and transport metadata, and may + still censor, deny service, or betray an exchange wholesale — the same issuer + trust NUT-11 carries. + +### Scope + +Version 1 supports: + +- one mint; +- two or more participants in one atomic two-class exchange (any N-vs-M shape); +- exactly two existing asset classes, one offered per side; +- owner-precommitted blinded receive outputs, with optional change outputs in the + offer keyset and alternative output bundles for FAK-style orders; +- per-asset-class conservation; and +- one atomic commit (all participants settle or none). + +Version 1 does **not** support: a coordinator as a required party; cross-mint +settlement; general N-way cycles that would require a solver; asset creation or +destruction. + +Separate `/v1/exchange` calls are independent: a failure or retry of one never +rolls back another, since each is its own database transaction. + +## Protocol + +### Terminology + +- **Asset class**: a mint keyset identified by its [NUT-02][02] `id`. A keyset's + unit is part of the class identity. Amounts from different classes MUST NOT be + added or compared. +- **Participant**: one owner in an exchange. Each offers one asset class and + receives the other. +- **Submitter**: whoever posts the settlement request to the mint — a participant + or a relay. It is not a custody role. +- **Change output**: an output in the participant's offer keyset, returning + unspent input value. Enabled by the condition's `allow_change` tag (see below). + +### `PAY_TO_UNLOCK` condition + +A [NUT-10][10] well-known secret `kind` named `PAY_TO_UNLOCK`. A proof carrying +it authorises one exact exchange — or, if `alt_outputs` is present, one exchange +chosen from a finite set of owner-authorised output bundles. + +```json +[ + "PAY_TO_UNLOCK", + { + "nonce": "", + "data": "", + "tags": [ + ["offer_keyset", ""], + ["expiry", ""], + ["refund", ""], + ["coordinator_pubkey", ""], + ["alt_outputs", "", "", "..."], + ["allow_change"], + ["min_output_amount", ""] + ] + } +] +``` + +**Required tags** (MUST appear exactly once): + +- `offer_keyset`: binds the participant's offered asset class. The mint MUST + verify `offer_keyset == Proof.id` on every input (prevents keyset-ID relabeling + when verification keys are shared across keysets). +- `expiry`: unix timestamp. Settlement valid only before it; refund only after. +- `refund`: fresh x-only public key whose private half the owner retains. + +**Optional tags** (MAY appear; each at most once): + +- `alt_outputs`: authorises a finite set of alternative output bundles in + addition to the primary `data`. Each value is a `H_recv` computed identically + to `data`. The submitted output bundle MUST hash to `data` or any listed + alternative. Enables FAK-style orders where the actual fill amount varies (each + bundle includes a different change amount). All inputs in one participant + record MUST carry the identical `alt_outputs` set. +- `allow_change`: if present, outputs MAY include entries in the offer keyset + (change outputs) in addition to entries in the receive keyset. Without this + tag, all outputs MUST use a single keyset (the receive keyset). See [Change + outputs](#change-outputs). +- `min_output_amount`: minimum total receive-keyset output amount (excluding + change). The mint MUST reject if the actual receive output is below this floor. + Prevents a coordinator from filling a tiny amount to consume the authorization + and force a refund. +- `coordinator_pubkey`: optional BIP-340 x-only public key (64 lowercase hex + chars / 32 bytes) binding these proofs to one coordinator. If present on any + input, the settlement requires `coordinator_sig` (see Mint validation); + permitted in standard and pool mode. It prevents the same authorization being + submitted to two coordinators — only the bound key can authorize a distinct + settlement. + +Unknown tags MUST be rejected. `expiry` is decimal unix seconds without leading +zeros. Keyset IDs use their [NUT-02][02] canonical form; the refund key is a +BIP-340 x-only pubkey in hex. + +Each input is validated under its own spending condition. Within one +record, all `PAY_TO_UNLOCK` inputs MUST use the same `data` and the same +tags, with a **unique `nonce` per proof** — for standard participants, +`data` is `H_recv` with the tags `offer_keyset`, `expiry`, `refund`, and +any optional tags; for pool-authorized participants, `data` is +`H_manifest` with the pool tags defined by +[NUT-Exchange-partial-fill][partial-fill]. A record MAY also contain +endpoint-compatible non-`PAY_TO_UNLOCK` inputs (rule 3). +The `nonce` provides per-proof anti-replay; the meaningful authorisation +fields are shared. This allows multiple proofs (e.g., micro-denomination +inputs) in one record without duplicate secrets. + +A record with no `PAY_TO_UNLOCK` inputs at all is a bare record (see +rule 3 for the condition classes and the record predicate). A bare record has no +`H_recv` commitment and no refund path under this NUT: its outputs are +constrained by the global two-class rule (rule 8) and conservation +(rule 10). Rules that read a `PAY_TO_UNLOCK` field (`H_recv`, `refund`, +`expiry`, coordinator binding) apply only to locked records. Across the +exchange, locked records' `offer_keyset` values and receive keysets MUST +lie within the two classes of rule 8. + +`request_digest` and `coordinator_digest` bind every input proof and +witness, every output, every pool field, and every condition-relevant +top-level field, for locked and bare records alike. Recovery restores +outputs only to a party that retained the corresponding blinded messages; +conservation gives the holder of a bare input no claim on outputs another +assembler chose. + +Non-normative cautions: a disclosed bearer proof — or any +non-output-binding condition together with its witness — can fund a +different valid request; keep such material private until direct +submission. For a request containing at least one `PAY_TO_UNLOCK` input, any party +able to assemble a complete valid request holds a timing option until the +earliest `PAY_TO_UNLOCK` `expiry` in it: it can choose whether and when to +submit. A coordinator is optional; when a +`coordinator_pubkey` binds a locked input, the holder of that key holds +the option. + +### Change outputs + +When `allow_change` is present, a participant's output bundle MAY contain entries +in both the receive keyset and the offer keyset. The receive-keyset entries are +the participant's desired receive amount; the offer-keyset entries are change +returned from unspent input. + +Change is determined by per-class conservation (rule 10) at the aggregate level. +The change outputs MUST be included in the committed bundle (`H_recv` or an +`alt_outputs` entry) — the coordinator cannot insert its own change outputs +because it lacks the owner's blinding factors. + +Change outputs do NOT add a third asset class. The exchange still has exactly two +keysets: the offer keyset (now appearing on both input and output sides) and the +receive keyset. + +### Receive-output commitment + +The receive destination is the owner's ordered list of `BlindedMessage` values, +including any change outputs. The canonical encoding of one entry is: + +```json +{"amount": , "id": "", "B_": ""} +``` + +The commitment is a BIP-340 tagged hash over the length-prefixed concatenation +of entries in declared order. Each `entry_canonical` is the entry serialized with +the [RFC 8785][rfc8785] JSON Canonicalization Scheme (JCS): UTF-8, object keys in +lexicographic order, no insignificant whitespace. **Amounts are encoded as +decimal strings** (not JSON numbers) in the canonical form to avoid IEEE-754 +precision loss above 2^53. The length prefix is a 4-byte little-endian unsigned +integer recording the **entry count** (not byte length): + +``` +recv_canonical = uint32_le(len) || entry[0]_canonical || ... || entry[n-1]_canonical +H_recv = tagged_hash("Cashu/PAY_TO_UNLOCK/recv", recv_canonical) +``` + +where `tagged_hash(tag, msg) = SHA256(SHA256(tag) || SHA256(tag) || msg)` and +each `entry_canonical` is `{"amount":"","id":"","B_":""}`. +Amounts are unsigned 64-bit integers; the mint MUST reject outputs whose amounts +are not representable as u64. + +Duplicate entries, unknown fields, non-canonical encodings, and list-prefix +matches MUST be rejected. + +### Canonical encodings + +- **Participant record**: the JSON object `{"inputs": [...], "outputs": [...]}`, + with `inputs` sorted by `(id, secret)` and `outputs` in declared order, + serialized via [RFC 8785][rfc8785] JCS. **All `amount` fields (`Proof.amount` + and `BlindedMessage.amount`) are encoded as decimal strings** (not JSON numbers) + in the canonical form, to avoid IEEE-754 precision loss above 2^53. Amounts + MUST be unsigned integers in `[0, 2^64)` with no leading zeros. The mint MUST + use checked, non-wrapping arithmetic for all sums. +- **`PAY_TO_UNLOCK` condition**: the three required tags (`offer_keyset`, + `expiry`, `refund`) MUST each appear exactly once. Optional tags (`alt_outputs`, + `allow_change`, `min_output_amount`, `coordinator_pubkey`) MAY each appear at + most once. Unknown tags + MUST be rejected. `alt_outputs` values MUST be distinct 64-char hex strings; + the mint MUST reject if `alt_outputs` count exceeds advertised `max_alt_outputs`. + `min_output_amount` is a minimal unsigned decimal string (no leading zeros). +- **Participant order**: records are ordered by the lexicographically smallest + `(id, secret)` among each participant's inputs. Proof secrets are unique + across the whole request, so this is a strict total order. +- **Request digest** (optional, for idempotent retries): + +``` +req_canonical = participant[0]_canonical || ... || participant[n-1]_canonical +request_digest = tagged_hash("Cashu/exchange/request", req_canonical) +coordinator_digest = tagged_hash("Cashu/PAY_TO_UNLOCK/coordinator", req_canonical) +``` + +`coordinator_sig` signs `coordinator_digest` and is excluded from `req_canonical`, +which already binds each proof secret (hence `coordinator_pubkey` and the spent +identifier `Y`), the outputs, and any pool manifest/selection. + +If the mint supports idempotent retries (advertised via `idempotent_retries` in +[NUT-06][06] info), the request digest enables fast retry: a byte-identical +request returns the cached response instead of failing on double-spend. Without +this feature, clients fall back to [NUT-09][09] recovery. + +### Preparation + +#### Two-party direct swap + +Two participants agree on an exact exchange. Both are online. Each participant: + +1. prepares blinded receive outputs and computes `H_recv`; +2. uses an ordinary [NUT-03][03] swap to convert its bearer proofs into + `PAY_TO_UNLOCK` proofs committed to `H_recv`; +3. verifies the [NUT-12][12] DLEQ proofs; +4. one participant assembles both participants' material and POSTs + `/v1/exchange`. + +#### Coordinator-mediated swap with FAK support + +A matching engine pairs orders. For exact-fill orders (FOK), each participant +prepares one `H_recv` as above. For variable-fill orders (FAK), a participant +uses `alt_outputs` + `allow_change` + `min_output_amount`: + +1. Generate the receive outputs for the **maximum** fill (e.g., 100 USD). +2. For each possible fill amount (one per price tick), generate a complete bundle: + receive outputs + change outputs in the offer keyset for the unspent portion. +3. Compute `H_recv` for each bundle. Set `data` to the max-fill bundle; list the + rest in `alt_outputs`. +4. Set `min_output_amount` to the minimum acceptable receive amount. +5. Lock the full input amount in one `PAY_TO_UNLOCK` proof (one NUT-03 swap). +6. The coordinator picks the matching bundle at match time. + +One proof, one swap, tick-level granularity. The coordinator can only select +among owner-authorised bundles; it cannot alter any bundle's contents. + +A participant that has pre-committed sufficient bundles may disconnect before +matching. Unused proofs are reclaimed via refund after `expiry`. + +### Settlement request + +```http +POST https://mint.host:3338/v1/exchange +``` + +```json +{ + "participants": [ + { + "inputs": "", + "outputs": "" + }, + { "...": "one record per participant; N >= 2" } + ], + "coordinator_sig": "" +} +``` + +#### Mint validation + +Before any mutation, the mint MUST verify: + +1. Two or more participant records, each with ≥1 input and ≥1 output. Advertised + limits respected. +2. Every proof is authentic, unspent, unique in the request, and signed by an + active or still-spendable keyset. +3. Every proof satisfies its own spending condition, restricted to + endpoint-compatible conditions: (a) no [NUT-10][10] condition — an + ordinary proof; (b) a canonical `PAY_TO_UNLOCK` condition: the three + required tags each exactly once, optional tags at most once, no unknown + tags; or (c) another supported [NUT-10][10] condition whose validation + depends only on the individual proof and its witness — no request + context of any kind. + Conditions that require a transaction preimage over this endpoint's + request (for example [NUT-11][11] `SIG_ALL`) MUST be rejected: this NUT + defines no such preimage. Within one record, let `L` be its + `PAY_TO_UNLOCK` inputs. If `L` is non-empty the record is a *locked + record*: every input in `L` MUST share the same `data` and tags per the + participant rule. A locked record whose inputs in `L` carry the pool + tags is a *pool record*: all of its inputs MUST be pool-authorized + `PAY_TO_UNLOCK` inputs, `pool_manifest` and `pool_selection` are + accepted only on it, and it is validated by the pool rule set + ([NUT-Exchange-partial-fill][partial-fill] 6p-9p) — base rules 6, 7, + and 12 do NOT apply to it. Any other locked record is a *standard + locked record*: rules 6, 7, and 12 apply to its complete `outputs` + list. If `L` is empty the record is a *bare record*: every + `PAY_TO_UNLOCK`-derived rule is skipped for it, and its outputs remain + subject to rules 8, 9, and 10. +4. No input proof is reused across records; every input is unique. +5. Each `PAY_TO_UNLOCK` input's `Proof.id == offer_keyset` (prevents keyset relabeling). +6. Each standard locked record's `outputs` list hashes to the condition's `data` **or** an + `alt_outputs` entry. (If `alt_outputs` is absent, must match `data` exactly.) +7. For a standard locked record — if `allow_change` is absent: every output `id` is the same (the receive + keyset), and that keyset differs from `offer_keyset`. If `allow_change` is + present: every output `id` is either the receive keyset or the `offer_keyset`; + at least one output MUST use the receive keyset; the receive keyset MUST differ + from `offer_keyset`. +8. Exactly two distinct keysets appear across all actual input `Proof.id` + values and all output `id` values in the request. Bare records are counted + by their actual keysets and remain subject to this two-class rule and to + conservation (rule 10). Per-class conservation does not require equal + participant counts per class, so any two-class shape is valid: 1-vs-N, + N-vs-M, or N-vs-N. +9. Every blinded output is unique, valid, uses an accepted keyset, and has not + been signed before. +10. For each asset class `c` independently, with checked, non-wrapping u64 + arithmetic: `sum(inputs_c) == sum(outputs_c) + input_fees_c`, where + `input_fees_c = (sum(input_fee_ppk over inputs with id == c) + 999) // 1000` + per [NUT-02][02]. Fees are computed and rounded **per class**, not globally. +11. The request is submitted before the minimum `expiry` across all + `PAY_TO_UNLOCK` inputs. This rule is skipped when the request contains + none. +12. For a standard locked record — if `min_output_amount` is present: the total receive-keyset output amount for + that participant MUST be ≥ `min_output_amount`. (Change outputs in the offer + keyset are excluded from this check.) +13. **Coordinator authentication.** If any canonical `PAY_TO_UNLOCK` input carries `coordinator_pubkey` + (BIP-340 x-only key, 64 lowercase hex / 32 bytes), every such input MUST + decode to the same key `K`, and the request MUST carry `coordinator_sig` — a + BIP-340 signature (128 lowercase hex / 64 bytes) valid under `K` over + `coordinator_digest` ([Canonical encodings](#canonical-encodings)). Version 1 + permits one `K` per request; if no canonical `PAY_TO_UNLOCK` input carries `coordinator_pubkey`, + `coordinator_sig` MUST be absent. Reject with 15015 on key disagreement or a + missing, invalid, or unexpected signature. The signature authorizes the + request but does not assert inputs are unspent (rules 2 and 11 remain; + [NUT-07][07] is advisory). Binding proofs to `K` prevents a _different_ + coordinator key from authorizing a distinct settlement. + +**Processing order.** Verify coordinator authentication (rule 13) before any +idempotency-cache lookup. If idempotent retries are supported, canonicalize and +compute `request_digest` (excluding `coordinator_sig`) first; if a committed +response exists, return it. Otherwise apply rules 1–13, then atomic commit. + +#### Atomic commit + +The mint MUST commit in one transaction: + +1. mark every input proof spent; +2. sign every blinded output; +3. persist every `BlindedMessage` / `BlindSignature` for [NUT-09][09] restoration; +4. if idempotent retries are supported, persist response keyed by `request_digest`. + +#### Response + +```json +{ + "signatures": [ + "", + "...", + "" + ] +} +``` + +### Recovery + +Recovery is the direct [NUT-07][07]/[NUT-09][09] path. Because each **owner** +retained its own receive `BlindedMessage` values, the owner — not the submitter — +recovers signatures from the mint and unblinds locally. A wallet SHOULD retry +[NUT-09][09] with bounded backoff when an input is spent but no response was +received. + +### Refund + +A `PAY_TO_UNLOCK` proof has two mutually exclusive spend paths: + +- **Before `expiry`**: only as an input to `/v1/exchange`. +- **At or after `expiry`**: only via an ordinary [NUT-03][03] swap to fresh + outputs of the offered asset class, where each refunded input carries a + `Proof.witness` containing a single BIP-340 Schnorr signature by the `refund` + private key over: + + ``` + refund_digest = tagged_hash("Cashu/PAY_TO_UNLOCK/refund", canonical_refund_request) + ``` + + where `canonical_refund_request` is the [RFC 8785][rfc8785] JCS encoding of the + swap request object `{inputs, outputs}`, with each input `Proof` serialized + **without** its `witness` field (the witness carries the signature and cannot + be included in its own preimage). **All `amount` fields are encoded as decimal + strings** (same rule as participant canonicalization). The mint verifies: + current time ≥ `expiry`; signature valid under `refund` public key; swap issues + outputs in an active keyset of the same unit as `offer_keyset`. Otherwise + rejected. + +The `refund` signature owner-gates the reclaim path: without it, any holder of +the bearer proof could refund it to itself. Liveness is preserved: a failed +`/v1/exchange` commits nothing, and the owner reclaims via refund once `expiry` +passes. + +### Fees + +Input fees follow [NUT-02][02] per-keyset rules, computed and rounded **per asset +class** as defined in rule 10. Change outputs are in the offer keyset and are +included in that class's conservation (they reduce the fee-adjusted output to the +counterparty, not the change recipient). + +### Mint info + +```json +{ + "exchange": { + "supported": true, + "version": 1, + "max_participants": "", + "max_inputs": "", + "max_outputs": "", + "max_request_bytes": "", + "idempotent_retries": "", + "max_alt_outputs": "", + "max_expiry_seconds": "" + } +} +``` + +`max_alt_outputs` bounds the number of alternative `H_recv` values per +condition. `max_expiry_seconds` bounds condition lifetime at [NUT-03][03] swap +time. + +## FAQ + +**Why commit to blinded outputs rather than a receive public key?** +A `BlindedMessage` already commits to amount, receive keyset, and the +wallet-chosen blinded point `B_`. Only the wallet that knows the secret and +blinding factor can unblind the signature and build the proof. + +**Can a submitter or coordinator steal the funds?** +No. (1) It receives only the _blinded_ receive messages; without the blinding +factors it cannot unblind signatures. (2) Inputs are locked by `PAY_TO_UNLOCK` +to owner-authorised bundles — including change outputs, which use the owner's +blinding factors. (3) The only theft vector is the refund key; the owner keeps +a fresh one per authorization. + +**How does FAK work?** +Use `alt_outputs` + `allow_change` + `min_output_amount`. One proof authorises a +finite set of output bundles (one per price tick). The coordinator picks the +matching bundle. See [Coordinator-mediated swap](#coordinator-mediated-swap-with-fak-support). + +## References + +- [NUT-02](02.md) · [NUT-03](03.md) · [NUT-06](06.md) · [NUT-07](07.md) · + [NUT-09](09.md) · [NUT-10](10.md) · [NUT-11](11.md) · [NUT-12](12.md) +- [Maurice Herlihy, Atomic Cross-Chain Swaps](https://arxiv.org/abs/1801.09515) +- [Mazumdar et al., Towards Faster Settlement in HTLC-based Cross-Chain + Swaps](https://arxiv.org/abs/2211.15804) + +[00]: 00.md +[02]: 02.md +[03]: 03.md +[06]: 06.md +[07]: 07.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[rfc8785]: https://www.rfc-editor.org/rfc/rfc8785.html