diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md new file mode 100644 index 00000000..cbf0721d --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -0,0 +1,192 @@ +# AC-00: Introduction and Core Models + +`mandatory` + +--- + +``` +Status: Stable draft +Scope: Shared data models used by AC-01/02/03/04/05/06 +Dependencies: cashu-kvac (KVAC), NUT-01/02 +``` + +This document defines the core data models and normative constraints used by the Anonymous Credentials (AC) extensions. + +## 1. Overview + +[cashu-kvac][KVAC] is treated as a black-box implementation of the cryptographic operations for Anonymous Credentials. This specification does not reproduce cryptographic details; instead it defines how objects are constructed, serialized, and validated between wallet and Mint. + +Terminology differences from the main specs: +- proof → note: the word “proof” refers to zero-knowledge proofs in this spec, so we use “note” for value-bearing objects. +- Mint (capital M) refers to the server component. +- JSON examples may embed symbolic types like `cashu_kvac::{Struct}` to indicate json-encoded KVAC types. + +## 2. Keys + +### 2.1 Mint Private Key + +Rust (illustrative only; use deterministic derivation in production): + +```rust +let scalars = (0..6).map(|_| cashu_kvac::secp::Scalar::random()).collect(); +let mint_privkey = cashu_kvac::models::MintPrivateKey::from_scalars(&scalars).unwrap(); +``` + +### 2.2 Mint Public Key + +```rust +let mint_pubkey: cashu_kvac::models::MintPublicKey = mint_privkey.public_key.clone(); +``` + +Constraints: +- Public keys MUST be compressed Secp256k1 format (33-byte, hex-encoded for wire) per AC-01/NUT-02. +- A keyset contains exactly one MintPublicKey (internally composed of two points, e.g., `Cw` and `I`). + +## 3. Data Models + +All hex strings are lowercase, no `0x` prefix, unless otherwise noted. + +### 3.1 UnsignedNote + +A client-created note not yet signed by the Mint. + +```json +{ + "keyset_id": "", + "amount": 1337, + "script": "", + "unit": "sat", + "tag": "<64-hex>", + "attributes": [ + "", + "" + ] +} +``` + +Constraints: +- keyset_id: hex-encoded keyset identifier (see AC-02/NUT-02). **MUST** correspond to an active keyset when used to request Mint signatures. +- amount: non-negative integer. For zero-amount bootstrap notes, see [AC-06](AC06). +- script: optional [NUT-10](10) script, serialized as bytes and hex-encoded. +- unit: currency unit string (e.g., "sat", "msat"). All Inputs/Outputs within one request MUST share the same unit. +- tag: 32-byte random scalar serialized as 64-hex. MUST be unique per request and SHOULD be globally unique. +- attributes: tuple of AmountAttribute and ScriptAttribute constructed by the client. + +Example attribute creation: + +```rust +let amount_attr = cashu_kvac::models::AmountAttribute::new(1337, Some("deadbeefdeadbeefdeadbeefdeadbeef")); +let script_attr = cashu_kvac::models::ScriptAttribute::new(b"", Some("baadc0debaadc0debaadc0debaadc0de")); +``` + +### 3.2 Output + +The Mint-visible view of an UnsignedNote. + +```json +{ + "id": "", + "tag": "<64-hex>", + "commitments": ["<66-hex>", "<66-hex>"] +} +``` + +Constraints: +- id **MUST** equal `unsigned_note.keyset_id`. +- commitments[0] is the amount commitment; commitments[1] is the script commitment. +- The pair (id, tag) **MUST** be unique. + +### 3.3 MAC + +A Mint signature over Output commitments and tag. + +```rust +let mac = MAC::generate( + &mint_privkey, + &output.commitments.0, + Some(&output.commitments.1), + Some(output.tag), +); +``` + +### 3.4 Note (signed) + +```json +{ + "keyset_id": "", + "amount": 1337, + "script": "", + "unit": "sat", + "tag": "<64-hex>", + "mac": "<66-hex>", + "attributes": [ + "", + "" + ], + "issuance_proof": "" +} +``` + +Constraints: +- Fields mirror UnsignedNote. +- `issuance_proof` **MUST** verify against the MintPublicKey for `keyset_id`. + +### 3.5 Input + +Derived from a Note to spend it. + +```json +{ + "id": "", + "script": "", + "randomized_commitments": "", + "witness": "" +} +``` + +Construction: + +```rust +let randomized_commitments = cashu_kvac::models::RandomizedCommitments::from_attributes_and_mac( + ¬e.attributes.0, + Some(¬e.attributes.1), + note.tag, + note.mac, + true, +); +``` + +Constraints: +- id MUST equal `note.keyset_id`. +- witness/script handling per NUT-10/11/14. + +## 4. Identifiers + +- Mint identifies Inputs by `input.randomized_commitments.Cv` (nullifier base) and Outputs by `output.tag`. +- Clients identify Notes/UnsignedNotes by `tag`. + +## 5. Validation and Interop Rules + +- All amounts within a single request **MUST** be expressed in the same `unit`. +- Keyset ids used for Outputs **MUST** refer to active keysets (see [AC-01](AC01)/[AC-02](AC02)) at the time of issuance. +- Tags **MUST** be unique per request to avoid ambiguity and replay within the same batch. + +## 6. Security and Policy Notes + +- For deterministic wallets, `tag` and attribute blinding factors **SHOULD** be derived per [AC-07](07) to allow recovery. When deterministic mode is used, clients **MUST** ensure counters are allocated per-output and not reused. +- Tags and blinding factors **MUST** be generated with cryptographically secure randomness. +- Scripts ([NUT-10](10)) may reveal spending policy to the Mint via commitments; avoid embedding sensitive metadata. +- IssuanceProof verification **MUST** use the correct MintPublicKey from the stated keyset. +- Nullifier reuse is prevented by the Mint; clients **SHOULD** treat spent Notes as invalid after a successful swap. + +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md +[AC-06]: AC06.md +[AC-07]: AC07.md +[KVAC]: https://github.com/lollerfirst/cashu-kvac.git +[01]: 01.md +[10]: 10.md +[11]: 11.md +[14]: 14.md +[02]: 02.md diff --git a/protocol-extensions/anonymous-credentials/AC01.md b/protocol-extensions/anonymous-credentials/AC01.md new file mode 100644 index 00000000..dbd7d0ef --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -0,0 +1,87 @@ +# AC-01: Mint Public Key Exchange + +`mandatory` + +--- +``` +Status: Stable draft +Scope: Retrieval of active and specific Mint keysets +Dependencies: AC-00, AC-02, NUT-02 +``` + +This document outlines how wallets obtain the Mint’s active keysets and optionally retrieve a specific keyset by id. + +## 1. Endpoints + +- GET `/v1/kvac/keys` — list of active keysets +- GET `/v1/kvac/keys/{keyset_id_hex}` — a specific keyset (active or inactive) + +Content-Type: application/json + +## 2. Responses + +### 2.1 GET /v1/kvac/keys + +```json +{ + "keysets": [ + { + "id": "", + "unit": "", + "final_expiry": "", + "keys": { + "Cw": "", + "I": "" + } + } + ] +} +``` + +Constraints: +- Returned keysets MUST be active (the Mint signs Outputs for them). +- Public keys MUST be compressed Secp256k1 format (33 bytes, hex). +- `id` MUST be recomputable from `keys` (see AC-02/NUT-02). +- `unit` defines the keyset’s unit (e.g., "sat"); clients MUST respect unit consistency across requests (see AC-00/AC-03). +- `final_expiry` MAY be provided; after expiry, notes from the keyset MAY be invalidated by Mint policy (see AC-02). + +### 2.2 GET /v1/kvac/keys/{keyset_id_hex} + +Returns the keyset regardless of active status: + +```json +{ + "keysets": [ + { + "id": "", + "unit": "", + "final_expiry": "", + "keys": { + "Cw": "", + "I": "" + } + } + ] +} +``` + +## 3. Client Behavior + +- SHOULD verify that `id` recomputes from the provided public keys (NUT-02). +- SHOULD fetch active keysets before issuing requests that involve Outputs (AC-03/AC-06). +- SHOULD cache and refresh active keysets periodically to handle rotation. + +## 4. Errors + +- 400 Bad Request — malformed `keyset_id` +- 404 Not Found — unknown `keyset_id` +- 5xx — server-side failures + +## 5. Security and Policy Notes + +- Key rotation: wallets SHOULD refresh the active set before constructing Outputs. +- Mint MAY rotate keys without prior notice; clients SHOULD handle changed active sets. + +[AC-00]: AC00.md +[AC-02]: AC02.md +[02]: 02.md diff --git a/protocol-extensions/anonymous-credentials/AC02.md b/protocol-extensions/anonymous-credentials/AC02.md new file mode 100644 index 00000000..b98f0a53 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -0,0 +1,62 @@ +# AC-02: Keysets and Fees + +`mandatory` + +--- +``` +Status: Stable draft +Scope: Listing all keysets and their fee policy +Dependencies: AC-00, AC-01, NUT-02 +``` + +KVAC keysets largely follow NUT-02. This document specifies the endpoint and wire format for retrieving all keysets and fee parameters used by Anonymous Credentials extensions. + +## 1. Endpoint + +- GET `/v1/kvac/keysets` +- Content-Type: application/json + +## 2. Response + +```json +{ + "keysets": [ + { + "id": "", + "unit": "", + "active": true, + "final_expiry": "", + "input_fee_fixed": "" + } + ] +} +``` + +Field semantics: +- `id`: keyset identifier recomputable from public keys (see NUT-02). +- `unit`: currency unit string for this keyset. +- `active`: whether the Mint will sign new Outputs for this keyset. +- `final_expiry`: optional Unix epoch after which the keyset may be permanently deleted. +- `input_fee_fixed`: optional fixed fee applied to the inputs in swap flows. If omitted, treated as 0. + +Constraints: +- Clients **MUST** treat `active=false` keysets as valid for Inputs but NOT for new Outputs. + +## 3. Client Behavior + +- SHOULD fetch `/v1/kvac/keysets` periodically to handle rotation and fee changes. +- MUST ensure Outputs reference active keysets only. +- SHOULD align units within a single request (see AC-00/AC-03). + +## 4. Errors + +- 5xx — server-side failures + +## 5. Security and Policy Notes + +- Keyset rotation and expiries can invalidate issuance; wallets SHOULD re-fetch before constructing requests that include Outputs. +- Fee policy changes are Mint-defined; clients SHOULD not assume stability without refresh. + +[AC-00]: AC00.md +[AC-03]: AC03.md +[02]: 02.md diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md new file mode 100644 index 00000000..bc7cff13 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -0,0 +1,164 @@ +# AC-03: Swap Transaction + +`mandatory` + +--- + +``` +Status: Stable draft +Scope: Spend existing Notes (Inputs) to obtain signatures on new UnsignedNotes (Outputs) +Dependencies: AC-00, AC-01, AC-02 +``` + +This document describes the process by which a client presents signed `Note`s as Inputs and obtains Mint signatures on new `UnsignedNote`s as Outputs, subject to correctness and fee policy. + +## 1. Endpoint + +- Method: POST +- Path: `/v1/kvac/swap` +- Content-Type: application/json + +## 2. Request + +The client sends a `SwapRequest`: + +```json +{ + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": "", + "mac_proofs": ["", ...], + "range_proof": "" +} +``` + +Constraints: +- inputs.len() == 2 (exactly two inputs required) +- outputs.len() == 2 (exactly two outputs required) +- mac_proofs.len() == 2 +- All Inputs/Outputs within the request **MUST** share the same `unit` (see AC-00). +- Each `outputs[i].id` **MUST** correspond to an active keyset (AC-01/AC-02). +- Tags **MUST** be unique within the request. +- Transcript ordering for proofs **MUST** be consistent between client and Mint (see §3). + +Decoy shaping rules: +- If the wallet has fewer than two Inputs, it **MUST** obtain zero-amount Notes from [AC-06](AC06) and include them as decoy Inputs. +- If the wallet has fewer than two Outputs, it **MUST** include a decoy Output encoding amount 0. The decoy Output **MUST** be covered by the RangeProof like any other Output. +## 3. Mint Verification + +Upon receiving `SwapRequest`, the Mint performs: + +1) Unit and keyset checks +- Verify single-unit consistency across all Inputs/Outputs. +- Verify each Output’s keyset id is active. + +2) Script verification +- For each Input, extract `script`/`witness` if present and verify per [NUT-11](11) or [NUT-14](14) or any other future spending condition. +- Reject on spending conditions failure. + +3) Nullifier checks +- Derive nullifier from `input.randomized_commitments.Cv`. +- Reject if nullifier already seen (double-spend prevention). + +4) Initialize transcript + +```rust +let mut transcript = cashu_kvac::transcript::CashuTranscript::new(); +``` + +5) Verify MacProofs (one per Input) + +```rust +cashu_kvac::kvac::MacProof::verify( + &mint_privkeys[input.id], + &input.randomized_commitments, + &input.script, // None if no script + mac_proof, + &mut transcript, +); +``` + +6) Verify RangeProof for Outputs + +```rust +let amount_commitments: Vec = outputs.iter().map(|o| o.commitments.0).collect(); +cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range_proof); +``` + +7) Verify BalanceProof + +- Let `delta` be the fee-adjusted difference. For no-fee keysets, `delta = 0`. +- With per-input fees (see [AC-02](AC02)), `delta = input_fee`. + +```rust +cashu_kvac::kvac::BalanceProof::verify( + &inputs_randomized_commitments, + &outputs_amount_commitments, + delta as i64, + balance_proof, + &mut transcript, +); +``` + +Reject if any verification fails. + +## 4. Response + +`SwapResponse`: + +```json +{ + "outputs": [, ...], + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] +} +``` + +- `issued_macs[i]` and `issuance_proofs[i]` correspond to `outputs[i]`. + +## 5. Client Verification and Note Assembly + +For each (`UnsignedNote`, `MAC`, `IssuanceProof`) triple: + +```rust +cashu_kvac::kvac::IssuanceProof::verify( + &mint_pubkey, // derived from outputs[i].id via AC-01/02 + unsigned_note.tag, + mac, + &unsigned_note.attributes.0, // amount attribute + Some(&unsigned_note.attributes.1), + issuance_proof.clone(), +); +``` + +If verification succeeds, the wallet combines `UnsignedNote`, `MAC`, and `IssuanceProof` into a `Note` ([AC-00](AC00)). + +## 6. Errors + +- 400 Bad Request + - Empty `inputs`/`outputs` + - Length mismatch (`inputs` vs `mac_proofs`) + - Malformed data or proof encoding + - Mixed units +- 403 Forbidden + - Script policy violation (NUT-11/14 verification failed) +- 409 Conflict + - Nullifier already seen (double-spend) +- 422 Unprocessable Entity + - Inactive/unknown keyset id + - Invalid MacProof, RangeProof, or BalanceProof +- 5xx Internal Server Error + - Server-side failures + +## 7. Security and Policy Notes + +- Proof ordering in the transcript **MUST** be identical between client and Mint. +- Keyset rotation: wallets **SHOULD** fetch active keysets before constructing Outputs. +- Fee policy: clients **MUST** match Mint rounding/fee policy for `delta`. +- Privacy: avoid reusing tags; keep scripts minimal to policy needs. + +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[11]: 11.md +[14]: 14.md diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md new file mode 100644 index 00000000..5558fdf9 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -0,0 +1,249 @@ +# AC-04: Mint Transaction + +`mandatory` + +--- + +``` +Status: Stable draft +Scope: Quoting and executing mint to obtain signatures on new Outputs after external payment +Dependencies: AC-00, AC-01, AC-02, AC-03, NUT-20, NUT-23, NUT-25 +``` + +Minting is a two-step process: +1. Request a quote for the chosen payment method. +2. After payment, submit a mint request to obtain MACs over client-provided Outputs. + +## 1. Supported methods + +Method-specific NUTs describe payment handling: +- [NUT-23][23] — bolt11 Lightning invoices +- [NUT-25][25] — bolt12 Offers + +## 2. Endpoints + +- POST `/v1/mint/quote/{method}` — request a mint quote +- GET `/v1/mint/quote/{method}/{quote_id}` — check quote status +- POST `/v1/kvac/mint/{method}` — execute mint after payment + +Content-Type: application/json + +## 3. Request/Response: Mint Quote + +### 3.1 Request (quote) + +```http +POST https://mint.host:3338/v1/mint/quote/{method} +``` + +Body (baseline, method-specific fields may apply): +```json +{ + "unit": "" +} +``` + +### 3.2 Response (quote) + +```json +{ + "quote": "", + "request": "", + "unit": "" + // method-specific fields may be included +} +``` + +Notes: +- `quote` is a unique, random identifier used to look up payment state. It MUST NOT be derivable from the payment request. +- Keep `quote` secret; use [NUT-20][20] locks to mitigate front‑running. + +### 3.3 Check quote status + +```http +GET https://mint.host:3338/v1/mint/quote/{method}/{quote_id} +``` + +Response: same structure as initial quote response; method-specific fields may indicate status. + +## 4. Request: Execute Mint + +After paying the quote, the wallet submits: + +```http +POST https://mint.host:3338/v1/kvac/mint/{method} +``` + +Body `MintRequest` (baseline): +```json +{ + "quote": "", + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": "", + "mac_proofs": ["", ...], + "range_proof": "" + // method-specific fields may be included +} +``` + +Constraints: +- inputs.len() == 2 (exactly two inputs required) +- outputs.len() == 2 (exactly two outputs required) +- mac_proofs.len() == 2 +- All Inputs/Outputs **MUST** share the same `unit` (see [AC-00](AC00)). +- Each `outputs[i].id` **MUST** correspond to an active keyset ([AC-01](AC01)/[AC-02](AC02)). +- `quote`s **MUST** be known, paid, and unexpired. +- Transcript ordering for proofs **MUST** match the Mint (see §5). + +Decoy shaping rules: +- If the wallet has fewer than two Inputs, it **MUST** obtain zero-amount Notes from [AC-06](AC-06.md) and include them as decoy Inputs. +- If the wallet has fewer than two Outputs, it **MUST** include a decoy Output encoding amount 0. The decoy Output **MUST** be covered by the RangeProof like any other Output. +Balance constraint (delta): +- Let `keyset_fees = input_fee` per [AC-02](AC02) for the given keysets present in `input_fee`. +- The BalanceProof MUST verify with `delta = keyset_fees - quote_amount` so that `inputs - outputs == keyset_fees - quote_amount`. + - If there are no inputs or no keyset fees, this reduces to `inputs - outputs == -quote_amount` (i.e., outputs encode exactly `quote_amount` more than inputs). + +## 5. Mint Verification + +Upon receiving `MintRequest`, the Mint performs: + +1) Quote checks +- Verify `quote` exists, is paid, and not expired. Reject otherwise. + +2) Unit/keyset checks +- Verify single-unit consistency across Inputs/Outputs. +- Ensure each Output’s keyset id is active. + +3) Script verification +- For each Input, verify `script`/`witness` per [NUT-11] or [NUT-14]. Reject on policy failure. + +4) Nullifier checks (for each Input) +- Derive nullifier from `input.randomized_commitments.Cv` and reject if already seen. + +5) Initialize transcript +```rust +let mut transcript = cashu_kvac::transcript::CashuTranscript::new(); +``` + +6) Verify MacProofs (one per Input) +```rust +cashu_kvac::kvac::MacProof::verify( + &mint_privkeys[input.id], + &input.randomized_commitments, + &input.script, + mac_proof, + &mut transcript, +); +``` + +7) Verify RangeProof for Outputs +```rust +let amount_commitments: Vec = outputs.iter().map(|o| o.commitments.0).collect(); +cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range_proof); +``` + +8) Verify BalanceProof +```rust +cashu_kvac::kvac::BalanceProof::verify( + &inputs_randomized_commitments, + &outputs_amount_commitments, + (fees as i64) - (quote_amount as i64), + balance_proof, + &mut transcript, +); +``` + +9) Issue MACs and IssuanceProofs +```rust +let mac = cashu_kvac::models::MAC::generate( + &mint_privkeys[output.id], + &output.commitments.0, + Some(&output.commitments.1), + Some(output.tag), +); +let issuance_proof = cashu_kvac::kvac::IssuanceProof::create( + &mint_privkeys[output.id], + output.tag, + mac, + output.commitments.0, + Some(output.commitments.1), +); +``` + +## 6. Response + +`MintResponse` (identical structure to `SwapResponse` in AC-03): + +```json +{ + "outputs": [, ...], + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] +} +``` + +## 7. Client Verification and Note Assembly + +As in [AC-03](AC03)/[AC-06](AC06): verify `IssuanceProof` per Output and assemble `Note`s if verification succeeds. + +## 8. Errors + +- 400 Bad Request + - Empty `outputs` + - Length mismatch (`inputs` vs `mac_proofs`) + - Malformed proof encoding + - Missing/invalid `quote` format +- 404 Not Found + - Unknown `quote` +- 409 Conflict + - Quote unpaid or expired + - Nullifier already seen (double-spend) +- 422 Unprocessable Entity + - Inactive/unknown keyset id + - Invalid MacProof, RangeProof, or BalanceProof +- 5xx Internal Server Error + +## 9. Settings + +The Mint advertises supported method-unit pairs in [NUT-06][06] under key `AC4`: + +```json +{ + "AC4": { + "methods": [ + , + ... + ], + "disabled": + } +} +``` + +`MintMethodSetting`: +```json +{ + "method": "", + "unit": "", + "min_amount": "", + "max_amount": "", + "options": "" +} +``` + +## 10. Security and Policy Notes + +- Keep `quote` secret; use [NUT-20][20] to bind minting to an authenticated key. +- Transcript ordering **MUST** match between client and Mint. +- Outputs **MUST** reference active keysets; clients **SHOULD** refresh keysets (AC-01/AC-02) before minting. +- Tags **MUST** be unique within the request. + +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md +[AC-06]: AC06.md +[06]: 06.md +[20]: 20.md +[23]: 23.md +[25]: 25.md diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md new file mode 100644 index 00000000..899506a9 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -0,0 +1,198 @@ +# AC-05: Melt Transaction + +`mandatory` + +--- +``` +Status: Stable draft +Scope: Quoting and executing melt (redeem notes for off-Mint payout) +Dependencies: AC-00, AC-01, AC-02, AC-03, NUT-23, NUT-25 +``` + +Melting is a two-step process: +1. Request a melt quote for an external payment. +2. Provide Inputs and execute the melt. + +## 1. Supported methods + +- [NUT-23][23] — bolt11 Lightning invoices +- [NUT-25][25] — bolt12 Lightning offers + +## 2. Endpoints + +- POST `/v1/melt/quote/{method}` — request a melt quote for a given external request and unit +- GET `/v1/melt/quote/{method}/{quote_id}` — check quote status +- POST `/v1/kvac/melt/{method}` — execute melt + +Content-Type: application/json + +## 3. Request/Response: Melt Quote + +### 3.1 Request (quote) + +```http +POST https://mint.host:3338/v1/melt/quote/{method} +``` + +Body (baseline): +```json +{ + "request": "", + "unit": "" +} +``` + +### 3.2 Response (quote) + +```json +{ + "quote": "", + "amount": , + "unit": "", + "state": "", + "expiry": , + "change": { + "fee_return": [, ], + "outputs": [, ...], + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] + } +} +``` + +Semantics: +- `amount`/`unit` are what the wallet must provide as Inputs, including estimated network fees. +- `state` ∈ {UNPAID, PENDING, PAID}. +- When `state == PAID`, `change` MUST be present and include Outputs and signatures for any returned amount. +- `fee_return` is optional; if present, it is `[index_of_tweaked_output, amount_added]`. + +## 4. Fee Return Handling + +After the external payment is executed, if actual network fees are less than estimated: + +``` +overpaid_network_fees = quote_amount - network_fees - keyset_fees +``` + +- If `overpaid_network_fees < 0`, abort this step (never subtract from outputs). +- Otherwise, tweak one output’s amount commitment by `overpaid_network_fees`: + +```rust +outputs[last].commitments[0].tweak_amount(cashu_kvac::secp::TWEAK_KIND::AMOUNT, overpaid_network_fees); +``` + +- Set `fee_return = [last, overpaid_network_fees]`. +- IMPORTANT: Mint MUST compute MACs and IssuanceProofs AFTER applying this tweak, or proofs will not validate. + +## 5. Request: Execute Melt + +```http +POST https://mint.host:3338/v1/kvac/melt/{method} +``` + +Body `MeltRequest` (baseline): +```json +{ + "quote": "", + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": "", + "mac_proofs": ["", ...], + "range_proof": "" +} +``` + +Constraints: +- inputs.len() == 2 (exactly two inputs required) +- outputs.len() == 2 (exactly two outputs required) +- mac_proofs.len() == 2 +- All Inputs/Outputs MUST share the same `unit`. +- Each Output’s `id` MUST be an active keyset. +- Transcript ordering for proofs MUST match the Mint. + +Decoy shaping rules: +- If the wallet has fewer than two Inputs, it SHOULD obtain zero-amount Notes from [AC-06](AC06) and include them as decoy Inputs. +- If the wallet has fewer than two Outputs, it SHOULD include a decoy Output encoding amount 0. The decoy Output MUST be covered by the RangeProof like any other Output. + +Balance constraint (delta): +- Let `fees = sum(input_fees)` per [AC-02](AC02). +- Let `amount = quote_amount`. +- The BalanceProof MUST verify with `delta = amount + fees` (i.e., `inputs - outputs == amount + fees`). +## 6. Mint Verification + +1) Quote checks: `quote` exists, unpaid/valid; for methods with external payment, this call may block until success/failure. +2) Unit/keyset checks: single-unit, Outputs reference active keysets. +3) Script verification: per NUT-11/14 for each Input. +4) Nullifier checks: reject if already seen. (one per Input) +5) Transcript init: `CashuTranscript::new()`. +6) Verify MacProofs: one per Input. +7) Verify RangeProof: for Outputs’ amount commitments. +8) Verify BalanceProof with `delta = - (amount + fees)`. +9) If overpaid network fees occurred, tweak Output commitment as in §4, then issue MACs and IssuanceProofs for all Outputs. + +## 7. Response + +On success, the Mint returns the payment `state` and, if applicable, the method-specific proof of payment and change outputs: + +```json +{ + "state": "", + "change": { + "fee_return": [, ], + "outputs": [, ...], + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] + } +} +``` + +## 8. Errors + +- 400 Bad Request — malformed request, mixed units, proof length mismatch +- 403 Forbidden — script policy violation +- 409 Conflict — nullifier already seen, invalid quote state +- 422 Unprocessable Entity — inactive/unknown keyset id, invalid proofs +- 429 Too Many Requests — Mint policy +- 5xx Internal Server Error + +## 9. Settings + +Advertised in [NUT-06][06] under key `AC5`: + +```json +{ + "AC5": { + "methods": [ + , + ... + ], + "disabled": + } +} +``` + +`MeltMethodSetting`: +```json +{ + "method": "", + "unit": "", + "min_amount": "", + "max_amount": "", + "options": "" +} +``` + +## 10. Security and Policy Notes + +- Calls may block while external payment executes; clients **SHOULD** use long/disabled timeouts. +- Do not subtract from Outputs on fee reconciliation; only add and re-sign. +- Clients **SHOULD** refresh active keysets before constructing Outputs. +- Avoid reusing tags within a batch; ensure strong randomness. + +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md +[06]: 06.md +[23]: 23.md +[25]: 25.md diff --git a/protocol-extensions/anonymous-credentials/AC06.md b/protocol-extensions/anonymous-credentials/AC06.md new file mode 100644 index 00000000..8a0aaae3 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC06.md @@ -0,0 +1,148 @@ +# AC-06: Bootstrap Requests + +Bootstrap requests let wallets obtain initial zero-amount Notes without providing inputs. Each Output must be accompanied by a proof that its amount is exactly zero. The Mint issues MACs for such Outputs and returns IssuanceProofs. No inputs, no MacProofs, no RangeProof, and no BalanceProof are used in this flow. + +## 1. Endpoint + +- Method: POST +- Path: `/v1/kvac/bootstrap` +- Content-Type: application/json + +The Mint MAY enforce request limits or anti-spam measures but MUST NOT require inputs for this endpoint. + +## 2. Request + +The client sends a `BootstrapRequest`: + +```json +{ + "outputs": [, ...], + "bootstrap_proofs": [, ...] +} +``` + +Where: + +- `outputs` are `Output`s as defined in [AC-00][AC-00]. +- `bootstrap_proofs` contains one `BootstrapProof` per output (serialized `cashu_kvac::ZKP`), proving the corresponding output’s amount is exactly zero. + +```rust +let bootstrap_proof = BootstrapProof::create(&bootstrap_attr, client_transcript.as_mut()); +``` + +Constraints: + +- `outputs.len() > 0` +- `outputs.len() == bootstrap_proofs.len()` +- Each `outputs[i].id` MUST correspond to an active keyset (see [AC-01][AC-01] and [AC-02][AC-02]). +- Wallets SHOULD avoid duplicate `tag`s within the same request. + +## 3. Mint verification + +Upon receiving a `BootstrapRequest`, the Mint performs the following: + +1) Pre-flight checks + +- Ensure `outputs` is non-empty and lengths match `bootstrap_proofs`. +- Ensure each `outputs[i].id` refers to an active keyset. + +2) Initialize transcript + +```rust +let mut transcript = cashu_kvac::transcript::CashuTranscript::new(); +``` + +3) Verify zero-amount per output + +For each index i: + +```rust +let ok = cashu_kvac::kvac::BootstrapProof::verify( + &outputs[i].commitments.0, // amount commitment + bootstrap_proofs[i].clone(), + &mut transcript, +); +if !ok { /* reject */ } +``` + +The Mint MUST reject the request if any proof fails. There are no inputs, no `MacProof`s, and no balance/range proofs in this flow. + +4) Issue MACs and IssuanceProofs + +For each valid `output`: + +```rust +let mac = cashu_kvac::models::MAC::generate( + &mint_privkeys[output.id], + &output.commitments.0, + Some(&output.commitments.1), + Some(output.tag), +); + +let issuance_proof = cashu_kvac::kvac::IssuanceProof::create( + &mint_privkeys[output.id], + output.tag, + mac, + output.commitments.0, + Some(output.commitments.1), +); +``` + +Note: Issuance proof generation does not use the transcript. + +## 4. Response + +The Mint responds with `BootstrapResponse`, identical in structure to `SwapResponse` from [AC-03][AC-03]: + +```json +{ + "outputs": [, ...], + "issued_macs": [, ...], + "issuance_proofs": [, ...] +} +``` + +- `outputs` are echoed back for convenience (aligns with [AC-03][AC-03]). +- `issued_macs[i]` and `issuance_proofs[i]` correspond to `outputs[i]`. + +## 5. Client verification and Note assembly + +For each (`UnsignedNote`, `MAC`, `IssuanceProof`) triple: + +```rust +cashu_kvac::kvac::IssuanceProof::verify( + &mint_pubkey, // derived from outputs[i].id via AC-01/02 + unsigned_note.tag, + mac, + &unsigned_note.attributes.0, // amount attribute + Some(&unsigned_note.attributes.1), // script attribute + issuance_proof.clone(), +); +``` + +If verification succeeds, the wallet combines `UnsignedNote` with `MAC` and `IssuanceProof` to form a `Note` (see [AC-00][AC-00]). + +## 6. Errors + +- 400 Bad Request + - Empty `outputs` + - Length mismatch (`outputs` vs `bootstrap_proofs`) + - Malformed data or proof encoding +- 422 Unprocessable Entity + - Inactive/unknown keyset id + - Invalid `BootstrapProof` (any output fails zero-amount proof) +- 429 Too Many Requests + - Rate-limited by Mint policy +- 5xx Internal Server Error + - Unexpected server-side failures + +## 7. Security and policy notes + +- Each output must individually prove zero amount; RangeProof and BalanceProof are not used in bootstrap. +- The Mint MAY apply rate limits or other DoS mitigations. +- Keyset rotation: wallets SHOULD fetch active keysets via [AC-01][AC-01]/[AC-02][AC-02] prior to bootstrap. + +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md new file mode 100644 index 00000000..e1bc517b --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -0,0 +1,135 @@ +# AC-07: Deterministic Derivation of Tag and Blinding Factors + +`mandatory` + +--- + +Status: Draft proposal +Scope: Deterministic derivation for KVAC attributes and tag to enable wallet recovery +Dependencies: AC-00, AC-01, AC-02, AC-03, AC-06; NUT-09, NUT-13 + +This document defines a deterministic derivation scheme for three per-note secrets used by KVAC-based Anonymous Credentials: +- tag (UnsignedNote.tag) +- amount attribute blinding factor (for AmountAttribute) +- script attribute blinding factor (for ScriptAttribute) + +It mirrors and extends [NUT-13][13] from two secrets to three and adopts the versioned derivation model from NUT-13 v1/v2. We retain BIP39 mnemonic → seed, but we DO NOT use BIP32 for keyset version v2. Instead, we use an HMAC-SHA256 KDF (see §2.2). BIP32 remains only for legacy keyset version v1 (backward compatibility). The goal is to allow wallets to regenerate these values for recovery (e.g., via a future AC restore endpoint akin to [NUT-09][09]). + +## 1. Overview + +- Wallets derive secrets from a user-held BIP39 mnemonic → BIP32 root. +- Derivations are scoped per keyset to avoid collisions across rotations (keyset-specific counters). +- For each note (i.e., for each Output/UnsignedNote the wallet constructs), the wallet increments a per-keyset counter and derives exactly three values: tag, amount_blind, script_blind. +- The same derivation MUST be used during recovery to reconstruct notes/outputs and request reissuance. + +## 2. Versioned Deterministic Derivation + +We adopt the versioned secret derivation model (see changes introduced in NUT-13 v1/v2). Wallets MUST first determine the keyset version from the keyset id (per NUT-13 changes): +- v1 (legacy, version 00): BIP32-based derivation (backward compatible) +- v2 (version 01): HMAC-SHA256 KDF; BIP32 MUST NOT be used + +Wallets MUST retain BIP39 mnemonic → seed, but MUST choose the derivation algorithm based on the keyset version as below. + +### 2.1 v1 (legacy) — BIP32 path (extended to 3 components) + +- Purpose' = 129372' (UTF‑8 for 🥜) +- Coin type' = 1' (reserved for AC/KVAC; base Cashu uses 0') +- Keyset id' = keyset_id_int' (see §3) +- Counter' = counter' (per‑keyset, incrementing) +- Component = c ∈ {0,1,2} + - c=0 → tag + - c=1 → amount attribute blinding factor + - c=2 → script attribute blinding factor + +Derivation: + +``` +m / 129372' / 1' / keyset_id_int' / counter' / c +``` + +The derived private key at this path is interpreted as a 32‑byte scalar modulo the Secp256k1 order n. + +### 2.2 v2 — HMAC-SHA256 KDF (no BIP32) + +For version 01 keysets, derive each component with an HMAC‑SHA256 keyed by the BIP39 seed. This mirrors the updated NUT‑13 v2 KDF and adds a component byte to obtain three distinct outputs. + +Let: +- seed = BIP39 seed (512-bit from mnemonic) +- keyset_id_bytes = raw keyset id bytes (v1: 8 bytes; v2: 33 bytes) +- counter_bytes = 8‑byte unsigned big‑endian counter +- c_bytes = 1‑byte component selector where c ∈ {0,1,2} +- label = ASCII string "Cashu_KDF_HMAC_SHA256" + +Compute for each c: + +``` +raw = HMAC_SHA256( key=seed, + data = label || keyset_id_bytes || counter_bytes || c_bytes ) +``` + +Interpret `raw` as a big‑endian 256‑bit integer and reduce modulo the Secp256k1 order n to obtain a scalar in [1, n−1]. We keep the whole 32‑byte output (no slicing beyond the HMAC‑SHA256 digest size): + +``` +scalar_c = int(raw) mod n +if scalar_c == 0: scalar_c = 1 +``` + +Mapping: +- c=0 → tag scalar → 32‑byte hex → UnsignedNote.tag (64‑hex) +- c=1 → amount attribute blinding factor (hex) +- c=2 → script attribute blinding factor (hex) + +## 3. Keyset ID to Integer (v1 only) + +As in [NUT-13][13], convert the 16-hex-character keyset id (8 bytes) to a big‑endian integer and reduce mod 2^31−1 to fit a hardened BIP32 child index: + +Python: +```python +keyset_id_int = int.from_bytes(bytes.fromhex(keyset_id_hex), "big") % (2**31 - 1) +``` +JavaScript: +```javascript +keysetIdInt = BigInt(`0x${keysetIdHex}`) % BigInt(2 ** 31 - 1); +``` + +Note: This integer mapping applies only to v1 keysets (8‑byte IDs). v2 uses the raw 33‑byte keyset id in the KDF (see §2.2) and does not require integer conversion. + +## 4. Counters + +- Maintain a per‑keyset counter starting at 0 when the keyset is first used. +- Increment the counter by 1 for each UnsignedNote you generate for that keyset (including decoy zero‑amount Outputs used to satisfy 2‑output policy in AC‑03/04/05). +- Store the latest counter in the wallet database; counters are independent per keyset. + +## 5. Deriving Values + +For each (keyset_id, counter): +- Derive private key material K_c from the path in §2 for each c ∈ {0,1,2}. +- Interpret K_c as a 32‑byte big‑endian scalar modulo the Secp256k1 curve order n. BIP32 hardened derivations ensure K_c ∈ [1, n−1]. + +Mapping to KVAC fields: +- tag := K_0 as 32‑byte array. This then becomes UnsignedNote.tag (64‑hex). +- amount_blindness := K_1 as 32‑byte array; pass as the optional blinding factor when constructing AmountAttribute. +- script_blindness := K_2 as 32‑byte array; pass as the optional blinding factor when constructing ScriptAttribute. + +Constructing attributes (example): +```rust +let amount_attr = cashu_kvac::models::AmountAttribute::new(amount, Some(&amount_blindness)); +let script_attr = cashu_kvac::models::ScriptAttribute::new(script_bytes, Some(&script_blindness)); +let tag = tag_hex; // 64-hex +``` + +## 6. Constraints and Interop Rules + +- Deterministic derivation is OPTIONAL; when used, it **MUST** follow this path to remain interoperable across wallets. +- Wallets **MUST NOT** reuse the same (keyset_id, counter) across multiple Outputs; reuse leads to tag reuse and linkability. +- For multi‑Output requests (AC‑03/04/05 require 2 outputs), allocate two consecutive counters and derive two separate triple‑sets. +- Decoy zero‑amount Outputs/Inputs **MUST** consume counters like normal Outputs. + + +[09]: ../09.md +[13]: ../13.md +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md +[AC-06]: AC06.md