From 47ca6f5756336704b7c4ee9bebd457065fec9662 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 11 Aug 2025 14:50:54 +0200 Subject: [PATCH 01/34] AC initial --- .../anonymous-credentials/AC00.md | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 protocol-extensions/anonymous-credentials/AC00.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md new file mode 100644 index 00000000..199e9101 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -0,0 +1,108 @@ +# Introduction and Core Models + +`mandatory` + +--- + +This document details the notation and models used throughout the specification. [Cashu-kvac]() is herein referenced as a black-box implementation of the cryptographic operations for Anonymous Credentials, so this specification **WON'T** delve into the mathematic details of the KVAC scheme. + +## Terminology Differences + +Here are some differences in terminology with respect to the main specifications: + +* *proof* $\rightarrow$ *note*: *proof* assumes a different meaning in this context (Zero Knowledge Proofs) so in order to avoid equivocation, the term *note* will be used in its place. +* *Mint* is written with a capital `M`, avoiding equivocation with the verb *to mint*. + +## Mint Keys + +### Private Key +The Mint's *signing* private key, which will be used to issue notes: +```rust +let scalars = (0..6).map(|_| cashu_kvac::Scalar::random()).collect(); +let mint_privkey = cashu_kvac::MintPrivateKey::from_scalars(&scalars).unwrap(); +``` + +### Public Key +The Mint's *signing* Public Key, which can be used to verify notes client-side: +```rust +let mint_pubkey: cashu_kvac::MintPublicKey = mint_privkey.public_key.clone(); +``` + +## Unsigned Note +An `UnsignedNote` is a note yet to be signed by the Mint: +```json +{ + "keyset_id": string, + "amount": number, + "script": string, + "unit": string, + "tag": 64-char-hex, + "attributes": [cashu_kvac::AmountAttribute, cashu_kvac::ScriptAttribute], +} +``` + +where: +* `keyset_id` is the ID of the keyset that will sign this note +* `amount` is the amount this note represents +* (optional) `script` is a [NUT-10](10.md) spending condition +* `unit` is the unit of the note (e.g. "sat", "msat", "usd") +* `tag` is a unique identifier for this note, chosen by the client. +* `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. + +## Output +`Output` is -as the name suggests- an output of a transaction. It is derived from an `UnsignedNote` as the following: +```json +pub struct KvacCoinMessage { + "id": string, + "tag": 64-char-hex, + "commitments": [66-char-hex, 66-char-hex], +} +``` + +where: +* `id` is `unsigned_note.keyset_id` +* `tag` is `unsigned_note.tag` +* `commitments` is a tuple containing the commiments from the original attributes: + * `unsigned_note.attributes.0.commitment()` + * `unsigned_note.attributes.1.commitment()`. + +## MAC +A `MAC` (Message Authentication Code) is the signature the Mint can issue on `Output`, using `MintPrivateKey`: + +```rust +let mac = MAC::generate( + &mint_privkey, + &output.commitments.0, + Some(&output.commitments.1), + Some(output.tag), +) +``` + +## Note + +A `Note` (or *signed note*) is a note that has received a `MAC` from the Mint. + +```json +{ + "keyset_id": string, + "amount": number, + "script": string, + "unit": string, + "tag": 64-char-hex, + "mac": 66-char-hex, + "attributes": [cashu_kvac::AmountAttribute, cashu_kvac::ScriptAttribute], + "issuance_proof": cashu_kvac::ZKP +} +``` +where: +* `keyset_id`, `amount`, `script`, `unit` `attributes` and `tag` are the same as in `UnsignedNote`. +* `mac` is the `MAC` public point value returned by the Mint +* `issuance_proof` is a Zero-Knowledge Proof returned by the Mint that confirms `MAC` was issued using `MintPublicKey`. + +## Input + +`Input` is an input to a transaction. It is derived from `Note` as the following: + +```json + +``` \ No newline at end of file From 198864de8593deb16cccb1ad53170985530bd4fd Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 11 Aug 2025 23:14:20 +0200 Subject: [PATCH 02/34] fix --- protocol-extensions/anonymous-credentials/AC00.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 199e9101..3fe48916 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -52,7 +52,7 @@ where: ## Output `Output` is -as the name suggests- an output of a transaction. It is derived from an `UnsignedNote` as the following: ```json -pub struct KvacCoinMessage { +{ "id": string, "tag": 64-char-hex, "commitments": [66-char-hex, 66-char-hex], @@ -96,7 +96,7 @@ A `Note` (or *signed note*) is a note that has received a `MAC` from the Mint. ``` where: * `keyset_id`, `amount`, `script`, `unit` `attributes` and `tag` are the same as in `UnsignedNote`. -* `mac` is the `MAC` public point value returned by the Mint +* `mac` is the signature value returned by the Mint * `issuance_proof` is a Zero-Knowledge Proof returned by the Mint that confirms `MAC` was issued using `MintPublicKey`. ## Input @@ -104,5 +104,7 @@ where: `Input` is an input to a transaction. It is derived from `Note` as the following: ```json - +{ + "" +} ``` \ No newline at end of file From e5ad221f31ed57f6b955b46aa06d01adebb40a5d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 12 Aug 2025 13:02:49 +0200 Subject: [PATCH 03/34] identifiers --- .../anonymous-credentials/AC00.md | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 3fe48916..b272ab3d 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -44,7 +44,7 @@ An `UnsignedNote` is a note yet to be signed by the Mint: where: * `keyset_id` is the ID of the keyset that will sign this note * `amount` is the amount this note represents -* (optional) `script` is a [NUT-10](10.md) spending condition +* (optional) `script` is a [NUT-10](10) spending condition * `unit` is the unit of the note (e.g. "sat", "msat", "usd") * `tag` is a unique identifier for this note, chosen by the client. * `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. @@ -53,16 +53,16 @@ where: `Output` is -as the name suggests- an output of a transaction. It is derived from an `UnsignedNote` as the following: ```json { - "id": string, - "tag": 64-char-hex, - "commitments": [66-char-hex, 66-char-hex], + "id": , + "tag": <64-char-hex>, + "commitments": <[66-char-hex, 66-char-hex]>, } ``` where: * `id` is `unsigned_note.keyset_id` * `tag` is `unsigned_note.tag` -* `commitments` is a tuple containing the commiments from the original attributes: +* `commitments` is a tuple containing the commiments from the original attributes, namely: * `unsigned_note.attributes.0.commitment()` * `unsigned_note.attributes.1.commitment()`. @@ -84,14 +84,14 @@ A `Note` (or *signed note*) is a note that has received a `MAC` from the Mint. ```json { - "keyset_id": string, - "amount": number, - "script": string, - "unit": string, - "tag": 64-char-hex, - "mac": 66-char-hex, - "attributes": [cashu_kvac::AmountAttribute, cashu_kvac::ScriptAttribute], - "issuance_proof": cashu_kvac::ZKP + "keyset_id": , + "amount": , + "script": , + "unit": , + "tag": <64-char-hex>, + "mac": <66-char-hex>, + "attributes": [, ], + "issuance_proof": } ``` where: @@ -105,6 +105,37 @@ where: ```json { - "" + "id": , + "script": , + "randomized_commitments": , + "witness": } -``` \ No newline at end of file +``` + +where: +* `id` is `note.keyset_id`. +* (optional) `script` is the serialized [NUT-10](10) spending condition associated with this note (`note.script`). +* (optional) `witness` is the serialized [NUT-10](10) witness that unlocks the specific spending condition in `script`. +* `randomized_commitments` are derived from `note.tag`, `note.mac` and `note.attributes` as the following: +```rust + let randomized_commitments = cashu_kvac::RandomizedCommitments::from_attributes_and_mac( + ¬e.attributes.0, + Some(¬e.attributes.1), + note.tag, + note.mac, + true); +``` + +## Identifiers + +### Mint + +The Mint identifies transaction `Input`s by their `Cv` randomized commitment (`input.randomized_commitments.Cv`). + +The Mint identifies transaction `Output`s by their `tag` (`output.tag`) + +### Clients + +Clients can identify both `Note` and `UnsignedNote` by their `tag` (`{unsigned_note|note}.tag`). + +[10]: 10.md \ No newline at end of file From 58a4a26839a578db32dfd0c5549c8a46fb37d11a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 12 Aug 2025 15:50:20 +0200 Subject: [PATCH 04/34] keys --- .../anonymous-credentials/AC00.md | 53 ++++++++- .../anonymous-credentials/AC01.md | 112 ++++++++++++++++++ 2 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 protocol-extensions/anonymous-credentials/AC01.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index b272ab3d..b1a576f4 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -1,4 +1,4 @@ -# Introduction and Core Models +# AC-00: Introduction and Core Models `mandatory` @@ -11,25 +11,32 @@ This document details the notation and models used throughout the specification. Here are some differences in terminology with respect to the main specifications: * *proof* $\rightarrow$ *note*: *proof* assumes a different meaning in this context (Zero Knowledge Proofs) so in order to avoid equivocation, the term *note* will be used in its place. + * *Mint* is written with a capital `M`, avoiding equivocation with the verb *to mint*. +* At times, `cashu_kvac::{Struct}` will be used inside JSON code-blocks. This is to abstract away structured types that are fundamental components of the cryptogaphic scheme, but of no interest for the scope of this specification. + ## Mint Keys ### Private Key -The Mint's *signing* private key, which will be used to issue notes: +The Mint's *signing* private key (`MintPrivateKey`), which will be used to issue notes: + +> [!NOTE] +> `Scalar::random()` is not a deterministic. In practice, use a deterministic derivation to generate secrets. + ```rust let scalars = (0..6).map(|_| cashu_kvac::Scalar::random()).collect(); let mint_privkey = cashu_kvac::MintPrivateKey::from_scalars(&scalars).unwrap(); ``` ### Public Key -The Mint's *signing* Public Key, which can be used to verify notes client-side: +The Mint's *signing* public key (`MintPublicKey`), which can be used to verify notes client-side: ```rust let mint_pubkey: cashu_kvac::MintPublicKey = mint_privkey.public_key.clone(); ``` ## Unsigned Note -An `UnsignedNote` is a note yet to be signed by the Mint: +An `UnsignedNote` is a note created by the client and yet to be signed by the Mint: ```json { "keyset_id": string, @@ -47,7 +54,40 @@ where: * (optional) `script` is a [NUT-10](10) spending condition * `unit` is the unit of the note (e.g. "sat", "msat", "usd") * `tag` is a unique identifier for this note, chosen by the client. -* `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. +* `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. These can be created by the client with: +```rust +let blinding_factor_amount = "deadbeefdeadbeefdeadbeefdeadbeef"; +let blinding_factor_script = "baadc0debaadc0debaadc0debaadc0de"; + +let amount_attr = cashu_kvac::AmountAttribute::new(1337, Some(blinding_factor_amount)); + +/* No spending conditions */ +let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_script)); + +/* Alternatively, NUT-11 spending conditions */ +let script = r#"[ + "P2PK", + { + "nonce": "da62796403af76c80cd6ce9153ed3746", + "data": "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", + "tags": [ + ["sigflag", "SIG_ALL"], + ["n_sigs", "2"], + ["locktime", "1689418329"], + [ + "refund", + "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e" + ], + [ + "pubkeys", + "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54" + ] + ] + } +]"#; +let script_attr = cashu_kvac::ScriptAttribute::new(script.to_string().as_bytes(), Some(blinding_factor_script)); +``` ## Output `Output` is -as the name suggests- an output of a transaction. It is derived from an `UnsignedNote` as the following: @@ -138,4 +178,5 @@ The Mint identifies transaction `Output`s by their `tag` (`output.tag`) Clients can identify both `Note` and `UnsignedNote` by their `tag` (`{unsigned_note|note}.tag`). -[10]: 10.md \ No newline at end of file +[10]: 10.md +[13]: 13.md \ No newline at end of file diff --git a/protocol-extensions/anonymous-credentials/AC01.md b/protocol-extensions/anonymous-credentials/AC01.md new file mode 100644 index 00000000..c1ece18a --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -0,0 +1,112 @@ +# AC-01: Mint public key exchange + +`mandatory` + +--- + +This document outlines the exchange of the public keys of the Mint with the wallet user. + +## Description + +Wallet user receives public keys from Mint via `GET /v1/kvac/keys`. + +The Mint responds only with its `active` keysets. Keyset are `active` if the mint will sign outputs with it. The mint will accept tokens from inactive keysets as inputs but will not sign with them for new outputs. The `active` keysets can change over time, for example due to key rotation. A list of all keysets, active and inactive, can be requested separately (see [AC-02](AC-02)). + +A wallet can ask for the keys of a specific (active or inactive) keyset via the endpoint `GET /v1/kvac/keys/{keyset_id}` (see [AC-02][02]). + +## Supported Currency Units + +Refer to [NUT-01](01), as there are no changes from the main spec. + +## Keyset generation + +Keysets are generated by the Mint. Each keyset contains exactly $1$ key, which is the `MintPublicKey` generated from `MintPrivateKey` as described in [AC-00](AC-00). +`MintPublicKey` is internally composed by $2$ public keys: +```json +{ + "Cw": "0312..3f", + "I": "0213..10" +} +``` +Therefore, its keyset `id` is computed from both of these values (see [AC-02](AC-02) and [NUT-02](02)). The mint **MUST** use the [compressed Secp256k1 public key format](https://learnmeabitcoin.com/technical/public-key#public-key-format) to represent its public keys. + + +## Network Requests + +A client can request: + +```http +GET https://mint.host:3338/v1/kvac/keys +``` + +With curl: + +```bash +curl -X GET https://mint.host:3338/v1/kvac/keys +``` + +Response `GetKeysResponse` of the Mint: + +```json +{ + "keysets": [ + { + "id": , + "unit": , + "final_expiry": , + "keys": { + "Cw": , + "I": , + } + }, + ... + ] +} +``` + +Where: +* `id` is the keyset ID, recomputable by the client (see [AC-02](AC-02) and [NUT-02](02)). +* `unit` is the unit of the keyset. +* (optional) `final_expiry` is the unix epoch after which the keyset might be permanently deleted from the Mint's database (and thus any note issued from that keyset is invalidated). +* `keys` holds the decomposition of the `MintPublicKey` as explained above. + +--- + +A client can also request: + +```http +GET https://mint.host:3338/v1/kvac/keys/{keyset_id_hex_str} +``` + +With curl: + +```bash +curl -X GET https://mint.host:3338/v1/kvac/keys/{keyset_id_hex_str} +``` + +In this case, the Mint will return the keyset -if it exists- regardless of its `active` status: + +```json +{ + "keysets": [ + { + "id": , + "unit": , + "final_expiry": , + "keys": { + "Cw": , + "I": , + } + } + ] +} +``` + + + + + +[01]: 01.md +[02]: 02.md +[AC-02]: AC02.md +[AC-00]: AC00.md \ No newline at end of file From 3f78e34611f4b45e1f485dca0fd2394415e099ac Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 12 Aug 2025 16:45:34 +0200 Subject: [PATCH 05/34] remove unnecessary example --- .../anonymous-credentials/AC00.md | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index b1a576f4..c7529242 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -64,28 +64,6 @@ let amount_attr = cashu_kvac::AmountAttribute::new(1337, Some(blinding_factor_am /* No spending conditions */ let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_script)); -/* Alternatively, NUT-11 spending conditions */ -let script = r#"[ - "P2PK", - { - "nonce": "da62796403af76c80cd6ce9153ed3746", - "data": "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", - "tags": [ - ["sigflag", "SIG_ALL"], - ["n_sigs", "2"], - ["locktime", "1689418329"], - [ - "refund", - "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e" - ], - [ - "pubkeys", - "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", - "023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54" - ] - ] - } -]"#; let script_attr = cashu_kvac::ScriptAttribute::new(script.to_string().as_bytes(), Some(blinding_factor_script)); ``` From 4fc1d0d84b74edf0cce8ac532027badfbf42878e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 12 Aug 2025 16:46:26 +0200 Subject: [PATCH 06/34] fix --- protocol-extensions/anonymous-credentials/AC00.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index c7529242..c90f5de0 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -63,8 +63,6 @@ let amount_attr = cashu_kvac::AmountAttribute::new(1337, Some(blinding_factor_am /* No spending conditions */ let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_script)); - -let script_attr = cashu_kvac::ScriptAttribute::new(script.to_string().as_bytes(), Some(blinding_factor_script)); ``` ## Output From 2ecf06ea5c09cd25c052007f3e0ac2bfdd94b2f4 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 12:01:19 +0200 Subject: [PATCH 07/34] keysets + corrections --- .../anonymous-credentials/AC00.md | 16 +++-- .../anonymous-credentials/AC01.md | 2 + .../anonymous-credentials/AC02.md | 68 +++++++++++++++++++ 3 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 protocol-extensions/anonymous-credentials/AC02.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index c90f5de0..a79ed718 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -4,7 +4,7 @@ --- -This document details the notation and models used throughout the specification. [Cashu-kvac]() is herein referenced as a black-box implementation of the cryptographic operations for Anonymous Credentials, so this specification **WON'T** delve into the mathematic details of the KVAC scheme. +This document details the notation and models used throughout the specification. [cashu-kvac][KVAC] is herein referenced as a black-box implementation of the cryptographic operations for Anonymous Credentials, so this specification **WON'T** delve into the mathematic details of the KVAC scheme. ## Terminology Differences @@ -49,7 +49,7 @@ An `UnsignedNote` is a note created by the client and yet to be signed by the Mi ``` where: -* `keyset_id` is the ID of the keyset that will sign this note +* `keyset_id` is the ID of the keyset that will sign this note (see [AC-02][AC-02]) * `amount` is the amount this note represents * (optional) `script` is a [NUT-10](10) spending condition * `unit` is the unit of the note (e.g. "sat", "msat", "usd") @@ -66,7 +66,7 @@ let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_scr ``` ## Output -`Output` is -as the name suggests- an output of a transaction. It is derived from an `UnsignedNote` as the following: +`Output` is -as the name suggests- an output of a transaction and it holds all of the information a Mint is allowed to see of a `UnsignedNote`: ```json { "id": , @@ -117,7 +117,7 @@ where: ## Input -`Input` is an input to a transaction. It is derived from `Note` as the following: +`Input` is an input to a transaction and holds all of the information a Mint is allowed to see of a `Note`. It's derived from `Note` as the following: ```json { @@ -130,8 +130,8 @@ where: where: * `id` is `note.keyset_id`. -* (optional) `script` is the serialized [NUT-10](10) spending condition associated with this note (`note.script`). -* (optional) `witness` is the serialized [NUT-10](10) witness that unlocks the specific spending condition in `script`. +* (optional) `script` is the serialized [NUT-10][10] spending condition associated with this note (`note.script`). +* (optional) `witness` is the serialized [NUT-10][10] witness that unlocks the specific spending condition in `script`. * `randomized_commitments` are derived from `note.tag`, `note.mac` and `note.attributes` as the following: ```rust let randomized_commitments = cashu_kvac::RandomizedCommitments::from_attributes_and_mac( @@ -154,5 +154,7 @@ The Mint identifies transaction `Output`s by their `tag` (`output.tag`) Clients can identify both `Note` and `UnsignedNote` by their `tag` (`{unsigned_note|note}.tag`). +[KVAC]: https://github.com/lollerfirst/cashu-kvac.git [10]: 10.md -[13]: 13.md \ No newline at end of file +[13]: 13.md +[AC-02]: AC02.md \ No newline at end of file diff --git a/protocol-extensions/anonymous-credentials/AC01.md b/protocol-extensions/anonymous-credentials/AC01.md index c1ece18a..04e83c55 100644 --- a/protocol-extensions/anonymous-credentials/AC01.md +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -2,6 +2,8 @@ `mandatory` +`depends on: NUT-01` + --- This document outlines the exchange of the public keys of the Mint with the wallet user. diff --git a/protocol-extensions/anonymous-credentials/AC02.md b/protocol-extensions/anonymous-credentials/AC02.md new file mode 100644 index 00000000..caeb5b7a --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -0,0 +1,68 @@ +# AC-02: Keysets and fees + +`mandatory` + +`depends on: NUT-02` + +--- + +KVAC keysets largely relies on the main specification in [NUT-02][02]. This document only reports the difference in network endpoints for *getting* keysets specific to this extension of the protocol. + +## Keyset Properties + +### Keyset ID + +A keyset id is an identifier for a specific keyset. It can be derived by anyone who knows the set of public keys of a mint. Wallets CAN compute the keyset id for a given keyset by themselves to confirm that the mint is supplying the correct keyset ID. + +The keyset id is in each `Note` so it can be used by wallets to identify which Mint and keyset it was generated from. The keyset field id is also present in the `Input`s and `Output`s sent and received to/from the Mint (see [AC-00][AC-00]). + +### Active keysets + +Refer to [NUT-02][02]. + +### Fees + +Refer to [NUT-02][02]. + +### Deriving the keyset ID + +Refer to [NUT-02][02]. + +## Network Requests + +A wallet can ask the mint for a list of all keysets via the `GET /v1/kvac/keysets` endpoint. + +Request: + +```http +GET https://mint.host:3338/v1/kvac/keysets +``` + +With curl: + +```bash +curl -X GET https://mint.host:3338/v1/kvac/keysets +``` + +Response `GetKeysetsResponse` of `Bob`: + +```json +{ + "keysets": [ + { + "id": , + "unit": , + "active": , + "final_expiry": , + "input_fee_ppk": , + }, + ... + ] +} +``` + +Here, `id` is the keyset ID, `unit` is the unit string (e.g. "sat") of the keyset, `active` indicates whether new ecash can be minted with this keyset, and `input_fee_ppk` is the fee (per thousand units) to spend one input spent from this keyset. If `input_fee_ppk` is not given, we assume it to be `0`. + + +[AC-00]: AC00.md +[02]: 02.md \ No newline at end of file From 09bd7661687c8012cd09cac82364be215ca9ee49 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 12:02:56 +0200 Subject: [PATCH 08/34] prettier --- .../anonymous-credentials/AC00.md | 65 ++++++++++++------- .../anonymous-credentials/AC01.md | 22 +++---- .../anonymous-credentials/AC02.md | 5 +- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index a79ed718..9b3f9da7 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -10,16 +10,17 @@ This document details the notation and models used throughout the specification. Here are some differences in terminology with respect to the main specifications: -* *proof* $\rightarrow$ *note*: *proof* assumes a different meaning in this context (Zero Knowledge Proofs) so in order to avoid equivocation, the term *note* will be used in its place. +- _proof_ $\rightarrow$ _note_: _proof_ assumes a different meaning in this context (Zero Knowledge Proofs) so in order to avoid equivocation, the term _note_ will be used in its place. -* *Mint* is written with a capital `M`, avoiding equivocation with the verb *to mint*. +- _Mint_ is written with a capital `M`, avoiding equivocation with the verb _to mint_. -* At times, `cashu_kvac::{Struct}` will be used inside JSON code-blocks. This is to abstract away structured types that are fundamental components of the cryptogaphic scheme, but of no interest for the scope of this specification. +- At times, `cashu_kvac::{Struct}` will be used inside JSON code-blocks. This is to abstract away structured types that are fundamental components of the cryptogaphic scheme, but of no interest for the scope of this specification. ## Mint Keys ### Private Key -The Mint's *signing* private key (`MintPrivateKey`), which will be used to issue notes: + +The Mint's _signing_ private key (`MintPrivateKey`), which will be used to issue notes: > [!NOTE] > `Scalar::random()` is not a deterministic. In practice, use a deterministic derivation to generate secrets. @@ -30,13 +31,17 @@ let mint_privkey = cashu_kvac::MintPrivateKey::from_scalars(&scalars).unwrap(); ``` ### Public Key -The Mint's *signing* public key (`MintPublicKey`), which can be used to verify notes client-side: + +The Mint's _signing_ public key (`MintPublicKey`), which can be used to verify notes client-side: + ```rust let mint_pubkey: cashu_kvac::MintPublicKey = mint_privkey.public_key.clone(); ``` ## Unsigned Note + An `UnsignedNote` is a note created by the client and yet to be signed by the Mint: + ```json { "keyset_id": string, @@ -49,12 +54,14 @@ An `UnsignedNote` is a note created by the client and yet to be signed by the Mi ``` where: -* `keyset_id` is the ID of the keyset that will sign this note (see [AC-02][AC-02]) -* `amount` is the amount this note represents -* (optional) `script` is a [NUT-10](10) spending condition -* `unit` is the unit of the note (e.g. "sat", "msat", "usd") -* `tag` is a unique identifier for this note, chosen by the client. -* `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. These can be created by the client with: + +- `keyset_id` is the ID of the keyset that will sign this note (see [AC-02][AC-02]) +- `amount` is the amount this note represents +- (optional) `script` is a [NUT-10](10) spending condition +- `unit` is the unit of the note (e.g. "sat", "msat", "usd") +- `tag` is a unique identifier for this note, chosen by the client. +- `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. These can be created by the client with: + ```rust let blinding_factor_amount = "deadbeefdeadbeefdeadbeefdeadbeef"; let blinding_factor_script = "baadc0debaadc0debaadc0debaadc0de"; @@ -66,7 +73,9 @@ let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_scr ``` ## Output + `Output` is -as the name suggests- an output of a transaction and it holds all of the information a Mint is allowed to see of a `UnsignedNote`: + ```json { "id": , @@ -76,13 +85,15 @@ let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_scr ``` where: -* `id` is `unsigned_note.keyset_id` -* `tag` is `unsigned_note.tag` -* `commitments` is a tuple containing the commiments from the original attributes, namely: - * `unsigned_note.attributes.0.commitment()` - * `unsigned_note.attributes.1.commitment()`. + +- `id` is `unsigned_note.keyset_id` +- `tag` is `unsigned_note.tag` +- `commitments` is a tuple containing the commiments from the original attributes, namely: + - `unsigned_note.attributes.0.commitment()` + - `unsigned_note.attributes.1.commitment()`. ## MAC + A `MAC` (Message Authentication Code) is the signature the Mint can issue on `Output`, using `MintPrivateKey`: ```rust @@ -96,7 +107,7 @@ let mac = MAC::generate( ## Note -A `Note` (or *signed note*) is a note that has received a `MAC` from the Mint. +A `Note` (or _signed note_) is a note that has received a `MAC` from the Mint. ```json { @@ -110,10 +121,12 @@ A `Note` (or *signed note*) is a note that has received a `MAC` from the Mint. "issuance_proof": } ``` + where: -* `keyset_id`, `amount`, `script`, `unit` `attributes` and `tag` are the same as in `UnsignedNote`. -* `mac` is the signature value returned by the Mint -* `issuance_proof` is a Zero-Knowledge Proof returned by the Mint that confirms `MAC` was issued using `MintPublicKey`. + +- `keyset_id`, `amount`, `script`, `unit` `attributes` and `tag` are the same as in `UnsignedNote`. +- `mac` is the signature value returned by the Mint +- `issuance_proof` is a Zero-Knowledge Proof returned by the Mint that confirms `MAC` was issued using `MintPublicKey`. ## Input @@ -129,10 +142,12 @@ where: ``` where: -* `id` is `note.keyset_id`. -* (optional) `script` is the serialized [NUT-10][10] spending condition associated with this note (`note.script`). -* (optional) `witness` is the serialized [NUT-10][10] witness that unlocks the specific spending condition in `script`. -* `randomized_commitments` are derived from `note.tag`, `note.mac` and `note.attributes` as the following: + +- `id` is `note.keyset_id`. +- (optional) `script` is the serialized [NUT-10][10] spending condition associated with this note (`note.script`). +- (optional) `witness` is the serialized [NUT-10][10] witness that unlocks the specific spending condition in `script`. +- `randomized_commitments` are derived from `note.tag`, `note.mac` and `note.attributes` as the following: + ```rust let randomized_commitments = cashu_kvac::RandomizedCommitments::from_attributes_and_mac( ¬e.attributes.0, @@ -157,4 +172,4 @@ Clients can identify both `Note` and `UnsignedNote` by their `tag` (`{unsigned_n [KVAC]: https://github.com/lollerfirst/cashu-kvac.git [10]: 10.md [13]: 13.md -[AC-02]: AC02.md \ No newline at end of file +[AC-02]: AC02.md diff --git a/protocol-extensions/anonymous-credentials/AC01.md b/protocol-extensions/anonymous-credentials/AC01.md index 04e83c55..2ecdf85a 100644 --- a/protocol-extensions/anonymous-credentials/AC01.md +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -24,14 +24,15 @@ Refer to [NUT-01](01), as there are no changes from the main spec. Keysets are generated by the Mint. Each keyset contains exactly $1$ key, which is the `MintPublicKey` generated from `MintPrivateKey` as described in [AC-00](AC-00). `MintPublicKey` is internally composed by $2$ public keys: + ```json { - "Cw": "0312..3f", - "I": "0213..10" + "Cw": "0312..3f", + "I": "0213..10" } ``` -Therefore, its keyset `id` is computed from both of these values (see [AC-02](AC-02) and [NUT-02](02)). The mint **MUST** use the [compressed Secp256k1 public key format](https://learnmeabitcoin.com/technical/public-key#public-key-format) to represent its public keys. +Therefore, its keyset `id` is computed from both of these values (see [AC-02](AC-02) and [NUT-02](02)). The mint **MUST** use the [compressed Secp256k1 public key format](https://learnmeabitcoin.com/technical/public-key#public-key-format) to represent its public keys. ## Network Requests @@ -67,10 +68,11 @@ Response `GetKeysResponse` of the Mint: ``` Where: -* `id` is the keyset ID, recomputable by the client (see [AC-02](AC-02) and [NUT-02](02)). -* `unit` is the unit of the keyset. -* (optional) `final_expiry` is the unix epoch after which the keyset might be permanently deleted from the Mint's database (and thus any note issued from that keyset is invalidated). -* `keys` holds the decomposition of the `MintPublicKey` as explained above. + +- `id` is the keyset ID, recomputable by the client (see [AC-02](AC-02) and [NUT-02](02)). +- `unit` is the unit of the keyset. +- (optional) `final_expiry` is the unix epoch after which the keyset might be permanently deleted from the Mint's database (and thus any note issued from that keyset is invalidated). +- `keys` holds the decomposition of the `MintPublicKey` as explained above. --- @@ -104,11 +106,7 @@ In this case, the Mint will return the keyset -if it exists- regardless of its ` } ``` - - - - [01]: 01.md [02]: 02.md [AC-02]: AC02.md -[AC-00]: AC00.md \ No newline at end of file +[AC-00]: AC00.md diff --git a/protocol-extensions/anonymous-credentials/AC02.md b/protocol-extensions/anonymous-credentials/AC02.md index caeb5b7a..18487b72 100644 --- a/protocol-extensions/anonymous-credentials/AC02.md +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -6,7 +6,7 @@ --- -KVAC keysets largely relies on the main specification in [NUT-02][02]. This document only reports the difference in network endpoints for *getting* keysets specific to this extension of the protocol. +KVAC keysets largely relies on the main specification in [NUT-02][02]. This document only reports the difference in network endpoints for _getting_ keysets specific to this extension of the protocol. ## Keyset Properties @@ -63,6 +63,5 @@ Response `GetKeysetsResponse` of `Bob`: Here, `id` is the keyset ID, `unit` is the unit string (e.g. "sat") of the keyset, `active` indicates whether new ecash can be minted with this keyset, and `input_fee_ppk` is the fee (per thousand units) to spend one input spent from this keyset. If `input_fee_ppk` is not given, we assume it to be `0`. - [AC-00]: AC00.md -[02]: 02.md \ No newline at end of file +[02]: 02.md From ec35b316f2a985bb68ecad606d9e97673624ed3c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 15:46:03 +0200 Subject: [PATCH 09/34] swap request --- .../anonymous-credentials/AC00.md | 5 +- .../anonymous-credentials/AC01.md | 2 - .../anonymous-credentials/AC02.md | 2 - .../anonymous-credentials/AC03.md | 140 ++++++++++++++++++ 4 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 protocol-extensions/anonymous-credentials/AC03.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 9b3f9da7..0a57ed5b 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -59,7 +59,10 @@ where: - `amount` is the amount this note represents - (optional) `script` is a [NUT-10](10) spending condition - `unit` is the unit of the note (e.g. "sat", "msat", "usd") -- `tag` is a unique identifier for this note, chosen by the client. +- `tag` is a random unique identifier for this note, chosen by the client. +```rust +let tag = Scalar::random(); +``` - `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. These can be created by the client with: ```rust diff --git a/protocol-extensions/anonymous-credentials/AC01.md b/protocol-extensions/anonymous-credentials/AC01.md index 2ecdf85a..a44ee771 100644 --- a/protocol-extensions/anonymous-credentials/AC01.md +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -2,8 +2,6 @@ `mandatory` -`depends on: NUT-01` - --- This document outlines the exchange of the public keys of the Mint with the wallet user. diff --git a/protocol-extensions/anonymous-credentials/AC02.md b/protocol-extensions/anonymous-credentials/AC02.md index 18487b72..77a9772b 100644 --- a/protocol-extensions/anonymous-credentials/AC02.md +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -2,8 +2,6 @@ `mandatory` -`depends on: NUT-02` - --- KVAC keysets largely relies on the main specification in [NUT-02][02]. This document only reports the difference in network endpoints for _getting_ keysets specific to this extension of the protocol. diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md new file mode 100644 index 00000000..4147dfff --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -0,0 +1,140 @@ +# AC-03: Swap Transaction + +`mandatory` + +--- + +This document describes the core process by which a client can present previously signed `Note`s and get Mint signatures on `UnsignedNote`s. That is, if all conditions are upheld. + +## Use Cases + +### Swap To Send + +``` +sending user: Alice + +receiving user: Carol +``` + +Alice has a 64 sat `Note`. She wants to send Carol 40 sat, but does not have the necessary `Note`. For that, Alice requests a swap from the Mint and uses her `Note` plus a `Note` worth *zero* (see [AC-??][AC-??]) as `Input`s [0, 64] and asks for new `Output`s worth [40, 24] totalling 64 sat. The `Note`s that Alice sent to the Mint as `Input`s of the swap operation are now invalidated. + +[AC-??]: AC??.md + +### Swap To Receive + +``` +sending user: Alice + +receiving user: Carol +``` + +The second use case for the swap operation follows up on the example above, where Alice has swapped her `Note`s ready to be sent to Carol. Carol can receive the `Note` using the same operation: the received `Note` is used as `Input` and new `Output`s are requested from the Mint. +Remember that `Output`s are just `UnsignedNote`s with less information about them. +Once Carol has redeemed to new `Output`s, Alice can't double-spend the `Note` anymore and the transaction is settled, all the while the Mint is unaware it happened. + +## Process + +We describe the swap process following the case of the "Swap To Receive" example. Specifically, we assume the client possesses two `Note`s: one that encodes the current balance `b`; the other is received from another user of the same Mint and encodes an amount `a`. The client needs to "merge" these `Note`s into one to attain a new balance. + +### 1. Client generates `UnsignedNote`s and `Output`s + +The client selects an appropriate `keyset_id` amongst the active keysets and generates two `UnsignedNote`s (see [AC-00][AC-00]): +* one with for the amount `b+a`. +* one with the amount `0`. + +Then, for each `UnsignedNote`, they derive the corresponding `Output` as shown in [AC-00][AC-00]. + +### 2. Client derives `Input`s. + +For each of the two `Note`s, the client derives `Input`s as shown in [AC-00][AC-00]. + +### 3. Client generates Zero-Knowledge Proofs + +First, create a `cashu_kvac::transcript::CashuTranscript` instance. This will be used throughout the generation of the proofs. +```rust +let transcript = cashu_kvac::transcript::CashuTranscript::new(); +``` + +Then, generate the following proofs: +1. For each `Input` generate a `MacProof`. This proves the signature on that input. +```rust +let mac_proof = cashu_kvac::kvac::MacProof::create(&mint_publickey, ¬e.attributes.0, Some(¬e.attributes.1), note.tag, &input.randomized_commitments, &mut transcript); +``` +2. Generate one `RangeProof` for *all* `UnsignedNote`s. This proves that each `Output` is in range. +```rust +let amount_attributes: Vec = unsigned_notes.iter().map(|u| u.attributes.0.clone()).collect(); +let range_proof = cashu_kvac::kvac::RangeProof::create_bulletproof(&mut transcript, &amount_attributes); +``` +3. Generate a `BalanceProof`. This proves that inputs and outputs are balanced. +```rust +let balance_proof = cashu_kvac::kvac::BalanceProof::create(&inputs_amount_attributes, &outputs_amount_attributes, &mut transcript); +``` + +> [!NOTE] +> The order is important. If client/Mint prove/verify in a different order, the verification won't succeed. + + +### 4. Client sends a `SwapRequest` + +The client then performs a `POST v1/kvac/swap` request with the following `SwapRequest` payload: +```json +{ + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": , + "mac_proofs": [, ...], + "range_proof": , +} +``` + + +### 5. Mint receives and verifies `SwapRequest` + +Validation of a swap request works this way: + +0. Verify all `Input`s and `Output`s are of the same unit. + +1. Extract -if any- `script` and `witness` from each `Input`, and verify the spending conditions according to either [NUT-11][11] or [NUT-14][14]. + +2. Create a `CashuTranscript` instance. This will be used throughout the verification of the proofs: +```rust +let transcript = cashu_kvac::transcript::CashuTranscript::new(); +``` + +3. Verify each (`Input`, `MacProof`) pair, using the key from the `input.id` keyset: +```rust +cashu_kvac::kvac::MacProof::verify( + &mint_privkeys[input.id], + &input.randomized_commitments, + &input.script, // This will be None if the input does not provide a script + mac_proof, + &mut transcript, +); +``` + +4. Verify the `RangeProof` with the amount commitments in each `Output`: +```rust +let amount_commitments: Vec = outputs.iter().map(|o| o.0).collect(); +cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range_proof); +``` + +5. Verify the `BalanceProof`. Use `input.randomized_commitments` from each input, `output.commitments[0]` from each output and the supposed difference between their value, which in this case is $0$. + +```rust +cashu_kvac::kvac::BalanceProof::verify( + &inputs_randomized_commitments, + &outputs_amount_commitments, + 0 as i64, + balance_proof, + &mut transcript +); +``` + +> [!NOTE] +> If a keyset supports fees, the difference between inputs and outputs should be equal to the fee the Mint enforces on the inputs. + +### 6. Mint issues `MAC`s + +[AC-00]: AC00.md +[11]: 11.md +[14]: 14.md \ No newline at end of file From 63f797e446b02fed96c931720a496149908b466b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 15:56:45 +0200 Subject: [PATCH 10/34] fix output commitments --- protocol-extensions/anonymous-credentials/AC03.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 4147dfff..9a44f8e7 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -114,7 +114,7 @@ cashu_kvac::kvac::MacProof::verify( 4. Verify the `RangeProof` with the amount commitments in each `Output`: ```rust -let amount_commitments: Vec = outputs.iter().map(|o| o.0).collect(); +let amount_commitments: Vec = outputs.iter().map(|o| o.commitments.0).collect(); cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range_proof); ``` From b570992db8b4045fc623334427d1b3649414fb24 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 18:52:45 +0200 Subject: [PATCH 11/34] finalize swap transaction --- .../anonymous-credentials/AC03.md | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 9a44f8e7..4edf6eae 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -101,7 +101,7 @@ Validation of a swap request works this way: let transcript = cashu_kvac::transcript::CashuTranscript::new(); ``` -3. Verify each (`Input`, `MacProof`) pair, using the key from the `input.id` keyset: +3. Verify each (`Input`, `MacProof`) pair: if `input.randomized_commitments.nullifier()` was already presented before, reject the transaction. If not, then use the key from the `input.id` keyset to verify the `MacProof`: ```rust cashu_kvac::kvac::MacProof::verify( &mint_privkeys[input.id], @@ -133,7 +133,51 @@ cashu_kvac::kvac::BalanceProof::verify( > [!NOTE] > If a keyset supports fees, the difference between inputs and outputs should be equal to the fee the Mint enforces on the inputs. -### 6. Mint issues `MAC`s +### 6. Mint issues signatures + +If all of the previous verifications succeeded, the Mint issues a `mac` for each `output` in `outputs` using the key from the keyset with ID `output.id` (see [AC-00][AC-00]). + +The Mint also computes and returns a `IssuanceProof` for each issued `mac`: + +```rust +let issuance_proof = cashu_kvac::kvac::IssuanceProof::create( + &mint_privkeys[output.id], + output.tag, + mac, + output.commitments.0, + Some(output.commitments.1), +); +``` + +> [!NOTE] +> The Mint does not use the transcript to compute this proof. + +Finally, the response the the swap request `SwapResponse` is as follows: + +```json +{ + "outputs": [, ...], + "issued_macs": [, ...], + "issuance_proofs": [, ...] +} +``` + +### 7. Client verifies and assembles `Notes` + +In the final step, the client receives a `SwapResponse` and verifies each (`UnsignedNote`, `MAC`, `IssuanceProof`) pair: +```rust + cashu_kvac::kvac::IssuanceProof::verify( + &mint_pubkey, + unsigned_note.tag, + mac, + &unsigned_note.attributes.0, + Some(&unsigned_note.attributes.1), + issuance_proof.clone(), + ) +``` + +They can then proceed to create a full `Note` by combining `UnsignedNote` and the received `MAC` and `IssuanceProof`. + [AC-00]: AC00.md [11]: 11.md From 63964bb9d1db07166d67f127d0fdce14f9541f72 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 18:53:40 +0200 Subject: [PATCH 12/34] fix typo --- protocol-extensions/anonymous-credentials/AC03.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 4edf6eae..253acaad 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -164,7 +164,7 @@ Finally, the response the the swap request `SwapResponse` is as follows: ### 7. Client verifies and assembles `Notes` -In the final step, the client receives a `SwapResponse` and verifies each (`UnsignedNote`, `MAC`, `IssuanceProof`) pair: +In the final step, the client receives a `SwapResponse` and verifies each (`UnsignedNote`, `MAC`, `IssuanceProof`) tuple: ```rust cashu_kvac::kvac::IssuanceProof::verify( &mint_pubkey, From 9e950e993b7e4f64bedfb09e984afaa6632acb41 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Aug 2025 18:59:27 +0200 Subject: [PATCH 13/34] better readability. --- .../anonymous-credentials/AC03.md | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 253acaad..ea013db8 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -58,16 +58,33 @@ let transcript = cashu_kvac::transcript::CashuTranscript::new(); Then, generate the following proofs: 1. For each `Input` generate a `MacProof`. This proves the signature on that input. ```rust -let mac_proof = cashu_kvac::kvac::MacProof::create(&mint_publickey, ¬e.attributes.0, Some(¬e.attributes.1), note.tag, &input.randomized_commitments, &mut transcript); +let mac_proof = cashu_kvac::kvac::MacProof::create( + &mint_publickey, + ¬e.attributes.0, + Some(¬e.attributes.1), + note.tag, + &input.randomized_commitments, + &mut transcript +); ``` 2. Generate one `RangeProof` for *all* `UnsignedNote`s. This proves that each `Output` is in range. ```rust -let amount_attributes: Vec = unsigned_notes.iter().map(|u| u.attributes.0.clone()).collect(); -let range_proof = cashu_kvac::kvac::RangeProof::create_bulletproof(&mut transcript, &amount_attributes); +let amount_attributes: Vec = unsigned_notes + .iter() + .map(|u| u.attributes.0.clone()) + .collect(); +let range_proof = cashu_kvac::kvac::RangeProof::create_bulletproof( + &mut transcript, + &amount_attributes +); ``` 3. Generate a `BalanceProof`. This proves that inputs and outputs are balanced. ```rust -let balance_proof = cashu_kvac::kvac::BalanceProof::create(&inputs_amount_attributes, &outputs_amount_attributes, &mut transcript); +let balance_proof = cashu_kvac::kvac::BalanceProof::create( + &inputs_amount_attributes, + &outputs_amount_attributes, + &mut transcript +); ``` > [!NOTE] @@ -114,8 +131,15 @@ cashu_kvac::kvac::MacProof::verify( 4. Verify the `RangeProof` with the amount commitments in each `Output`: ```rust -let amount_commitments: Vec = outputs.iter().map(|o| o.commitments.0).collect(); -cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range_proof); +let amount_commitments: Vec = outputs + .iter() + .map(|o| o.commitments.0) + .collect(); +cashu_kvac::kvac::RangeProof::verify( + &mut transcript, + &amount_commitments, + range_proof +); ``` 5. Verify the `BalanceProof`. Use `input.randomized_commitments` from each input, `output.commitments[0]` from each output and the supposed difference between their value, which in this case is $0$. From 02e7ce77d58111ac63a74f28da25880877c44fad Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 18 Aug 2025 11:13:04 +0200 Subject: [PATCH 14/34] update --- protocol-extensions/anonymous-credentials/AC03.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index ea013db8..df2149b5 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -113,12 +113,14 @@ Validation of a swap request works this way: 1. Extract -if any- `script` and `witness` from each `Input`, and verify the spending conditions according to either [NUT-11][11] or [NUT-14][14]. -2. Create a `CashuTranscript` instance. This will be used throughout the verification of the proofs: +2. Verify each `Input` nullifier (`input.randomized_commitments.get_nullifier()`) was not already presented. + +3. Create a `CashuTranscript` instance. This will be used throughout the verification of the proofs: ```rust let transcript = cashu_kvac::transcript::CashuTranscript::new(); ``` -3. Verify each (`Input`, `MacProof`) pair: if `input.randomized_commitments.nullifier()` was already presented before, reject the transaction. If not, then use the key from the `input.id` keyset to verify the `MacProof`: +4. Verify each (`Input`, `MacProof`) pair. Use the key from the `input.id` keyset to verify the `MacProof`: ```rust cashu_kvac::kvac::MacProof::verify( &mint_privkeys[input.id], @@ -129,7 +131,7 @@ cashu_kvac::kvac::MacProof::verify( ); ``` -4. Verify the `RangeProof` with the amount commitments in each `Output`: +5. Verify the `RangeProof` with the amount commitments in each `Output`: ```rust let amount_commitments: Vec = outputs .iter() @@ -142,7 +144,7 @@ cashu_kvac::kvac::RangeProof::verify( ); ``` -5. Verify the `BalanceProof`. Use `input.randomized_commitments` from each input, `output.commitments[0]` from each output and the supposed difference between their value, which in this case is $0$. +6. Verify the `BalanceProof`. Use `input.randomized_commitments` from each input, `output.commitments[0]` from each output and the supposed difference between their value, which in this case is $0$. ```rust cashu_kvac::kvac::BalanceProof::verify( From 735459ab74969dc73580c1efd522cb1e34adb02b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 18 Aug 2025 12:51:31 +0200 Subject: [PATCH 15/34] add AC-04 for minting --- .../anonymous-credentials/AC00.md | 24 +-- .../anonymous-credentials/AC03.md | 2 +- .../anonymous-credentials/AC04.md | 167 ++++++++++++++++++ 3 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 protocol-extensions/anonymous-credentials/AC04.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 0a57ed5b..a26e7d22 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -26,8 +26,8 @@ The Mint's _signing_ private key (`MintPrivateKey`), which will be used to issue > `Scalar::random()` is not a deterministic. In practice, use a deterministic derivation to generate secrets. ```rust -let scalars = (0..6).map(|_| cashu_kvac::Scalar::random()).collect(); -let mint_privkey = cashu_kvac::MintPrivateKey::from_scalars(&scalars).unwrap(); +let scalars = (0..6).map(|_| cashu_kvac::secp::Scalar::random()).collect(); +let mint_privkey = cashu_kvac::models::MintPrivateKey::from_scalars(&scalars).unwrap(); ``` ### Public Key @@ -35,7 +35,7 @@ let mint_privkey = cashu_kvac::MintPrivateKey::from_scalars(&scalars).unwrap(); The Mint's _signing_ public key (`MintPublicKey`), which can be used to verify notes client-side: ```rust -let mint_pubkey: cashu_kvac::MintPublicKey = mint_privkey.public_key.clone(); +let mint_pubkey: cashu_kvac::models::MintPublicKey = mint_privkey.public_key.clone(); ``` ## Unsigned Note @@ -44,12 +44,12 @@ An `UnsignedNote` is a note created by the client and yet to be signed by the Mi ```json { - "keyset_id": string, - "amount": number, - "script": string, - "unit": string, - "tag": 64-char-hex, - "attributes": [cashu_kvac::AmountAttribute, cashu_kvac::ScriptAttribute], + "keyset_id": , + "amount": , + "script": , + "unit": , + "tag": <64-char-hex>, + "attributes": [, ], } ``` @@ -69,10 +69,10 @@ let tag = Scalar::random(); let blinding_factor_amount = "deadbeefdeadbeefdeadbeefdeadbeef"; let blinding_factor_script = "baadc0debaadc0debaadc0debaadc0de"; -let amount_attr = cashu_kvac::AmountAttribute::new(1337, Some(blinding_factor_amount)); +let amount_attr = cashu_kvac::models::AmountAttribute::new(1337, Some(blinding_factor_amount)); /* No spending conditions */ -let script_attr = cashu_kvac::ScriptAttribute::new(b"", Some(blinding_factor_script)); +let script_attr = cashu_kvac::models::ScriptAttribute::new(b"", Some(blinding_factor_script)); ``` ## Output @@ -152,7 +152,7 @@ where: - `randomized_commitments` are derived from `note.tag`, `note.mac` and `note.attributes` as the following: ```rust - let randomized_commitments = cashu_kvac::RandomizedCommitments::from_attributes_and_mac( + let randomized_commitments = cashu_kvac::models::RandomizedCommitments::from_attributes_and_mac( ¬e.attributes.0, Some(¬e.attributes.1), note.tag, diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index df2149b5..0c1ce6d1 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -78,7 +78,7 @@ let range_proof = cashu_kvac::kvac::RangeProof::create_bulletproof( &amount_attributes ); ``` -3. Generate a `BalanceProof`. This proves that inputs and outputs are balanced. +3. Generate a `BalanceProof`. This proves that inputs and outputs are balanced when accounting for the input fees: `inputs - outputs == inputFees`. ```rust let balance_proof = cashu_kvac::kvac::BalanceProof::create( &inputs_amount_attributes, diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md new file mode 100644 index 00000000..23341a58 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -0,0 +1,167 @@ +# AC-04: Mint Transaction + +`mandatory` + +--- + +Minting notes is a two-step process: requesting a mint quote and minting new notes. This document describes the general flow that applies to all payment methods, with specifics for each supported payment method provided in dedicated NUTs. + +## Supported methods + +Method-specific NUTs describe how to handle different payment methods. The currently specified models are: + +- [NUT-23][23] for bolt11 Lightning invoices + +## General Flow + +The minting process follows these steps for all payment methods: + +1. The wallet requests a mint quote for the `unit` to mint, specifying the payment `method`. +2. The mint responds with a quote that includes a `quote` id and a payment `request`. +3. The user pays the `request` using the specified payment method. +4. The wallet then requests minting of new notes with the mint, including the `quote` id and new `outputs`. +5. The mint verifies payment and returns blind signatures. + +## Common Request and Response Formats + +### Requesting a Mint Quote + +To request a mint quote, a wallet makes a `POST /v1/mint/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). + +```http +POST https://mint.host:3338/v1/mint/quote/{method} +``` + +Depending on the payment method, the request structure may vary, but all methods will include at minimum: + +```json +{ + "unit": + // Additional method-specific fields may be required +} +``` + +The Mint responds with a quote that includes some common fields for all methods: + +```json +{ + "quote": , + "request": , + "unit": , + // Additional method-specific fields will be included +} +``` + +Where `quote` is the quote ID, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request. + +> [!CAUTION] +> +> `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the notes that this operation mints. To prevent this, use [NUT-20][20] locks to enforce public key authentication during minting. + +### Check Mint Quote State + +To check whether a mint quote has been paid, the wallet makes a `GET /v1/mint/quote/{method}/{quote_id}`. + +```http +GET https://mint.host:3338/v1/mint/quote/{method}/{quote_id} +``` + +The mint responds with the same structure as the initial quote response. + +### Executing a Mint Quote + +After requesting a mint quote and paying the request, the wallet proceeds with minting new notes by calling the `POST /v1/kvac/mint/{method}` endpoint. + +```http +POST https://mint.host:3338/v1/kvac/mint/{method} +``` + +The wallet sends a `MintRequest` with the following payload: + +```json +{ + "quote": , + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": , + "mac_proofs": [, ...], + "range_proof": , +} +``` + +where: +* `quote` is the quote ID from the previous step +* `inputs` are the same as in `swap_request.inputs` ([AC-03][AC-03]) +* `outputs` are the same as in `swap_request.outputs` ([AC-03][AC-03]) +* `balance_proof` proves that the difference `delta = inputs - outputs` is equal to **NEGATIVE** the amount in the previously generated quote. This -in English- means "the outputs encode exactly `quote.amount` more than the inputs". +* `mac_proofs` are the same as in `swap_request.mac_proofs` ([AC-03][AC-03]) +* `range_proof` is the same as in `swap_request.range_proof` ([AC-03][AC-03]) + +The mint then responds with `MintResponse`, which is exactly the same as `SwapResponse` ([AC-03][AC-03]): + +```json +{ + "outputs": [, ...], + "issued_macs": [, ...], + "issuance_proofs": [, ...] +} +``` + +The wallet can then follow step 7 in [AC-03][AC-03] to obtain `Note`s from `response.outputs`. + +## Adding New Payment Methods + +To add a new payment method (e.g., BOLT12), implement the following: + +1. Define the method-specific request and response structures following the pattern above +2. Implement the three required endpoints: quote request, quote check, and mint execution +3. Update the settings to include the new method + +## Settings + +The settings for this NUT indicate the supported method-unit pairs for minting. They are part of the info response of the mint ([NUT-06][06]) which reads: + +```json +{ + "4": { + "methods": [ + , + ... + ], + "disabled": + } +} +``` + +`MintMethodSetting` indicates supported `method` and `unit` pairs and additional settings of the mint. `disabled` indicates whether minting is disabled. + +`MintMethodSetting` is of the form: + +```json +{ + "method": , + "unit": , + "min_amount": , + "max_amount": , + "options": +} +``` + +`min_amount` and `max_amount` indicate the minimum and maximum amount for an operation of this method-unit pair. `options` are method-specific and can be defined in method-specific NUTs. + +[00]: 00.md +[01]: 01.md +[02]: 02.md +[03]: 03.md +[04]: 04.md +[05]: 05.md +[06]: 06.md +[07]: 07.md +[08]: 08.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[20]: 20.md +[23]: 23.md +[AC-03]: AC03.md From 11c870cdc8a2d92d2c482612add61e88b4eabf32 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 18 Aug 2025 12:53:38 +0200 Subject: [PATCH 16/34] prettier --- .../anonymous-credentials/AC00.md | 2 ++ .../anonymous-credentials/AC03.md | 34 ++++++++++++------- .../anonymous-credentials/AC04.md | 13 +++---- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index a26e7d22..b13a8a74 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -60,9 +60,11 @@ where: - (optional) `script` is a [NUT-10](10) spending condition - `unit` is the unit of the note (e.g. "sat", "msat", "usd") - `tag` is a random unique identifier for this note, chosen by the client. + ```rust let tag = Scalar::random(); ``` + - `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. These can be created by the client with: ```rust diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 0c1ce6d1..b90c11f6 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -16,7 +16,7 @@ sending user: Alice receiving user: Carol ``` -Alice has a 64 sat `Note`. She wants to send Carol 40 sat, but does not have the necessary `Note`. For that, Alice requests a swap from the Mint and uses her `Note` plus a `Note` worth *zero* (see [AC-??][AC-??]) as `Input`s [0, 64] and asks for new `Output`s worth [40, 24] totalling 64 sat. The `Note`s that Alice sent to the Mint as `Input`s of the swap operation are now invalidated. +Alice has a 64 sat `Note`. She wants to send Carol 40 sat, but does not have the necessary `Note`. For that, Alice requests a swap from the Mint and uses her `Note` plus a `Note` worth _zero_ (see [AC-??][AC-??]) as `Input`s [0, 64] and asks for new `Output`s worth [40, 24] totalling 64 sat. The `Note`s that Alice sent to the Mint as `Input`s of the swap operation are now invalidated. [AC-??]: AC??.md @@ -28,19 +28,20 @@ sending user: Alice receiving user: Carol ``` -The second use case for the swap operation follows up on the example above, where Alice has swapped her `Note`s ready to be sent to Carol. Carol can receive the `Note` using the same operation: the received `Note` is used as `Input` and new `Output`s are requested from the Mint. -Remember that `Output`s are just `UnsignedNote`s with less information about them. +The second use case for the swap operation follows up on the example above, where Alice has swapped her `Note`s ready to be sent to Carol. Carol can receive the `Note` using the same operation: the received `Note` is used as `Input` and new `Output`s are requested from the Mint. +Remember that `Output`s are just `UnsignedNote`s with less information about them. Once Carol has redeemed to new `Output`s, Alice can't double-spend the `Note` anymore and the transaction is settled, all the while the Mint is unaware it happened. ## Process -We describe the swap process following the case of the "Swap To Receive" example. Specifically, we assume the client possesses two `Note`s: one that encodes the current balance `b`; the other is received from another user of the same Mint and encodes an amount `a`. The client needs to "merge" these `Note`s into one to attain a new balance. +We describe the swap process following the case of the "Swap To Receive" example. Specifically, we assume the client possesses two `Note`s: one that encodes the current balance `b`; the other is received from another user of the same Mint and encodes an amount `a`. The client needs to "merge" these `Note`s into one to attain a new balance. ### 1. Client generates `UnsignedNote`s and `Output`s The client selects an appropriate `keyset_id` amongst the active keysets and generates two `UnsignedNote`s (see [AC-00][AC-00]): -* one with for the amount `b+a`. -* one with the amount `0`. + +- one with for the amount `b+a`. +- one with the amount `0`. Then, for each `UnsignedNote`, they derive the corresponding `Output` as shown in [AC-00][AC-00]. @@ -51,12 +52,15 @@ For each of the two `Note`s, the client derives `Input`s as shown in [AC-00][AC- ### 3. Client generates Zero-Knowledge Proofs First, create a `cashu_kvac::transcript::CashuTranscript` instance. This will be used throughout the generation of the proofs. + ```rust let transcript = cashu_kvac::transcript::CashuTranscript::new(); -``` +``` Then, generate the following proofs: + 1. For each `Input` generate a `MacProof`. This proves the signature on that input. + ```rust let mac_proof = cashu_kvac::kvac::MacProof::create( &mint_publickey, @@ -67,7 +71,9 @@ let mac_proof = cashu_kvac::kvac::MacProof::create( &mut transcript ); ``` -2. Generate one `RangeProof` for *all* `UnsignedNote`s. This proves that each `Output` is in range. + +2. Generate one `RangeProof` for _all_ `UnsignedNote`s. This proves that each `Output` is in range. + ```rust let amount_attributes: Vec = unsigned_notes .iter() @@ -78,7 +84,9 @@ let range_proof = cashu_kvac::kvac::RangeProof::create_bulletproof( &amount_attributes ); ``` + 3. Generate a `BalanceProof`. This proves that inputs and outputs are balanced when accounting for the input fees: `inputs - outputs == inputFees`. + ```rust let balance_proof = cashu_kvac::kvac::BalanceProof::create( &inputs_amount_attributes, @@ -90,10 +98,10 @@ let balance_proof = cashu_kvac::kvac::BalanceProof::create( > [!NOTE] > The order is important. If client/Mint prove/verify in a different order, the verification won't succeed. - ### 4. Client sends a `SwapRequest` The client then performs a `POST v1/kvac/swap` request with the following `SwapRequest` payload: + ```json { "inputs": [, ...], @@ -104,7 +112,6 @@ The client then performs a `POST v1/kvac/swap` request with the following `SwapR } ``` - ### 5. Mint receives and verifies `SwapRequest` Validation of a swap request works this way: @@ -116,11 +123,13 @@ Validation of a swap request works this way: 2. Verify each `Input` nullifier (`input.randomized_commitments.get_nullifier()`) was not already presented. 3. Create a `CashuTranscript` instance. This will be used throughout the verification of the proofs: + ```rust let transcript = cashu_kvac::transcript::CashuTranscript::new(); ``` 4. Verify each (`Input`, `MacProof`) pair. Use the key from the `input.id` keyset to verify the `MacProof`: + ```rust cashu_kvac::kvac::MacProof::verify( &mint_privkeys[input.id], @@ -132,6 +141,7 @@ cashu_kvac::kvac::MacProof::verify( ``` 5. Verify the `RangeProof` with the amount commitments in each `Output`: + ```rust let amount_commitments: Vec = outputs .iter() @@ -191,6 +201,7 @@ Finally, the response the the swap request `SwapResponse` is as follows: ### 7. Client verifies and assembles `Notes` In the final step, the client receives a `SwapResponse` and verifies each (`UnsignedNote`, `MAC`, `IssuanceProof`) tuple: + ```rust cashu_kvac::kvac::IssuanceProof::verify( &mint_pubkey, @@ -204,7 +215,6 @@ In the final step, the client receives a `SwapResponse` and verifies each (`Unsi They can then proceed to create a full `Note` by combining `UnsignedNote` and the received `MAC` and `IssuanceProof`. - [AC-00]: AC00.md [11]: 11.md -[14]: 14.md \ No newline at end of file +[14]: 14.md diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index 23341a58..8c19d32e 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -90,12 +90,13 @@ The wallet sends a `MintRequest` with the following payload: ``` where: -* `quote` is the quote ID from the previous step -* `inputs` are the same as in `swap_request.inputs` ([AC-03][AC-03]) -* `outputs` are the same as in `swap_request.outputs` ([AC-03][AC-03]) -* `balance_proof` proves that the difference `delta = inputs - outputs` is equal to **NEGATIVE** the amount in the previously generated quote. This -in English- means "the outputs encode exactly `quote.amount` more than the inputs". -* `mac_proofs` are the same as in `swap_request.mac_proofs` ([AC-03][AC-03]) -* `range_proof` is the same as in `swap_request.range_proof` ([AC-03][AC-03]) + +- `quote` is the quote ID from the previous step +- `inputs` are the same as in `swap_request.inputs` ([AC-03][AC-03]) +- `outputs` are the same as in `swap_request.outputs` ([AC-03][AC-03]) +- `balance_proof` proves that the difference `delta = inputs - outputs` is equal to **NEGATIVE** the amount in the previously generated quote. This -in English- means "the outputs encode exactly `quote.amount` more than the inputs". +- `mac_proofs` are the same as in `swap_request.mac_proofs` ([AC-03][AC-03]) +- `range_proof` is the same as in `swap_request.range_proof` ([AC-03][AC-03]) The mint then responds with `MintResponse`, which is exactly the same as `SwapResponse` ([AC-03][AC-03]): From a4b78d4f43084af9ea04709d31950b01a5df5121 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 11 Sep 2025 11:03:08 +0200 Subject: [PATCH 17/34] add nut-25 to ac04 --- protocol-extensions/anonymous-credentials/AC04.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index 8c19d32e..ff895317 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -11,6 +11,7 @@ Minting notes is a two-step process: requesting a mint quote and minting new not Method-specific NUTs describe how to handle different payment methods. The currently specified models are: - [NUT-23][23] for bolt11 Lightning invoices +- [NUT-25][25] for bolt12 Offers ## General Flow @@ -165,4 +166,5 @@ The settings for this NUT indicate the supported method-unit pairs for minting. [12]: 12.md [20]: 20.md [23]: 23.md +[25]: 25.md [AC-03]: AC03.md From 67ad0074d7908ca54b7294517348b66a1872026c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 20 Sep 2025 13:01:04 +0200 Subject: [PATCH 18/34] melt ac05 --- .../anonymous-credentials/AC04.md | 13 +- .../anonymous-credentials/AC05.md | 201 ++++++++++++++++++ 2 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 protocol-extensions/anonymous-credentials/AC05.md diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index ff895317..b0c7252d 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -77,7 +77,7 @@ After requesting a mint quote and paying the request, the wallet proceeds with m POST https://mint.host:3338/v1/kvac/mint/{method} ``` -The wallet sends a `MintRequest` with the following payload: +The wallet sends a `MintRequest`, which may vary between different methods, but with the following base payload: ```json { @@ -87,6 +87,7 @@ The wallet sends a `MintRequest` with the following payload: "balance_proof": , "mac_proofs": [, ...], "range_proof": , + // Other method-specific options } ``` @@ -111,21 +112,13 @@ The mint then responds with `MintResponse`, which is exactly the same as `SwapRe The wallet can then follow step 7 in [AC-03][AC-03] to obtain `Note`s from `response.outputs`. -## Adding New Payment Methods - -To add a new payment method (e.g., BOLT12), implement the following: - -1. Define the method-specific request and response structures following the pattern above -2. Implement the three required endpoints: quote request, quote check, and mint execution -3. Update the settings to include the new method - ## Settings The settings for this NUT indicate the supported method-unit pairs for minting. They are part of the info response of the mint ([NUT-06][06]) which reads: ```json { - "4": { + "AC4": { "methods": [ , ... diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md new file mode 100644 index 00000000..9720c3e1 --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -0,0 +1,201 @@ +# NUT-05: Melting tokens + +`mandatory` + +--- + +Melting is the process by which a client redeems coins for an off-Mint payout. Like minting, melting is a two-step process: requesting a melt quote and melting coins. This document describes the general flow that applies to all payment methods, with specifics for each supported payment method provided in dedicated method-specific NUTs. + +## Supported methods + +Method-specific NUTs describe how to handle different payment methods. The currently specified models are: + +- [NUT-23][23] for bolt11 Lightning invoices +- [NUT-25][25] for bolt12 Lightning offers + +## General Flow + +The melting process follows these steps for all payment methods: + +1. The wallet requests a melt quote for a `request` it wants paid by the mint, specifying the payment `method` and the `unit` the wallet would like to spend +2. The mint responds with a quote that includes a `quote` id and an `amount` demanded in the requested unit +3. The wallet sends a melting request including the `quote` id and provides `inputs` of the required amount +4. The mint executes the payment and responds with the payment `state` and any method-specific proof of payment + +## Common Request and Response Formats + +### Requesting a Melt Quote + +To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). + +```http +POST https://mint.host:3338/v1/melt/quote/{method} +``` + +Depending on the payment method, the request structure may vary, but all methods will include at minimum: + +```json +{ + "request": , + "unit": + // Additional method-specific fields will be required +} +``` + +The mint `Bob` responds with a quote that includes some common fields for all methods: + +```json +{ + "quote": , + "amount": , + "unit": , + "state": , + "expiry": , + "change": + // Additional method-specific fields will be included +} +``` + +Where `quote` is the quote ID, `amount` and `unit` the amount and unit that need to be provided (including estimated network fees), and `expiry` is the Unix timestamp until which the melt quote is valid. + +`state` is an enum string field with possible values `"UNPAID"`, `"PENDING"`, `"PAID"`: + +- `"UNPAID"` means that the request has not been paid yet. +- `"PENDING"` means that the request is currently being paid. +- `"PAID"` means that the request has been paid successfully. + +`change` is an optional structured field of type `MeltChange` containing the outputs of the operation: +```json +{ + "fee_return": [, ], + "outputs": [, ...], + "issued_macs": [, ...], + "issuance_proofs": [, ...] +} +``` + +Where all the fields except `fee_return` have the same function as in [AC-03](AC03) and are issued after the same verification steps described in it. The key difference is that `-(amount + keyset_fees)` (NEGATIVE the `amount` plus the keyset fees) is used as the "delta" for the `BalanceProof` verification. + +`fee_return` is a tuple of integers: +- the first is the index of the `Output` to which `fee_return` was added +- the second informs of how much was added. + +After a quote is `PAID`, `change` is no longer optional: the Mint **MUST** always provide outputs to the operation. + +### Adding FEE RETURN to the outputs + +After the payment has been processed, if the network fee is less than estimated during the melt quote step, the Mint **MUST** return the +overpaid amount, calculated as: + +``` +overpaid_network_fees = quote_amount - network_fees - keyset_fees +``` + + +> [!CAUTION] +> If `overpaid_fees` is negative, this step **MUST** be abandoned: subtracting from the outputs leads to a serious security issue. + + +The Mint can then tweak the amount commitment of one of the outputs (by convention -- the last), adding `overpaid_fees`: + +```rust +outputs[1].commitments[0].tweak_amount(cashu_kvac::secp::TWEAK_KIND::AMOUNT, overpaid_fees) +``` + +Then set `fee_return = [, overpaid_fees]`. + +**ATTENTION:** The issuance of macs and related issuance proofs **MUST** be computed **AFTER** this step, otherwise they will be invalid for the tweaked output. + + +### Check Melt Quote State + +To check whether a melt quote has been paid, the wallet makes a `GET /v1/melt/quote/{method}/{quote_id}`. + +```http +GET https://mint.host:3338/v1/melt/quote/{method}/{quote_id} +``` + +The mint responds with the same structure as the quote response. + +### Executing a Melt Quote + +To execute the melting process, the wallet calls the `POST /v1/kvac/melt/{method}` endpoint. + +```http +POST https://mint.host:3338/v1/kvac/melt/{method} +``` + +> [!IMPORTANT] +> For methods that involve external payments (like Lightning), this call may block until the payment either succeeds or fails. This can take a long time. Make sure to **use no (or a very long) timeout when making this call**! + +The wallet includes the following common data in its request: + +```json +{ + "quote": , + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": , + "mac_proofs": [, ...], + "range_proof": , + // Other method-specific options +} +``` +where: + +- `quote` is the quote ID from the previous step +- `inputs` are the same as in `swap_request.inputs` ([AC-03][AC-03]) +- `outputs` are the same as in `swap_request.outputs` ([AC-03][AC-03]) +- `balance_proof` proves that the difference `delta = inputs - outputs` is equal to **NEGATIVE** the amount in the previously generated quote. This -in English- means "the outputs encode exactly `quote.amount` more than the inputs". +- `mac_proofs` are the same as in `swap_request.mac_proofs` ([AC-03][AC-03]) +- `range_proof` is the same as in `swap_request.range_proof` ([AC-03][AC-03]) + + +## Settings + +The mint's settings for this NUT indicate the supported method-unit pairs for melting. They are part of the info response of the mint ([NUT-06][06]): + +```json +{ + "AC5": { + "methods": [ + , + ... + ], + "disabled": + } +} +``` + +`MeltMethodSetting` indicates supported `method` and `unit` pairs and additional settings of the mint. `disabled` indicates whether melting is disabled. + +`MeltMethodSetting` is of the form: + +```json +{ + "method": , + "unit": , + "min_amount": , + "max_amount": , + "options": +} +``` + +`min_amount` and `max_amount` indicate the minimum and maximum amount for an operation of this method-unit pair. `options` are method-specific and can be defined in method-specific NUTs. + +[00]: 00.md +[01]: 01.md +[02]: 02.md +[03]: 03.md +[04]: 04.md +[05]: 05.md +[06]: 06.md +[07]: 07.md +[08]: 08.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[23]: 23.md +[25]: 25.md +[AC03]: AC03.md From 77df722f606cb1683536226abc77e26063755c99 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 20 Sep 2025 13:04:05 +0200 Subject: [PATCH 19/34] small fixes --- protocol-extensions/anonymous-credentials/AC05.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md index 9720c3e1..32c7fb7c 100644 --- a/protocol-extensions/anonymous-credentials/AC05.md +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -76,9 +76,9 @@ Where `quote` is the quote ID, `amount` and `unit` the amount and unit that need Where all the fields except `fee_return` have the same function as in [AC-03](AC03) and are issued after the same verification steps described in it. The key difference is that `-(amount + keyset_fees)` (NEGATIVE the `amount` plus the keyset fees) is used as the "delta" for the `BalanceProof` verification. -`fee_return` is a tuple of integers: +`fee_return` is an *optional* tuple of integers: - the first is the index of the `Output` to which `fee_return` was added -- the second informs of how much was added. +- the second is how much was added. After a quote is `PAID`, `change` is no longer optional: the Mint **MUST** always provide outputs to the operation. From 67fb77224abf1e24e257828c5c06842183943b41 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 20 Sep 2025 13:05:27 +0200 Subject: [PATCH 20/34] minor change --- protocol-extensions/anonymous-credentials/AC05.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md index 32c7fb7c..4adfc792 100644 --- a/protocol-extensions/anonymous-credentials/AC05.md +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -80,7 +80,7 @@ Where all the fields except `fee_return` have the same function as in [AC-03](AC - the first is the index of the `Output` to which `fee_return` was added - the second is how much was added. -After a quote is `PAID`, `change` is no longer optional: the Mint **MUST** always provide outputs to the operation. +When a quote is `PAID`, `change` is *required*: the Mint **MUST** provide outputs to the operation. ### Adding FEE RETURN to the outputs From 3c33070e2adb44224aba96a0b2c544eabf8f9e8a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sun, 21 Sep 2025 23:47:14 +0200 Subject: [PATCH 21/34] AC06 bootstrap --- .../anonymous-credentials/AC00.md | 3 + .../anonymous-credentials/AC03.md | 7 +- .../anonymous-credentials/AC06.md | 148 ++++++++++++++++++ 3 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 protocol-extensions/anonymous-credentials/AC06.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index b13a8a74..7d384fea 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -173,6 +173,9 @@ The Mint identifies transaction `Output`s by their `tag` (`output.tag`) ### Clients Clients can identify both `Note` and `UnsignedNote` by their `tag` (`{unsigned_note|note}.tag`). +[AC-03]: AC03.md +[AC-06]: AC06.md + [KVAC]: https://github.com/lollerfirst/cashu-kvac.git [10]: 10.md diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index b90c11f6..81d9a97b 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -16,9 +16,7 @@ sending user: Alice receiving user: Carol ``` -Alice has a 64 sat `Note`. She wants to send Carol 40 sat, but does not have the necessary `Note`. For that, Alice requests a swap from the Mint and uses her `Note` plus a `Note` worth _zero_ (see [AC-??][AC-??]) as `Input`s [0, 64] and asks for new `Output`s worth [40, 24] totalling 64 sat. The `Note`s that Alice sent to the Mint as `Input`s of the swap operation are now invalidated. - -[AC-??]: AC??.md +Alice has a 64 sat `Note`. She wants to send Carol 40 sat, but does not have the necessary `Note`. For that, Alice requests a swap from the Mint and uses her `Note` plus a `Note` worth _zero_ (see [AC-06][AC-06]) as `Input`s [0, 64] and asks for new `Output`s worth [40, 24] totalling 64 sat. The `Note`s that Alice sent to the Mint as `Input`s of the swap operation are now invalidated. ### Swap To Receive @@ -41,7 +39,7 @@ We describe the swap process following the case of the "Swap To Receive" example The client selects an appropriate `keyset_id` amongst the active keysets and generates two `UnsignedNote`s (see [AC-00][AC-00]): - one with for the amount `b+a`. -- one with the amount `0`. +- one with the amount `0` (see [AC-06][AC-06]). Then, for each `UnsignedNote`, they derive the corresponding `Output` as shown in [AC-00][AC-00]. @@ -216,5 +214,6 @@ In the final step, the client receives a `SwapResponse` and verifies each (`Unsi They can then proceed to create a full `Note` by combining `UnsignedNote` and the received `MAC` and `IssuanceProof`. [AC-00]: AC00.md +[AC-06]: AC06.md [11]: 11.md [14]: 14.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 From bb06d5661816f36f9d0696c90e18ca173256bb9d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 12:00:41 +0200 Subject: [PATCH 22/34] AC-00/01/02/03: Restructure to mirror AC-06 style - Add Status/Scope/Dependencies headings - Normalize numbered sections: Endpoint, Request, Verification, Response, Errors, Security - Enforce normative constraints (units, active keysets, tag/hex formats) - Clarify transcript ordering and fee delta in AC-03 - Align AC-01/02 keyset semantics and error codes - Improve interop notes and references --- .../anonymous-credentials/AC00.md | 211 +++++++-------- .../anonymous-credentials/AC01.md | 122 ++++----- .../anonymous-credentials/AC02.md | 84 +++--- .../anonymous-credentials/AC03.md | 246 +++++++----------- 4 files changed, 291 insertions(+), 372 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 7d384fea..3f457126 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -4,102 +4,99 @@ --- -This document details the notation and models used throughout the specification. [cashu-kvac][KVAC] is herein referenced as a black-box implementation of the cryptographic operations for Anonymous Credentials, so this specification **WON'T** delve into the mathematic details of the KVAC scheme. +Status: Stable draft +Scope: Shared data models used by AC-01/02/03/04/05/06 +Dependencies: cashu-kvac (KVAC), NUT-01/02/10/11/14 -## Terminology Differences +This document defines the core data models and normative constraints used by the Anonymous Credentials (AC) extensions. It mirrors the sectioning and constraint style used in AC-06 to improve clarity and conformance. -Here are some differences in terminology with respect to the main specifications: +## 1. Overview -- _proof_ $\rightarrow$ _note_: _proof_ assumes a different meaning in this context (Zero Knowledge Proofs) so in order to avoid equivocation, the term _note_ will be used in its place. +[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. -- _Mint_ is written with a capital `M`, avoiding equivocation with the verb _to 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 binary-encoded KVAC types. -- At times, `cashu_kvac::{Struct}` will be used inside JSON code-blocks. This is to abstract away structured types that are fundamental components of the cryptogaphic scheme, but of no interest for the scope of this specification. +## 2. Keys -## Mint Keys +### 2.1 Mint Private Key -### Private Key - -The Mint's _signing_ private key (`MintPrivateKey`), which will be used to issue notes: - -> [!NOTE] -> `Scalar::random()` is not a deterministic. In practice, use a deterministic derivation to generate secrets. +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(); ``` -### Public Key - -The Mint's _signing_ public key (`MintPublicKey`), which can be used to verify notes client-side: +### 2.2 Mint Public Key ```rust let mint_pubkey: cashu_kvac::models::MintPublicKey = mint_privkey.public_key.clone(); ``` -## Unsigned Note +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 -An `UnsignedNote` is a note created by the client and yet to be signed by the Mint: +A client-created note not yet signed by the Mint. ```json { - "keyset_id": , - "amount": , - "script": , - "unit": , - "tag": <64-char-hex>, - "attributes": [, ], + "keyset_id": "", + "amount": 1337, + "script": "", + "unit": "sat", + "tag": "<64-hex>", + "attributes": [ + "", + "" + ] } ``` -where: +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. +- script: optional NUT-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. -- `keyset_id` is the ID of the keyset that will sign this note (see [AC-02][AC-02]) -- `amount` is the amount this note represents -- (optional) `script` is a [NUT-10](10) spending condition -- `unit` is the unit of the note (e.g. "sat", "msat", "usd") -- `tag` is a random unique identifier for this note, chosen by the client. +Example attribute creation: ```rust -let tag = Scalar::random(); -``` - -- `attributes` is a tuple containing a `AmountAttribute` and a `ScriptAttribute`. These can be created by the client with: - -```rust -let blinding_factor_amount = "deadbeefdeadbeefdeadbeefdeadbeef"; -let blinding_factor_script = "baadc0debaadc0debaadc0debaadc0de"; - -let amount_attr = cashu_kvac::models::AmountAttribute::new(1337, Some(blinding_factor_amount)); - -/* No spending conditions */ -let script_attr = cashu_kvac::models::ScriptAttribute::new(b"", Some(blinding_factor_script)); +let amount_attr = cashu_kvac::models::AmountAttribute::new(1337, Some("deadbeefdeadbeefdeadbeefdeadbeef")); +let script_attr = cashu_kvac::models::ScriptAttribute::new(b"", Some("baadc0debaadc0debaadc0debaadc0de")); ``` -## Output +### 3.2 Output -`Output` is -as the name suggests- an output of a transaction and it holds all of the information a Mint is allowed to see of a `UnsignedNote`: +The Mint-visible view of an UnsignedNote. ```json { - "id": , - "tag": <64-char-hex>, - "commitments": <[66-char-hex, 66-char-hex]>, + "id": "", + "tag": "<64-hex>", + "commitments": ["<66-hex>", "<66-hex>"] } ``` -where: +Constraints: +- id MUST equal `unsigned_note.keyset_id`. +- commitments[0] is the amount commitment; commitments[1] is the script commitment. +- The pair (id, tag) SHOULD be unique in any single request. -- `id` is `unsigned_note.keyset_id` -- `tag` is `unsigned_note.tag` -- `commitments` is a tuple containing the commiments from the original attributes, namely: - - `unsigned_note.attributes.0.commitment()` - - `unsigned_note.attributes.1.commitment()`. +### 3.3 MAC -## MAC - -A `MAC` (Message Authentication Code) is the signature the Mint can issue on `Output`, using `MintPrivateKey`: +A Mint signature over Output commitments and tag. ```rust let mac = MAC::generate( @@ -107,77 +104,87 @@ let mac = MAC::generate( &output.commitments.0, Some(&output.commitments.1), Some(output.tag), -) +); ``` -## Note - -A `Note` (or _signed note_) is a note that has received a `MAC` from the Mint. +### 3.4 Note (signed) ```json { - "keyset_id": , - "amount": , - "script": , - "unit": , - "tag": <64-char-hex>, - "mac": <66-char-hex>, - "attributes": [, ], - "issuance_proof": + "keyset_id": "", + "amount": 1337, + "script": "", + "unit": "sat", + "tag": "<64-hex>", + "mac": "<66-hex>", + "attributes": [ + "", + "" + ], + "issuance_proof": "" } ``` -where: - -- `keyset_id`, `amount`, `script`, `unit` `attributes` and `tag` are the same as in `UnsignedNote`. -- `mac` is the signature value returned by the Mint -- `issuance_proof` is a Zero-Knowledge Proof returned by the Mint that confirms `MAC` was issued using `MintPublicKey`. +Constraints: +- Fields mirror UnsignedNote. +- `issuance_proof` MUST verify against the MintPublicKey for `keyset_id`. -## Input +### 3.5 Input -`Input` is an input to a transaction and holds all of the information a Mint is allowed to see of a `Note`. It's derived from `Note` as the following: +Derived from a Note to spend it. ```json { - "id": , - "script": , - "randomized_commitments": , - "witness": + "id": "", + "script": "", + "randomized_commitments": "", + "witness": "" } ``` -where: - -- `id` is `note.keyset_id`. -- (optional) `script` is the serialized [NUT-10][10] spending condition associated with this note (`note.script`). -- (optional) `witness` is the serialized [NUT-10][10] witness that unlocks the specific spending condition in `script`. -- `randomized_commitments` are derived from `note.tag`, `note.mac` and `note.attributes` as the following: +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); +let randomized_commitments = cashu_kvac::models::RandomizedCommitments::from_attributes_and_mac( + ¬e.attributes.0, + Some(¬e.attributes.1), + note.tag, + note.mac, + true, +); ``` -## Identifiers +Constraints: +- id MUST equal `note.keyset_id`. +- witness/script handling per NUT-10/11/14. -### Mint +## 4. Identifiers -The Mint identifies transaction `Input`s by their `Cv` randomized commitment (`input.randomized_commitments.Cv`). +- Mint identifies Inputs by `input.randomized_commitments.Cv` (nullifier base) and Outputs by `output.tag`. +- Clients identify Notes/UnsignedNotes by `tag`. -The Mint identifies transaction `Output`s by their `tag` (`output.tag`) +## 5. Validation and Interop Rules -### Clients +- 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/AC-02) at the time of issuance. +- Tags SHOULD be unique per request to avoid ambiguity and replay within the same batch. -Clients can identify both `Note` and `UnsignedNote` by their `tag` (`{unsigned_note|note}.tag`). -[AC-03]: AC03.md -[AC-06]: AC06.md +## 6. Security and Policy Notes +- Tags and blinding factors MUST be generated with cryptographically secure randomness. +- Scripts (NUT-10) may reveal spending policy to the Mint via commitments; avoid embedding sensitive metadata. +- IssuanceProof verification MUST use the correct MintPublicKey derived from the stated keyset id. +- Nullifier reuse is prevented by the Mint; clients SHOULD treat spent Notes as invalid after a successful swap. +## 7. References + +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md +[AC-06]: AC06.md [KVAC]: https://github.com/lollerfirst/cashu-kvac.git +[01]: 01.md [10]: 10.md -[13]: 13.md -[AC-02]: AC02.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 index a44ee771..aed20b48 100644 --- a/protocol-extensions/anonymous-credentials/AC01.md +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -1,110 +1,86 @@ -# AC-01: Mint public key exchange +# AC-01: Mint Public Key Exchange `mandatory` --- -This document outlines the exchange of the public keys of the Mint with the wallet user. +Status: Stable draft +Scope: Retrieval of active and specific Mint keysets +Dependencies: AC-00, AC-02, NUT-02 -## Description +This document outlines how wallets obtain the Mint’s active keysets and optionally retrieve a specific keyset by id. -Wallet user receives public keys from Mint via `GET /v1/kvac/keys`. +## 1. Endpoints -The Mint responds only with its `active` keysets. Keyset are `active` if the mint will sign outputs with it. The mint will accept tokens from inactive keysets as inputs but will not sign with them for new outputs. The `active` keysets can change over time, for example due to key rotation. A list of all keysets, active and inactive, can be requested separately (see [AC-02](AC-02)). +- GET `/v1/kvac/keys` — list of active keysets +- GET `/v1/kvac/keys/{keyset_id_hex}` — a specific keyset (active or inactive) -A wallet can ask for the keys of a specific (active or inactive) keyset via the endpoint `GET /v1/kvac/keys/{keyset_id}` (see [AC-02][02]). +Content-Type: application/json -## Supported Currency Units +## 2. Responses -Refer to [NUT-01](01), as there are no changes from the main spec. - -## Keyset generation - -Keysets are generated by the Mint. Each keyset contains exactly $1$ key, which is the `MintPublicKey` generated from `MintPrivateKey` as described in [AC-00](AC-00). -`MintPublicKey` is internally composed by $2$ public keys: - -```json -{ - "Cw": "0312..3f", - "I": "0213..10" -} -``` - -Therefore, its keyset `id` is computed from both of these values (see [AC-02](AC-02) and [NUT-02](02)). The mint **MUST** use the [compressed Secp256k1 public key format](https://learnmeabitcoin.com/technical/public-key#public-key-format) to represent its public keys. - -## Network Requests - -A client can request: - -```http -GET https://mint.host:3338/v1/kvac/keys -``` - -With curl: - -```bash -curl -X GET https://mint.host:3338/v1/kvac/keys -``` - -Response `GetKeysResponse` of the Mint: +### 2.1 GET /v1/kvac/keys ```json { "keysets": [ { - "id": , - "unit": , - "final_expiry": , + "id": "", + "unit": "", + "final_expiry": "", "keys": { - "Cw": , - "I": , + "Cw": "", + "I": "" } - }, - ... + } ] } ``` -Where: +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). -- `id` is the keyset ID, recomputable by the client (see [AC-02](AC-02) and [NUT-02](02)). -- `unit` is the unit of the keyset. -- (optional) `final_expiry` is the unix epoch after which the keyset might be permanently deleted from the Mint's database (and thus any note issued from that keyset is invalidated). -- `keys` holds the decomposition of the `MintPublicKey` as explained above. +### 2.2 GET /v1/kvac/keys/{keyset_id_hex} ---- - -A client can also request: - -```http -GET https://mint.host:3338/v1/kvac/keys/{keyset_id_hex_str} -``` - -With curl: - -```bash -curl -X GET https://mint.host:3338/v1/kvac/keys/{keyset_id_hex_str} -``` - -In this case, the Mint will return the keyset -if it exists- regardless of its `active` status: +Returns the keyset regardless of active status: ```json { "keysets": [ { - "id": , - "unit": , - "final_expiry": , + "id": "", + "unit": "", + "final_expiry": "", "keys": { - "Cw": , - "I": , + "Cw": "", + "I": "" } } ] } ``` -[01]: 01.md -[02]: 02.md -[AC-02]: AC02.md +## 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 index 77a9772b..217bf9ea 100644 --- a/protocol-extensions/anonymous-credentials/AC02.md +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -1,65 +1,61 @@ -# AC-02: Keysets and fees +# AC-02: Keysets and Fees `mandatory` --- -KVAC keysets largely relies on the main specification in [NUT-02][02]. This document only reports the difference in network endpoints for _getting_ keysets specific to this extension of the protocol. +Status: Stable draft +Scope: Listing all keysets and their fee policy +Dependencies: AC-00, AC-01, NUT-02 -## Keyset Properties +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. -### Keyset ID +## 1. Endpoint -A keyset id is an identifier for a specific keyset. It can be derived by anyone who knows the set of public keys of a mint. Wallets CAN compute the keyset id for a given keyset by themselves to confirm that the mint is supplying the correct keyset ID. +- GET `/v1/kvac/keysets` +- Content-Type: application/json -The keyset id is in each `Note` so it can be used by wallets to identify which Mint and keyset it was generated from. The keyset field id is also present in the `Input`s and `Output`s sent and received to/from the Mint (see [AC-00][AC-00]). - -### Active keysets - -Refer to [NUT-02][02]. - -### Fees - -Refer to [NUT-02][02]. - -### Deriving the keyset ID - -Refer to [NUT-02][02]. - -## Network Requests - -A wallet can ask the mint for a list of all keysets via the `GET /v1/kvac/keysets` endpoint. - -Request: - -```http -GET https://mint.host:3338/v1/kvac/keysets -``` - -With curl: - -```bash -curl -X GET https://mint.host:3338/v1/kvac/keysets -``` - -Response `GetKeysetsResponse` of `Bob`: +## 2. Response ```json { "keysets": [ { - "id": , - "unit": , - "active": , - "final_expiry": , - "input_fee_ppk": , - }, - ... + "id": "", + "unit": "", + "active": true, + "final_expiry": "", + "input_fee_ppk": "" + } ] } ``` -Here, `id` is the keyset ID, `unit` is the unit string (e.g. "sat") of the keyset, `active` indicates whether new ecash can be minted with this keyset, and `input_fee_ppk` is the fee (per thousand units) to spend one input spent from this keyset. If `input_fee_ppk` is not given, we assume it to be `0`. +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_ppk`: optional per-thousand fee applied per input in swap flows. If omitted, treated as 0. + +Constraints: +- Clients MUST treat `active=false` keysets as valid for Inputs but NOT for new Outputs. +- Fee calculation: in AC-03 balance verification, the delta MUST equal the sum of per-input fees (see AC-03 §3.6). If fractional fees arise, Mints MUST define rounding (RECOMMENDED: floor to integer units); clients MUST match the Mint policy. + +## 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 [02]: 02.md diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 81d9a97b..7875af42 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -4,216 +4,156 @@ --- -This document describes the core process by which a client can present previously signed `Note`s and get Mint signatures on `UnsignedNote`s. That is, if all conditions are upheld. +Status: Stable draft +Scope: Spend existing Notes (Inputs) to obtain signatures on new UnsignedNotes (Outputs) +Dependencies: AC-00, AC-01, AC-02, NUT-10/11/14 -## Use Cases +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. -### Swap To Send +## 1. Endpoint -``` -sending user: Alice - -receiving user: Carol -``` - -Alice has a 64 sat `Note`. She wants to send Carol 40 sat, but does not have the necessary `Note`. For that, Alice requests a swap from the Mint and uses her `Note` plus a `Note` worth _zero_ (see [AC-06][AC-06]) as `Input`s [0, 64] and asks for new `Output`s worth [40, 24] totalling 64 sat. The `Note`s that Alice sent to the Mint as `Input`s of the swap operation are now invalidated. - -### Swap To Receive - -``` -sending user: Alice - -receiving user: Carol -``` - -The second use case for the swap operation follows up on the example above, where Alice has swapped her `Note`s ready to be sent to Carol. Carol can receive the `Note` using the same operation: the received `Note` is used as `Input` and new `Output`s are requested from the Mint. -Remember that `Output`s are just `UnsignedNote`s with less information about them. -Once Carol has redeemed to new `Output`s, Alice can't double-spend the `Note` anymore and the transaction is settled, all the while the Mint is unaware it happened. - -## Process - -We describe the swap process following the case of the "Swap To Receive" example. Specifically, we assume the client possesses two `Note`s: one that encodes the current balance `b`; the other is received from another user of the same Mint and encodes an amount `a`. The client needs to "merge" these `Note`s into one to attain a new balance. - -### 1. Client generates `UnsignedNote`s and `Output`s - -The client selects an appropriate `keyset_id` amongst the active keysets and generates two `UnsignedNote`s (see [AC-00][AC-00]): - -- one with for the amount `b+a`. -- one with the amount `0` (see [AC-06][AC-06]). - -Then, for each `UnsignedNote`, they derive the corresponding `Output` as shown in [AC-00][AC-00]. - -### 2. Client derives `Input`s. - -For each of the two `Note`s, the client derives `Input`s as shown in [AC-00][AC-00]. - -### 3. Client generates Zero-Knowledge Proofs - -First, create a `cashu_kvac::transcript::CashuTranscript` instance. This will be used throughout the generation of the proofs. - -```rust -let transcript = cashu_kvac::transcript::CashuTranscript::new(); -``` - -Then, generate the following proofs: - -1. For each `Input` generate a `MacProof`. This proves the signature on that input. - -```rust -let mac_proof = cashu_kvac::kvac::MacProof::create( - &mint_publickey, - ¬e.attributes.0, - Some(¬e.attributes.1), - note.tag, - &input.randomized_commitments, - &mut transcript -); -``` - -2. Generate one `RangeProof` for _all_ `UnsignedNote`s. This proves that each `Output` is in range. +- Method: POST +- Path: `/v1/kvac/swap` +- Content-Type: application/json -```rust -let amount_attributes: Vec = unsigned_notes - .iter() - .map(|u| u.attributes.0.clone()) - .collect(); -let range_proof = cashu_kvac::kvac::RangeProof::create_bulletproof( - &mut transcript, - &amount_attributes -); -``` - -3. Generate a `BalanceProof`. This proves that inputs and outputs are balanced when accounting for the input fees: `inputs - outputs == inputFees`. - -```rust -let balance_proof = cashu_kvac::kvac::BalanceProof::create( - &inputs_amount_attributes, - &outputs_amount_attributes, - &mut transcript -); -``` - -> [!NOTE] -> The order is important. If client/Mint prove/verify in a different order, the verification won't succeed. +## 2. Request -### 4. Client sends a `SwapRequest` - -The client then performs a `POST v1/kvac/swap` request with the following `SwapRequest` payload: +The client sends a `SwapRequest`: ```json { - "inputs": [, ...], - "outputs": [, ...], - "balance_proof": , - "mac_proofs": [, ...], - "range_proof": , + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": "", + "mac_proofs": ["", ...], + "range_proof": "" } ``` -### 5. Mint receives and verifies `SwapRequest` +Constraints: +- inputs.len() > 0 +- outputs.len() > 0 +- mac_proofs.len() == inputs.len() +- 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 SHOULD be unique within the request; duplicate tags across Outputs SHOULD be avoided. +- Transcript ordering for proofs MUST be consistent between client and Mint (see §3). + +## 3. Mint Verification -Validation of a swap request works this way: +Upon receiving `SwapRequest`, the Mint performs: -0. Verify all `Input`s and `Output`s are of the same unit. +1) Unit and keyset checks +- Verify single-unit consistency across all Inputs/Outputs. +- Verify each Output’s keyset id is active. -1. Extract -if any- `script` and `witness` from each `Input`, and verify the spending conditions according to either [NUT-11][11] or [NUT-14][14]. +2) Script verification +- For each Input, extract `script`/`witness` if present and verify per [NUT-11] or [NUT-14]. +- Reject on policy failure. -2. Verify each `Input` nullifier (`input.randomized_commitments.get_nullifier()`) was not already presented. +3) Nullifier checks +- Derive nullifier from `input.randomized_commitments.Cv`. +- Reject if nullifier already seen (double-spend). -3. Create a `CashuTranscript` instance. This will be used throughout the verification of the proofs: +4) Initialize transcript ```rust -let transcript = cashu_kvac::transcript::CashuTranscript::new(); +let mut transcript = cashu_kvac::transcript::CashuTranscript::new(); ``` -4. Verify each (`Input`, `MacProof`) pair. Use the key from the `input.id` keyset to verify the `MacProof`: +5) Verify MacProofs (one per Input) ```rust cashu_kvac::kvac::MacProof::verify( &mint_privkeys[input.id], &input.randomized_commitments, - &input.script, // This will be None if the input does not provide a script + &input.script, // None if no script mac_proof, &mut transcript, ); ``` -5. Verify the `RangeProof` with the amount commitments in each `Output`: +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 -); +let amount_commitments: Vec = outputs.iter().map(|o| o.commitments.0).collect(); +cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range_proof); ``` -6. Verify the `BalanceProof`. Use `input.randomized_commitments` from each input, `output.commitments[0]` from each output and the supposed difference between their value, which in this case is $0$. +7) Verify BalanceProof + +- Let `delta` be the fee-adjusted difference. For no-fee keysets, `delta = 0`. +- With per-input fees (see AC-02), `delta = sum(input_fees)`. ```rust cashu_kvac::kvac::BalanceProof::verify( &inputs_randomized_commitments, &outputs_amount_commitments, - 0 as i64, + delta as i64, balance_proof, - &mut transcript + &mut transcript, ); ``` -> [!NOTE] -> If a keyset supports fees, the difference between inputs and outputs should be equal to the fee the Mint enforces on the inputs. +Reject if any verification fails. -### 6. Mint issues signatures +## 4. Response -If all of the previous verifications succeeded, the Mint issues a `mac` for each `output` in `outputs` using the key from the keyset with ID `output.id` (see [AC-00][AC-00]). - -The Mint also computes and returns a `IssuanceProof` for each issued `mac`: - -```rust -let issuance_proof = cashu_kvac::kvac::IssuanceProof::create( - &mint_privkeys[output.id], - output.tag, - mac, - output.commitments.0, - Some(output.commitments.1), -); -``` - -> [!NOTE] -> The Mint does not use the transcript to compute this proof. - -Finally, the response the the swap request `SwapResponse` is as follows: +`SwapResponse`: ```json { - "outputs": [, ...], - "issued_macs": [, ...], - "issuance_proofs": [, ...] + "outputs": [, ...], + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] } ``` -### 7. Client verifies and assembles `Notes` +- `issued_macs[i]` and `issuance_proofs[i]` correspond to `outputs[i]`. -In the final step, the client receives a `SwapResponse` and verifies each (`UnsignedNote`, `MAC`, `IssuanceProof`) tuple: +## 5. Client Verification and Note Assembly + +For each (`UnsignedNote`, `MAC`, `IssuanceProof`) triple: ```rust - cashu_kvac::kvac::IssuanceProof::verify( - &mint_pubkey, - unsigned_note.tag, - mac, - &unsigned_note.attributes.0, - Some(&unsigned_note.attributes.1), - issuance_proof.clone(), - ) +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(), +); ``` -They can then proceed to create a full `Note` by combining `UnsignedNote` and the received `MAC` and `IssuanceProof`. +If verification succeeds, the wallet combines `UnsignedNote`, `MAC`, and `IssuanceProof` into a `Note` (AC-00). + +## 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-06]: AC06.md +[AC-01]: AC01.md +[AC-02]: AC02.md [11]: 11.md [14]: 14.md From e363526a26fb071e9196fdacee9cc9b9a198772a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 12:05:28 +0200 Subject: [PATCH 23/34] AC-04/05: Restructure to mirror AC-06 style - Add Status/Scope/Dependencies and numbered sections - Normalize endpoints, request/response, constraints, verification, errors - Define mint/melt balance delta, unit and keyset constraints - Add fee return handling and ordering of tweaks vs signature issuance (AC-05) - Clarify security/policy notes and references --- .../anonymous-credentials/AC04.md | 241 ++++++++++++------ .../anonymous-credentials/AC05.md | 223 ++++++++-------- 2 files changed, 268 insertions(+), 196 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index b0c7252d..ced7cac1 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -4,117 +4,201 @@ --- -Minting notes is a two-step process: requesting a mint quote and minting new notes. This document describes the general flow that applies to all payment methods, with specifics for each supported payment method provided in dedicated NUTs. +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 -## Supported methods +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. This document mirrors the AC-06 structure for clarity and interop. -Method-specific NUTs describe how to handle different payment methods. The currently specified models are: +## 1. Supported methods -- [NUT-23][23] for bolt11 Lightning invoices -- [NUT-25][25] for bolt12 Offers +Method-specific NUTs describe payment handling: +- [NUT-23][23] — bolt11 Lightning invoices +- [NUT-25][25] — bolt12 Offers -## General Flow +## 2. Endpoints -The minting process follows these steps for all payment methods: +- 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 -1. The wallet requests a mint quote for the `unit` to mint, specifying the payment `method`. -2. The mint responds with a quote that includes a `quote` id and a payment `request`. -3. The user pays the `request` using the specified payment method. -4. The wallet then requests minting of new notes with the mint, including the `quote` id and new `outputs`. -5. The mint verifies payment and returns blind signatures. +Content-Type: application/json -## Common Request and Response Formats +## 3. Request/Response: Mint Quote -### Requesting a Mint Quote - -To request a mint quote, a wallet makes a `POST /v1/mint/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). +### 3.1 Request (quote) ```http POST https://mint.host:3338/v1/mint/quote/{method} ``` -Depending on the payment method, the request structure may vary, but all methods will include at minimum: - +Body (baseline, method-specific fields may apply): ```json { - "unit": - // Additional method-specific fields may be required + "unit": "" } ``` -The Mint responds with a quote that includes some common fields for all methods: +### 3.2 Response (quote) ```json { - "quote": , - "request": , - "unit": , - // Additional method-specific fields will be included + "quote": "", + "request": "", + "unit": "" + // method-specific fields may be included } ``` -Where `quote` is the quote ID, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request. - -> [!CAUTION] -> -> `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the notes that this operation mints. To prevent this, use [NUT-20][20] locks to enforce public key authentication during minting. - -### Check Mint Quote State +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. -To check whether a mint quote has been paid, the wallet makes a `GET /v1/mint/quote/{method}/{quote_id}`. +### 3.3 Check quote status ```http GET https://mint.host:3338/v1/mint/quote/{method}/{quote_id} ``` -The mint responds with the same structure as the initial quote response. +Response: same structure as initial quote response; method-specific fields may indicate status. -### Executing a Mint Quote +## 4. Request: Execute Mint -After requesting a mint quote and paying the request, the wallet proceeds with minting new notes by calling the `POST /v1/kvac/mint/{method}` endpoint. +After paying the quote, the wallet submits: ```http POST https://mint.host:3338/v1/kvac/mint/{method} ``` -The wallet sends a `MintRequest`, which may vary between different methods, but with the following base payload: - +Body `MintRequest` (baseline): ```json { - "quote": , - "inputs": [, ...], - "outputs": [, ...], - "balance_proof": , - "mac_proofs": [, ...], - "range_proof": , - // Other method-specific options + "quote": "", + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": "", + "mac_proofs": ["", ...], + "range_proof": "" + // method-specific fields may be included } ``` -where: +Constraints: +- outputs.len() > 0 +- mac_proofs.len() == inputs.len() +- All Inputs/Outputs MUST share the same `unit` (see AC-00). +- Each `outputs[i].id` MUST correspond to an active keyset (AC-01/AC-02). +- `quote` MUST be known, paid, and unexpired. +- Transcript ordering for proofs MUST match the Mint (see §5). + +Balance constraint (delta): +- Let `fees = sum(input_fees)` per AC-02 for the given keysets present in `inputs`. +- The BalanceProof MUST verify with `delta = fees - quote_amount` so that `inputs - outputs == 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 +- 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), +); +``` -- `quote` is the quote ID from the previous step -- `inputs` are the same as in `swap_request.inputs` ([AC-03][AC-03]) -- `outputs` are the same as in `swap_request.outputs` ([AC-03][AC-03]) -- `balance_proof` proves that the difference `delta = inputs - outputs` is equal to **NEGATIVE** the amount in the previously generated quote. This -in English- means "the outputs encode exactly `quote.amount` more than the inputs". -- `mac_proofs` are the same as in `swap_request.mac_proofs` ([AC-03][AC-03]) -- `range_proof` is the same as in `swap_request.range_proof` ([AC-03][AC-03]) +## 6. Response -The mint then responds with `MintResponse`, which is exactly the same as `SwapResponse` ([AC-03][AC-03]): +`MintResponse` (identical structure to `SwapResponse` in AC-03): ```json { - "outputs": [, ...], - "issued_macs": [, ...], - "issuance_proofs": [, ...] + "outputs": [, ...], + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] } ``` -The wallet can then follow step 7 in [AC-03][AC-03] to obtain `Note`s from `response.outputs`. +## 7. Client Verification and Note Assembly + +As in AC-03/AC-06: verify `IssuanceProof` per Output and assemble `Note`s if verification succeeds. -## Settings +## 8. Errors -The settings for this NUT indicate the supported method-unit pairs for minting. They are part of the info response of the mint ([NUT-06][06]) which reads: +- 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 { @@ -128,36 +212,29 @@ The settings for this NUT indicate the supported method-unit pairs for minting. } ``` -`MintMethodSetting` indicates supported `method` and `unit` pairs and additional settings of the mint. `disabled` indicates whether minting is disabled. - -`MintMethodSetting` is of the form: - +`MintMethodSetting`: ```json { - "method": , - "unit": , - "min_amount": , - "max_amount": , - "options": + "method": "", + "unit": "", + "min_amount": "", + "max_amount": "", + "options": "" } ``` -`min_amount` and `max_amount` indicate the minimum and maximum amount for an operation of this method-unit pair. `options` are method-specific and can be defined in method-specific NUTs. +## 10. Security and Policy Notes -[00]: 00.md -[01]: 01.md -[02]: 02.md -[03]: 03.md -[04]: 04.md -[05]: 05.md +- 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 SHOULD be unique within the request to avoid ambiguity/replay. + +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md [06]: 06.md -[07]: 07.md -[08]: 08.md -[09]: 09.md -[10]: 10.md -[11]: 11.md -[12]: 12.md [20]: 20.md [23]: 23.md [25]: 25.md -[AC-03]: AC03.md diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md index 4adfc792..d29fb8d3 100644 --- a/protocol-extensions/anonymous-credentials/AC05.md +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -1,159 +1,161 @@ -# NUT-05: Melting tokens +# AC-05: Melt Transaction `mandatory` --- -Melting is the process by which a client redeems coins for an off-Mint payout. Like minting, melting is a two-step process: requesting a melt quote and melting coins. This document describes the general flow that applies to all payment methods, with specifics for each supported payment method provided in dedicated method-specific NUTs. +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 -## Supported methods +Melting is a two-step process: (1) request a melt quote for an external payment, (2) provide Inputs and execute the melt. This document mirrors AC-06 structure and aligns fee-return handling with proof verification. -Method-specific NUTs describe how to handle different payment methods. The currently specified models are: +## 1. Supported methods -- [NUT-23][23] for bolt11 Lightning invoices -- [NUT-25][25] for bolt12 Lightning offers +- [NUT-23][23] — bolt11 Lightning invoices +- [NUT-25][25] — bolt12 Lightning offers -## General Flow +## 2. Endpoints -The melting process follows these steps for all payment methods: +- 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 -1. The wallet requests a melt quote for a `request` it wants paid by the mint, specifying the payment `method` and the `unit` the wallet would like to spend -2. The mint responds with a quote that includes a `quote` id and an `amount` demanded in the requested unit -3. The wallet sends a melting request including the `quote` id and provides `inputs` of the required amount -4. The mint executes the payment and responds with the payment `state` and any method-specific proof of payment +Content-Type: application/json -## Common Request and Response Formats +## 3. Request/Response: Melt Quote -### Requesting a Melt Quote - -To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). +### 3.1 Request (quote) ```http POST https://mint.host:3338/v1/melt/quote/{method} ``` -Depending on the payment method, the request structure may vary, but all methods will include at minimum: - +Body (baseline): ```json { - "request": , - "unit": - // Additional method-specific fields will be required + "request": "", + "unit": "" } ``` -The mint `Bob` responds with a quote that includes some common fields for all methods: +### 3.2 Response (quote) ```json { - "quote": , + "quote": "", "amount": , - "unit": , - "state": , + "unit": "", + "state": "", "expiry": , - "change": - // Additional method-specific fields will be included -} -``` - -Where `quote` is the quote ID, `amount` and `unit` the amount and unit that need to be provided (including estimated network fees), and `expiry` is the Unix timestamp until which the melt quote is valid. - -`state` is an enum string field with possible values `"UNPAID"`, `"PENDING"`, `"PAID"`: - -- `"UNPAID"` means that the request has not been paid yet. -- `"PENDING"` means that the request is currently being paid. -- `"PAID"` means that the request has been paid successfully. - -`change` is an optional structured field of type `MeltChange` containing the outputs of the operation: -```json -{ + "change": { "fee_return": [, ], "outputs": [, ...], - "issued_macs": [, ...], - "issuance_proofs": [, ...] + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] + } } ``` -Where all the fields except `fee_return` have the same function as in [AC-03](AC03) and are issued after the same verification steps described in it. The key difference is that `-(amount + keyset_fees)` (NEGATIVE the `amount` plus the keyset fees) is used as the "delta" for the `BalanceProof` verification. - -`fee_return` is an *optional* tuple of integers: -- the first is the index of the `Output` to which `fee_return` was added -- the second is how much was added. - -When a quote is `PAID`, `change` is *required*: the Mint **MUST** provide outputs to the operation. +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]`. -### Adding FEE RETURN to the outputs +## 4. Fee Return Handling -After the payment has been processed, if the network fee is less than estimated during the melt quote step, the Mint **MUST** return the -overpaid amount, calculated as: +After the external payment is executed, if actual network fees are less than estimated: ``` overpaid_network_fees = quote_amount - network_fees - keyset_fees ``` - -> [!CAUTION] -> If `overpaid_fees` is negative, this step **MUST** be abandoned: subtracting from the outputs leads to a serious security issue. - - -The Mint can then tweak the amount commitment of one of the outputs (by convention -- the last), adding `overpaid_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[1].commitments[0].tweak_amount(cashu_kvac::secp::TWEAK_KIND::AMOUNT, overpaid_fees) +outputs[last].commitments[0].tweak_amount(cashu_kvac::secp::TWEAK_KIND::AMOUNT, overpaid_network_fees); ``` -Then set `fee_return = [, overpaid_fees]`. - -**ATTENTION:** The issuance of macs and related issuance proofs **MUST** be computed **AFTER** this step, otherwise they will be invalid for the tweaked output. +- Set `fee_return = [last, overpaid_network_fees]`. +- IMPORTANT: Mint MUST compute MACs and IssuanceProofs AFTER applying this tweak, or proofs will not validate. - -### Check Melt Quote State - -To check whether a melt quote has been paid, the wallet makes a `GET /v1/melt/quote/{method}/{quote_id}`. +## 5. Request: Execute Melt ```http -GET https://mint.host:3338/v1/melt/quote/{method}/{quote_id} +POST https://mint.host:3338/v1/kvac/melt/{method} ``` -The mint responds with the same structure as the quote response. +Body `MeltRequest` (baseline): +```json +{ + "quote": "", + "inputs": [, ...], + "outputs": [, ...], + "balance_proof": "", + "mac_proofs": ["", ...], + "range_proof": "" +} +``` -### Executing a Melt Quote +Constraints: +- inputs.len() > 0 +- 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. -To execute the melting process, the wallet calls the `POST /v1/kvac/melt/{method}` endpoint. +Balance constraint (delta): +- Let `fees = sum(input_fees)` per AC-02. +- Let `amount = quote_amount`. +- The BalanceProof MUST verify with `delta = (amount + fees)` NEGATED, i.e.: -```http -POST https://mint.host:3338/v1/kvac/melt/{method} +```text +inputs - outputs == -(amount + fees) ``` -> [!IMPORTANT] -> For methods that involve external payments (like Lightning), this call may block until the payment either succeeds or fails. This can take a long time. Make sure to **use no (or a very long) timeout when making this call**! +This ensures the Outputs encode (amount + fees) less than Inputs. -The wallet includes the following common data in its request: +## 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. +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 { - "quote": , - "inputs": [, ...], + "state": "", + "change": { + "fee_return": [, ], "outputs": [, ...], - "balance_proof": , - "mac_proofs": [, ...], - "range_proof": , - // Other method-specific options + "issued_macs": ["", ...], + "issuance_proofs": ["", ...] + } } ``` -where: -- `quote` is the quote ID from the previous step -- `inputs` are the same as in `swap_request.inputs` ([AC-03][AC-03]) -- `outputs` are the same as in `swap_request.outputs` ([AC-03][AC-03]) -- `balance_proof` proves that the difference `delta = inputs - outputs` is equal to **NEGATIVE** the amount in the previously generated quote. This -in English- means "the outputs encode exactly `quote.amount` more than the inputs". -- `mac_proofs` are the same as in `swap_request.mac_proofs` ([AC-03][AC-03]) -- `range_proof` is the same as in `swap_request.range_proof` ([AC-03][AC-03]) +## 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 -## Settings +## 9. Settings -The mint's settings for this NUT indicate the supported method-unit pairs for melting. They are part of the info response of the mint ([NUT-06][06]): +Advertised in [NUT-06][06] under key `AC5`: ```json { @@ -167,35 +169,28 @@ The mint's settings for this NUT indicate the supported method-unit pairs for me } ``` -`MeltMethodSetting` indicates supported `method` and `unit` pairs and additional settings of the mint. `disabled` indicates whether melting is disabled. - -`MeltMethodSetting` is of the form: - +`MeltMethodSetting`: ```json { - "method": , - "unit": , - "min_amount": , - "max_amount": , - "options": + "method": "", + "unit": "", + "min_amount": "", + "max_amount": "", + "options": "" } ``` -`min_amount` and `max_amount` indicate the minimum and maximum amount for an operation of this method-unit pair. `options` are method-specific and can be defined in method-specific NUTs. +## 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. -[00]: 00.md -[01]: 01.md -[02]: 02.md -[03]: 03.md -[04]: 04.md -[05]: 05.md +[AC-00]: AC00.md +[AC-01]: AC01.md +[AC-02]: AC02.md +[AC-03]: AC03.md [06]: 06.md -[07]: 07.md -[08]: 08.md -[09]: 09.md -[10]: 10.md -[11]: 11.md -[12]: 12.md [23]: 23.md [25]: 25.md -[AC03]: AC03.md From 970f840d2f4ab917bc5f5d5196102b63cea864b4 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 12:14:08 +0200 Subject: [PATCH 24/34] AC-03/04/05: Enforce exactly 2 inputs and 2 outputs per request - Add normative constraints: inputs.len()==2, outputs.len()==2, mac_proofs.len()==2 - Document decoy shaping via AC-06 zero-amount notes and zero-amount outputs (with RangeProof) - Keep unit/keyset/transcript constraints consistent across specs --- .../anonymous-credentials/AC03.md | 9 ++++++--- .../anonymous-credentials/AC04.md | 8 ++++++-- .../anonymous-credentials/AC05.md | 17 ++++++++--------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 7875af42..b678eeea 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -31,14 +31,17 @@ The client sends a `SwapRequest`: ``` Constraints: -- inputs.len() > 0 -- outputs.len() > 0 -- mac_proofs.len() == inputs.len() +- 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 SHOULD be unique within the request; duplicate tags across Outputs SHOULD be avoided. - 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 SHOULD obtain zero-amount Notes from [AC-06] 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. ## 3. Mint Verification Upon receiving `SwapRequest`, the Mint performs: diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index ced7cac1..a5417934 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -84,13 +84,17 @@ Body `MintRequest` (baseline): ``` Constraints: -- outputs.len() > 0 -- mac_proofs.len() == inputs.len() +- 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). - Each `outputs[i].id` MUST correspond to an active keyset (AC-01/AC-02). - `quote` 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 SHOULD obtain zero-amount Notes from [AC-06] 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 for the given keysets present in `inputs`. - The BalanceProof MUST verify with `delta = fees - quote_amount` so that `inputs - outputs == fees - quote_amount`. diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md index d29fb8d3..0036416a 100644 --- a/protocol-extensions/anonymous-credentials/AC05.md +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -100,22 +100,21 @@ Body `MeltRequest` (baseline): ``` Constraints: -- inputs.len() > 0 +- 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] 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. - Let `amount = quote_amount`. -- The BalanceProof MUST verify with `delta = (amount + fees)` NEGATED, i.e.: - -```text -inputs - outputs == -(amount + fees) -``` - -This ensures the Outputs encode (amount + fees) less than Inputs. - +- 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. From c9bb99902e946b7b6ed5991f330f84be02ba88a9 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 14:08:08 +0200 Subject: [PATCH 25/34] fix a couple of errors --- protocol-extensions/anonymous-credentials/AC04.md | 7 ++++--- protocol-extensions/anonymous-credentials/AC05.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index a5417934..8910c4dd 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -93,11 +93,11 @@ Constraints: - Transcript ordering for proofs MUST match the Mint (see §5). Decoy shaping rules: -- If the wallet has fewer than two Inputs, it SHOULD obtain zero-amount Notes from [AC-06] and include them as decoy Inputs. +- If the wallet has fewer than two Inputs, it SHOULD 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 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 for the given keysets present in `inputs`. -- The BalanceProof MUST verify with `delta = fees - quote_amount` so that `inputs - outputs == fees - quote_amount`. +- Let `keyset_fees = sum(input_fees)` per AC-02 for the given keysets present in `inputs`. +- 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 @@ -238,6 +238,7 @@ The Mint advertises supported method-unit pairs in [NUT-06][06] under key `AC4`: [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 diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md index 0036416a..606e0562 100644 --- a/protocol-extensions/anonymous-credentials/AC05.md +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -114,7 +114,7 @@ Decoy shaping rules: Balance constraint (delta): - Let `fees = sum(input_fees)` per AC-02. - Let `amount = quote_amount`. -- The BalanceProof MUST verify with `delta = - (amount + fees)` (i.e., `inputs - outputs == -(amount + fees)`). +- 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. From 1719923679bd14b3392687c819069c45a0e4c3bb Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 14:09:31 +0200 Subject: [PATCH 26/34] fix error --- protocol-extensions/anonymous-credentials/AC00.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 3f457126..41074818 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -176,8 +176,6 @@ Constraints: - IssuanceProof verification MUST use the correct MintPublicKey derived from the stated keyset id. - Nullifier reuse is prevented by the Mint; clients SHOULD treat spent Notes as invalid after a successful swap. -## 7. References - [AC-01]: AC01.md [AC-02]: AC02.md [AC-03]: AC03.md From 2a8bf55998261ac9eee4060d0509ae5ae127748b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 15:16:32 +0200 Subject: [PATCH 27/34] AC-07: Deterministic derivation for tag and KVAC attribute blinding factors (NUT-13 style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define BIP32 path: m/129372'/1'/{keyset_id_int}'/{counter}'/{c} with c∈{0:tag,1:amount_bf,2:script_bf} - Specify keyset-scoped counters, integer keyset id mapping, and interop rules - Add AC-00 note referencing AC-07 for deterministic wallets --- .../anonymous-credentials/AC00.md | 1 + .../anonymous-credentials/AC07.md | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 protocol-extensions/anonymous-credentials/AC07.md diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index 41074818..bda0cd23 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -171,6 +171,7 @@ Constraints: ## 6. Security and Policy Notes +- For deterministic wallets (see AC-07), `tag` and attribute blinding factors SHOULD be derived per AC-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) may reveal spending policy to the Mint via commitments; avoid embedding sensitive metadata. - IssuanceProof verification MUST use the correct MintPublicKey derived from the stated keyset id. diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md new file mode 100644 index 00000000..925e6ddf --- /dev/null +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -0,0 +1,122 @@ +# AC-07: Deterministic Derivation of Tag and Blinding Factors + +`optional` + +--- + +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 the approach of [NUT-13][13] and extends it from two secrets to three. 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. Derivation Path + +We adopt the NUT-13 style path with a separate “coin type” for KVAC to avoid collisions with classic Cashu secrets: + +- Purpose' = 129372' (UTF‑8 for 🥜) +- Coin type' = 1' (reserved for AC/KVAC; NUT‑13 uses 0') +- Keyset id' = keyset_id_int' (integer form of the keyset id; 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 +``` + +Rationale: +- Using 1' separates AC/KVAC derivations from NUT‑13’s base cashu derivations (0'). +- Using hardened components ensures derived private keys are valid and non-leaking. + +## 3. Keyset ID to Integer + +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); +``` + +## 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 := SerializeScalar(K_0) as 32‑byte hex (lowercase, no 0x). This becomes UnsignedNote.tag (64‑hex). +- amount_blind := SerializeScalar(K_1) as 32‑byte hex; pass as the optional blinding factor when constructing AmountAttribute. +- script_blind := SerializeScalar(K_2) as 32‑byte hex; pass as the optional blinding factor when constructing ScriptAttribute. + +Rust sketch: +```rust +fn derive_scalar_hex(path: &str) -> String { /* BIP32 hardened derivation → 32B → hex */ } +let tag_hex = derive_scalar_hex(&format!("m/129372'/1'/{keyset}'/{counter}'/0")); +let amount_bf_hex = derive_scalar_hex(&format!("m/129372'/1'/{keyset}'/{counter}'/1")); +let script_bf_hex = derive_scalar_hex(&format!("m/129372'/1'/{keyset}'/{counter}'/2")); +``` + +Constructing attributes (example): +```rust +let amount_attr = cashu_kvac::models::AmountAttribute::new(amount, Some(&amount_bf_hex)); +let script_attr = cashu_kvac::models::ScriptAttribute::new(script_bytes, Some(&script_bf_hex)); +let tag = tag_hex; // 64-hex +``` + +## 6. Recovery Behavior + +- Given mnemonic and the keyset id, wallets iterate counters in batches (e.g., 100 at a time) to regenerate candidate (tag, amount_blind, script_blind) tuples and corresponding Outputs. +- A future AC restore endpoint (akin to [NUT‑09][09]) can accept these reconstructed Outputs to reissue signatures or to return spent states. Until defined, wallets may use mint‑specific tooling or database dumps. + +## 7. 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. +- All hex encodings are lowercase, no 0x prefix. + +## 8. Security Considerations + +- Seed compromise endangers privacy of deterministic tags; protect mnemonic with best practices (hardware secure elements where possible). +- Keeping derivations keyset‑scoped avoids cross‑mint/keyset correlation; wallets MUST derive `keyset_id_int` per AC‑01/02. +- Deterministic tags are never sent to third parties except the Mint; however, Mints can observe tag uniqueness. Prefer not to embed sensitive metadata in scripts. + +## 9. Test Vectors (TBD) + +Add cross‑wallet test vectors once a reference implementation exists. + +[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 From 501db9836b2a7a53e1bc17413b986b8b4b43be50 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 15:32:57 +0200 Subject: [PATCH 28/34] AC-07: Switch to versioned derivation (align with NUT-13 v1/v2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v1: legacy BIP32 path (extended to 3 components) - v2: HMAC-SHA512 KDF keyed by BIP39 seed; no BIP32 usage - Component byte c∈{0,1,2} distinguishes tag, amount_bf, script_bf - Clarify counters, keyset_id mapping, security, and interop --- .../anonymous-credentials/AC07.md | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md index 925e6ddf..8eb9377f 100644 --- a/protocol-extensions/anonymous-credentials/AC07.md +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -13,7 +13,7 @@ This document defines a deterministic derivation scheme for three per-note secre - amount attribute blinding factor (for AmountAttribute) - script attribute blinding factor (for ScriptAttribute) -It mirrors the approach of [NUT-13][13] and extends it from two secrets to three. 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]). +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-SHA512 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 @@ -22,13 +22,19 @@ It mirrors the approach of [NUT-13][13] and extends it from two secrets to three - 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. Derivation Path +## 2. Versioned Deterministic Derivation -We adopt the NUT-13 style path with a separate “coin type” for KVAC to avoid collisions with classic Cashu secrets: +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-SHA512 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; NUT‑13 uses 0') -- Keyset id' = keyset_id_int' (integer form of the keyset id; see §3) +- 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 @@ -41,9 +47,37 @@ Derivation: m / 129372' / 1' / keyset_id_int' / counter' / c ``` -Rationale: -- Using 1' separates AC/KVAC derivations from NUT‑13’s base cashu derivations (0'). -- Using hardened components ensures derived private keys are valid and non-leaking. +The derived private key at this path is interpreted as a 32‑byte scalar modulo the Secp256k1 order n. + +### 2.2 v2 — HMAC-SHA512 KDF (no BIP32) + +For version 01 keysets, derive each component with an HMAC‑SHA512 keyed by the BIP39 seed. This mirrors the 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 = 8‑byte raw keyset id (big‑endian) +- counter_bytes = 4‑byte unsigned big‑endian counter +- c_bytes = 1‑byte component selector where c ∈ {0,1,2} +- label = ASCII string "Cashu_KDF_HMAC_SHA512" + +Compute for each c: + +``` +raw = HMAC_SHA512( key=seed, + data = label || keyset_id_bytes || counter_bytes || c_bytes ) +``` + +Obtain a 32‑byte scalar by reducing the left 32 bytes mod n (or the full 64‑byte output mod n): + +``` +scalar_c = bytes_to_scalar(raw[0..32]) 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 From 63ed873ea89c42ced876d7c0869f0679e9f14931 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 15:38:15 +0200 Subject: [PATCH 29/34] AC-07: Update v2 KDF to HMAC-SHA256 (keep full 32-byte digest) - Replace HMAC-SHA512 with HMAC-SHA256 for keyset v2 - Use label Cashu_KDF_HMAC_SHA256 and keep entire digest, reduce mod n - Maintain BIP32 only for v1 legacy --- protocol-extensions/anonymous-credentials/AC07.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md index 8eb9377f..1892f173 100644 --- a/protocol-extensions/anonymous-credentials/AC07.md +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -1,6 +1,6 @@ # AC-07: Deterministic Derivation of Tag and Blinding Factors -`optional` +`mandatory` --- @@ -49,28 +49,28 @@ 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-SHA512 KDF (no BIP32) +### 2.2 v2 — HMAC-SHA256 KDF (no BIP32) -For version 01 keysets, derive each component with an HMAC‑SHA512 keyed by the BIP39 seed. This mirrors the NUT‑13 v2 KDF and adds a component byte to obtain three distinct outputs. +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 = 8‑byte raw keyset id (big‑endian) - counter_bytes = 4‑byte unsigned big‑endian counter - c_bytes = 1‑byte component selector where c ∈ {0,1,2} -- label = ASCII string "Cashu_KDF_HMAC_SHA512" +- label = ASCII string "Cashu_KDF_HMAC_SHA256" Compute for each c: ``` -raw = HMAC_SHA512( key=seed, +raw = HMAC_SHA256( key=seed, data = label || keyset_id_bytes || counter_bytes || c_bytes ) ``` -Obtain a 32‑byte scalar by reducing the left 32 bytes mod n (or the full 64‑byte output mod n): +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 = bytes_to_scalar(raw[0..32]) mod n +scalar_c = int(raw) mod n if scalar_c == 0: scalar_c = 1 ``` From b6441122c1ca98e8de5ef7df68bce0d57bf152e0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 15:41:01 +0200 Subject: [PATCH 30/34] AC-07: Correct v2 KDF inputs per latest NUT-13 changes - keyset_id_bytes = raw keyset id (v1: 8 bytes, v2: 33 bytes) - counter_bytes = 8-byte big-endian unsigned - Keep HMAC-SHA256 label and full digest mod n --- protocol-extensions/anonymous-credentials/AC07.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md index 1892f173..ae86ac0b 100644 --- a/protocol-extensions/anonymous-credentials/AC07.md +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -13,7 +13,7 @@ This document defines a deterministic derivation scheme for three per-note secre - 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-SHA512 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]). +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 @@ -55,8 +55,8 @@ For version 01 keysets, derive each component with an HMAC‑SHA256 keyed by the Let: - seed = BIP39 seed (512-bit from mnemonic) -- keyset_id_bytes = 8‑byte raw keyset id (big‑endian) -- counter_bytes = 4‑byte unsigned big‑endian counter +- 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" @@ -79,7 +79,7 @@ Mapping: - c=1 → amount attribute blinding factor (hex) - c=2 → script attribute blinding factor (hex) -## 3. Keyset ID to Integer +## 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: @@ -92,6 +92,7 @@ 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. From 0737d019f3c1826008fa5eed5aadf628318b9215 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 22 Sep 2025 15:47:55 +0200 Subject: [PATCH 31/34] fixes --- .../anonymous-credentials/AC07.md | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md index ae86ac0b..8421be1a 100644 --- a/protocol-extensions/anonymous-credentials/AC07.md +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -26,7 +26,7 @@ It mirrors and extends [NUT-13][13] from two secrets to three and adopts the ver 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-SHA512 KDF; BIP32 MUST NOT be used +- 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. @@ -93,6 +93,7 @@ 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. @@ -106,22 +107,14 @@ For each (keyset_id, counter): - 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 := SerializeScalar(K_0) as 32‑byte hex (lowercase, no 0x). This becomes UnsignedNote.tag (64‑hex). -- amount_blind := SerializeScalar(K_1) as 32‑byte hex; pass as the optional blinding factor when constructing AmountAttribute. -- script_blind := SerializeScalar(K_2) as 32‑byte hex; pass as the optional blinding factor when constructing ScriptAttribute. - -Rust sketch: -```rust -fn derive_scalar_hex(path: &str) -> String { /* BIP32 hardened derivation → 32B → hex */ } -let tag_hex = derive_scalar_hex(&format!("m/129372'/1'/{keyset}'/{counter}'/0")); -let amount_bf_hex = derive_scalar_hex(&format!("m/129372'/1'/{keyset}'/{counter}'/1")); -let script_bf_hex = derive_scalar_hex(&format!("m/129372'/1'/{keyset}'/{counter}'/2")); -``` +- 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_bf_hex)); -let script_attr = cashu_kvac::models::ScriptAttribute::new(script_bytes, Some(&script_bf_hex)); +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 ``` @@ -138,15 +131,6 @@ let tag = tag_hex; // 64-hex - Decoy zero‑amount Outputs/Inputs MUST consume counters like normal Outputs. - All hex encodings are lowercase, no 0x prefix. -## 8. Security Considerations - -- Seed compromise endangers privacy of deterministic tags; protect mnemonic with best practices (hardware secure elements where possible). -- Keeping derivations keyset‑scoped avoids cross‑mint/keyset correlation; wallets MUST derive `keyset_id_int` per AC‑01/02. -- Deterministic tags are never sent to third parties except the Mint; however, Mints can observe tag uniqueness. Prefer not to embed sensitive metadata in scripts. - -## 9. Test Vectors (TBD) - -Add cross‑wallet test vectors once a reference implementation exists. [09]: ../09.md [13]: ../13.md From 024eeb062774ab89555bbd7f03a3205bfc1e64c3 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 29 Sep 2025 18:02:18 +0200 Subject: [PATCH 32/34] refinements --- .../anonymous-credentials/AC00.md | 37 ++++++++++--------- .../anonymous-credentials/AC01.md | 3 +- .../anonymous-credentials/AC02.md | 8 ++-- .../anonymous-credentials/AC03.md | 32 ++++++++-------- .../anonymous-credentials/AC04.md | 28 ++++++++------ .../anonymous-credentials/AC05.md | 17 +++++---- 6 files changed, 70 insertions(+), 55 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC00.md b/protocol-extensions/anonymous-credentials/AC00.md index bda0cd23..cbf0721d 100644 --- a/protocol-extensions/anonymous-credentials/AC00.md +++ b/protocol-extensions/anonymous-credentials/AC00.md @@ -4,11 +4,13 @@ --- +``` Status: Stable draft Scope: Shared data models used by AC-01/02/03/04/05/06 -Dependencies: cashu-kvac (KVAC), NUT-01/02/10/11/14 +Dependencies: cashu-kvac (KVAC), NUT-01/02 +``` -This document defines the core data models and normative constraints used by the Anonymous Credentials (AC) extensions. It mirrors the sectioning and constraint style used in AC-06 to improve clarity and conformance. +This document defines the core data models and normative constraints used by the Anonymous Credentials (AC) extensions. ## 1. Overview @@ -17,7 +19,7 @@ This document defines the core data models and normative constraints used by the 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 binary-encoded KVAC types. +- JSON examples may embed symbolic types like `cashu_kvac::{Struct}` to indicate json-encoded KVAC types. ## 2. Keys @@ -63,9 +65,9 @@ A client-created note not yet signed by the Mint. ``` 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. -- script: optional NUT-10 script, serialized as bytes and hex-encoded. +- 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. @@ -90,9 +92,9 @@ The Mint-visible view of an UnsignedNote. ``` Constraints: -- id MUST equal `unsigned_note.keyset_id`. +- id **MUST** equal `unsigned_note.keyset_id`. - commitments[0] is the amount commitment; commitments[1] is the script commitment. -- The pair (id, tag) SHOULD be unique in any single request. +- The pair (id, tag) **MUST** be unique. ### 3.3 MAC @@ -127,7 +129,7 @@ let mac = MAC::generate( Constraints: - Fields mirror UnsignedNote. -- `issuance_proof` MUST verify against the MintPublicKey for `keyset_id`. +- `issuance_proof` **MUST** verify against the MintPublicKey for `keyset_id`. ### 3.5 Input @@ -165,22 +167,23 @@ Constraints: ## 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/AC-02) at the time of issuance. -- Tags SHOULD be unique per request to avoid ambiguity and replay within the same batch. +- 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 (see AC-07), `tag` and attribute blinding factors SHOULD be derived per AC-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) may reveal spending policy to the Mint via commitments; avoid embedding sensitive metadata. -- IssuanceProof verification MUST use the correct MintPublicKey derived from the stated keyset id. -- Nullifier reuse is prevented by the Mint; clients SHOULD treat spent Notes as invalid after a successful swap. +- 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 diff --git a/protocol-extensions/anonymous-credentials/AC01.md b/protocol-extensions/anonymous-credentials/AC01.md index aed20b48..dbd7d0ef 100644 --- a/protocol-extensions/anonymous-credentials/AC01.md +++ b/protocol-extensions/anonymous-credentials/AC01.md @@ -3,10 +3,11 @@ `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. diff --git a/protocol-extensions/anonymous-credentials/AC02.md b/protocol-extensions/anonymous-credentials/AC02.md index 217bf9ea..ade3f4fc 100644 --- a/protocol-extensions/anonymous-credentials/AC02.md +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -3,10 +3,11 @@ `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. @@ -39,8 +40,8 @@ Field semantics: - `input_fee_ppk`: optional per-thousand fee applied per input in swap flows. If omitted, treated as 0. Constraints: -- Clients MUST treat `active=false` keysets as valid for Inputs but NOT for new Outputs. -- Fee calculation: in AC-03 balance verification, the delta MUST equal the sum of per-input fees (see AC-03 §3.6). If fractional fees arise, Mints MUST define rounding (RECOMMENDED: floor to integer units); clients MUST match the Mint policy. +- Clients **MUST** treat `active=false` keysets as valid for Inputs but NOT for new Outputs. +- Fee calculation: input dependant fees as defined in [NUT-02](02) §3.6. ## 3. Client Behavior @@ -58,4 +59,5 @@ Constraints: - 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 index b678eeea..33debf22 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -4,9 +4,11 @@ --- +``` Status: Stable draft Scope: Spend existing Notes (Inputs) to obtain signatures on new UnsignedNotes (Outputs) -Dependencies: AC-00, AC-01, AC-02, NUT-10/11/14 +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. @@ -34,14 +36,14 @@ 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 SHOULD be unique within the request; duplicate tags across Outputs SHOULD be avoided. -- Transcript ordering for proofs MUST be consistent between client and Mint (see §3). +- 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 SHOULD obtain zero-amount Notes from [AC-06] 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. +- 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: @@ -51,12 +53,12 @@ Upon receiving `SwapRequest`, the Mint performs: - Verify each Output’s keyset id is active. 2) Script verification -- For each Input, extract `script`/`witness` if present and verify per [NUT-11] or [NUT-14]. -- Reject on policy failure. +- 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). +- Reject if nullifier already seen (double-spend prevention). 4) Initialize transcript @@ -86,7 +88,7 @@ cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range 7) Verify BalanceProof - Let `delta` be the fee-adjusted difference. For no-fee keysets, `delta = 0`. -- With per-input fees (see AC-02), `delta = sum(input_fees)`. +- With per-input fees (see [AC-02](AC02)), `delta = sum(input_fees)`. ```rust cashu_kvac::kvac::BalanceProof::verify( @@ -129,7 +131,7 @@ cashu_kvac::kvac::IssuanceProof::verify( ); ``` -If verification succeeds, the wallet combines `UnsignedNote`, `MAC`, and `IssuanceProof` into a `Note` (AC-00). +If verification succeeds, the wallet combines `UnsignedNote`, `MAC`, and `IssuanceProof` into a `Note` ([AC-00](AC00)). ## 6. Errors @@ -150,9 +152,9 @@ If verification succeeds, the wallet combines `UnsignedNote`, `MAC`, and `Issuan ## 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`. +- 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 diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index 8910c4dd..c7c8909c 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -4,11 +4,15 @@ --- +``` 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. This document mirrors the AC-06 structure for clarity and interop. +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 @@ -87,14 +91,14 @@ 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). -- Each `outputs[i].id` MUST correspond to an active keyset (AC-01/AC-02). -- `quote` MUST be known, paid, and unexpired. -- Transcript ordering for proofs MUST match the Mint (see §5). +- 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 SHOULD 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 SHOULD include a decoy Output encoding amount 0. The decoy Output MUST be covered by the RangeProof like any other Output. +- 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 = sum(input_fees)` per AC-02 for the given keysets present in `inputs`. - The BalanceProof MUST verify with `delta = keyset_fees - quote_amount` so that `inputs - outputs == keyset_fees - quote_amount`. @@ -114,7 +118,7 @@ Upon receiving `MintRequest`, the Mint performs: 3) Script verification - For each Input, verify `script`/`witness` per [NUT-11] or [NUT-14]. Reject on policy failure. -4) Nullifier checks +4) Nullifier checks (for each Input) - Derive nullifier from `input.randomized_commitments.Cv` and reject if already seen. 5) Initialize transcript @@ -181,7 +185,7 @@ let issuance_proof = cashu_kvac::kvac::IssuanceProof::create( ## 7. Client Verification and Note Assembly -As in AC-03/AC-06: verify `IssuanceProof` per Output and assemble `Note`s if verification succeeds. +As in [AC-03](AC03)/[AC-06](AC06): verify `IssuanceProof` per Output and assemble `Note`s if verification succeeds. ## 8. Errors @@ -230,9 +234,9 @@ The Mint advertises supported method-unit pairs in [NUT-06][06] under key `AC4`: ## 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 SHOULD be unique within the request to avoid ambiguity/replay. +- 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 diff --git a/protocol-extensions/anonymous-credentials/AC05.md b/protocol-extensions/anonymous-credentials/AC05.md index 606e0562..899506a9 100644 --- a/protocol-extensions/anonymous-credentials/AC05.md +++ b/protocol-extensions/anonymous-credentials/AC05.md @@ -3,12 +3,15 @@ `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. This document mirrors AC-06 structure and aligns fee-return handling with proof verification. +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 @@ -108,11 +111,11 @@ Constraints: - 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] and include them as decoy Inputs. +- 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. +- 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 @@ -120,7 +123,7 @@ Balance constraint (delta): 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. +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. @@ -181,9 +184,9 @@ Advertised in [NUT-06][06] under key `AC5`: ## 10. Security and Policy Notes -- Calls may block while external payment executes; clients SHOULD use long/disabled timeouts. +- 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. +- Clients **SHOULD** refresh active keysets before constructing Outputs. - Avoid reusing tags within a batch; ensure strong randomness. [AC-00]: AC00.md From 394634c28ea1c57eac8d54ee7afd096858c2881e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 30 Sep 2025 00:52:34 +0200 Subject: [PATCH 33/34] fixes --- protocol-extensions/anonymous-credentials/AC07.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC07.md b/protocol-extensions/anonymous-credentials/AC07.md index 8421be1a..e1bc517b 100644 --- a/protocol-extensions/anonymous-credentials/AC07.md +++ b/protocol-extensions/anonymous-credentials/AC07.md @@ -118,18 +118,12 @@ let script_attr = cashu_kvac::models::ScriptAttribute::new(script_bytes, Some(&s let tag = tag_hex; // 64-hex ``` -## 6. Recovery Behavior +## 6. Constraints and Interop Rules -- Given mnemonic and the keyset id, wallets iterate counters in batches (e.g., 100 at a time) to regenerate candidate (tag, amount_blind, script_blind) tuples and corresponding Outputs. -- A future AC restore endpoint (akin to [NUT‑09][09]) can accept these reconstructed Outputs to reissue signatures or to return spent states. Until defined, wallets may use mint‑specific tooling or database dumps. - -## 7. 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. +- 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. -- All hex encodings are lowercase, no 0x prefix. +- Decoy zero‑amount Outputs/Inputs **MUST** consume counters like normal Outputs. [09]: ../09.md From 31758e7f53f001294aba6b147d02d37d7e9fbc10 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 1 Oct 2025 09:59:54 +0200 Subject: [PATCH 34/34] fixed input fee --- protocol-extensions/anonymous-credentials/AC02.md | 5 ++--- protocol-extensions/anonymous-credentials/AC03.md | 2 +- protocol-extensions/anonymous-credentials/AC04.md | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/protocol-extensions/anonymous-credentials/AC02.md b/protocol-extensions/anonymous-credentials/AC02.md index ade3f4fc..b98f0a53 100644 --- a/protocol-extensions/anonymous-credentials/AC02.md +++ b/protocol-extensions/anonymous-credentials/AC02.md @@ -26,7 +26,7 @@ KVAC keysets largely follow NUT-02. This document specifies the endpoint and wir "unit": "", "active": true, "final_expiry": "", - "input_fee_ppk": "" + "input_fee_fixed": "" } ] } @@ -37,11 +37,10 @@ Field semantics: - `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_ppk`: optional per-thousand fee applied per input in swap flows. If omitted, treated as 0. +- `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. -- Fee calculation: input dependant fees as defined in [NUT-02](02) §3.6. ## 3. Client Behavior diff --git a/protocol-extensions/anonymous-credentials/AC03.md b/protocol-extensions/anonymous-credentials/AC03.md index 33debf22..bc7cff13 100644 --- a/protocol-extensions/anonymous-credentials/AC03.md +++ b/protocol-extensions/anonymous-credentials/AC03.md @@ -88,7 +88,7 @@ cashu_kvac::kvac::RangeProof::verify(&mut transcript, &amount_commitments, range 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 = sum(input_fees)`. +- With per-input fees (see [AC-02](AC02)), `delta = input_fee`. ```rust cashu_kvac::kvac::BalanceProof::verify( diff --git a/protocol-extensions/anonymous-credentials/AC04.md b/protocol-extensions/anonymous-credentials/AC04.md index c7c8909c..5558fdf9 100644 --- a/protocol-extensions/anonymous-credentials/AC04.md +++ b/protocol-extensions/anonymous-credentials/AC04.md @@ -100,7 +100,7 @@ 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 = sum(input_fees)` per AC-02 for the given keysets present in `inputs`. +- 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).