From 38bc16a50f40fccf723cfa205599c66592d459be Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 2 Oct 2025 09:55:45 +0200 Subject: [PATCH 01/21] NUT-26: Pay-to-Blinded-Key (P2BK): formalize public key blinding atop NUT-11; add spec and README entry; ensure proper Markdown formatting --- 26.md | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 +- 2 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 26.md diff --git a/26.md b/26.md new file mode 100644 index 00000000..71afa1a9 --- /dev/null +++ b/26.md @@ -0,0 +1,183 @@ +# NUT-26: Pay-to-Blinded-Key (P2BK) + +`optional` + +`depends on: NUT-11` + +--- + +## Summary + +This NUT defines Pay-to-Blinded-Key (P2BK), a wallet-to-wallet privacy extension of [NUT-11][11] (P2PK). P2BK blinds the public keys contained in a P2PK Secret so the mint cannot learn the true recipient key material when issuing or redeeming ecash. The scheme requires no changes to mint behavior: wallets MUST normalize P2BK Secrets to standard P2PK Secrets before sending requests to a mint. All Schnorr signing and verification at the mint remains exactly as specified in NUT-11. + +## Motivation + +- In NUT-11, the receiver public key(s) appear in cleartext in the Secret. On redemption, the mint learns the long-term public key(s) that control the ecash. +- P2BK hides those keys from the mint by blinding each receiver public key P with a random scalar r: P' = P + r·G. The receiver later derives the corresponding private key k = p + r (mod n) to sign per NUT-11. The mint only ever sees normal P2PK Secrets and signatures. + +## Definitions and notation + +- Curve: secp256k1 with base point G and order n. +- Unblinded pubkey (compressed hex): P. +- Blinding scalar: r with 1 ≤ r ≤ n − 1. +- Blinded pubkey: P' = P + r·G (compressed hex). +- Derived private key: k = (p + r) mod n. + +## Wire format + +Secret kind: "P2BK" + +P2BK introduces a new well-known Secret kind. It has the same structure as a NUT-10/11 P2PK Secret except for the kind string and an additional tag that carries the blinding scalars: + +```json +[ + "P2BK", + { + "nonce": "", + "data": "", + "tags": [ + ["sigflag", "SIG_INPUTS"], + ["pubkeys", "", ""], + ["n_sigs", ""], + ["locktime", ""], + ["refund", "", ""], + ["n_sigs_refund", ""], + ["r", "", "", ""] + ] + } +] +``` + +Notes: + +- data holds the primary blinded pubkey P0'. +- pubkeys holds zero or more additional blinded pubkeys P1', P2', … for locktime multisig. +- refund holds zero or more blinded refund pubkeys R0', R1', … for refund multisig. +- r is an array of the blinding scalars as fixed-length 32-byte hex strings (lowercase) without prefix. The order of r MUST match the concatenation order of all blinded keys present in the Secret: + 1) the primary lock key data, then 2) any keys in pubkeys, then 3) any keys in refund. + For example, if there are m lock keys in total (1 in data + pubkeys.length) and t refund keys, the r tag contains m + t entries: [r0, r1, …, r(m−1), r(m), …, r(m+t−1)]. + +## Normalization for mints (wallet MUST) + +Mints do not understand "P2BK". Wallets MUST transform a P2BK Secret into a standard P2PK Secret before using it in any request to a mint (swap/melt/etc.). Normalization is purely a string transformation: + +1. Parse the JSON Secret [kind, meta]. If kind != "P2BK", return unchanged. +2. Remove the r tag from meta.tags if present. +3. Replace kind with "P2PK". +4. Re-serialize to JSON string. This string is the message to be signed for NUT-11 witnesses and is what wallets MUST send to mints. + +No other fields are changed. In particular, data, pubkeys, refund remain the blinded pubkeys P', R'. + +## Signing and verification (unchanged at the mint) + +- Signature scheme, sigflag semantics, multisig, locktime/refund behavior, and message aggregation (SIG_INPUTS, SIG_ALL) are as in NUT-11. All signatures are Schnorr signatures over the SHA256 hash of the normalized P2PK Secret string (and any aggregated outputs per NUT-11). +- Mints MUST NOT receive r values and MUST NOT receive Secrets of kind "P2BK". Wallets MUST perform normalization before sending any request to a mint. + +## Wallet behavior + +### Sender (building P2BK) + +- For each recipient lock/refund pubkey Pi, choose fresh ri ∈ [1, n − 1]. +- Compute Pi' = Pi + ri·G. +- Construct a P2PK Secret as in NUT-11 but replace all pubkeys with their blinded forms. Blind the message as usual to obtain outputs and promises from the mint. +- After unblinding signatures to proofs, rewrite the proof Secret to kind "P2BK" and append a tag ["r", r0, r1, …] carrying all ris in the order defined above. Deliver these P2BK proofs to the receiver. + +### Receiver (spending P2BK) + +- Parse the P2BK Secret and extract r values. +- Derive the signing private keys ki = (pi + ri) mod n for each of its keys pi that are expected to sign under NUT-11 rules (respecting locktime and multisig tags). +- Normalize the Secret to P2PK (strip r; set kind = "P2PK"). +- Produce Schnorr signature(s) over the normalized P2PK Secret string according to the applicable sigflag and multisig rules. +- Submit the proofs to the mint. The mint verifies exactly as in NUT-11. + +## Determinism and canonicalization + +- The r tag MUST contain lowercase hex and be 64 hex chars per entry (32 bytes), matching the conventional encoding used for scalars elsewhere in Cashu. +- Wallets SHOULD ignore duplicate r entries and MUST deduplicate derived signing keys if the same ri appears multiple times. Ordering rules still apply to allow position-based mapping. +- Wallets MUST NOT include an empty r tag; if present, mints will never see it due to normalization, but receivers SHOULD treat an empty r tag as equivalent to absence (no blinding information). + +## Constraints and errors + +- ri MUST satisfy 1 ≤ ri ≤ n − 1. Senders MUST use uniformly random ri and MUST NOT reuse ri across different blinded keys. +- The derived private key ki = (pi + ri) mod n MUST be non-zero. If ki = 0 (probability ~1/n), the receiver cannot sign; the remedy is to request a re-send with different randomness. +- Wallets MUST treat any P2BK Secret missing the r tag as a non-blinded P2PK Secret (i.e., treat like NUT-11 where P' are just arbitrary pubkeys). + +## Security considerations + +- Mint Privacy: Mints never see r nor unblinded P. They see only blinded pubkeys P' inside a standard P2PK Secret, and standard Schnorr signatures. This prevents mints from trivially linking P' to a receiver's long-term P if P is reused elsewhere. +- Freshness: Reusing `r`s across payments or across keys enables linkability across P' values. Senders MUST generate fresh `r`s for every blinded key instance. +- Receiver Key Hygiene: Receivers SHOULD avoid reusing the same base key p across many P2BK payments if linkability via off-chain correlation is a concern, although the mint cannot derive P. +- Metadata Leakage: The presence of P' alone does not reveal P without knowledge of r or p. The r tag is only exchanged wallet-to-wallet and MUST NOT be forwarded to mints or third parties. + +## Compatibility + +- Backwards Compatibility: Mints require no changes. Wallets that do not understand P2BK will fail to parse the Secret kind and thus cannot accept such proofs. Senders SHOULD only send P2BK proofs to receivers known to support NUT-26. +- Interop with NUT-11: All NUT-11 tag semantics (sigflag, n_sigs, locktime, refund, n_sigs_refund) apply unchanged when used with blinded keys. + +## Mint info + +No new mint capability flag is required because mint behavior is unchanged. [NUT-06][06] remains the source of P2PK support (NUT-11). Wallet capability advertisement is out-of-scope for this NUT. + +## Worked example + +Receiver base pubkey P (compressed): + +``` +033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e +``` + +Sender samples r0 (hex): + +``` +1f3a... (64 hex chars) +``` + +Blinded lock key: + +``` +P0' = P + r0·G +``` + +P2BK Secret (SIG_INPUTS, no multisig): + +```json +[ + "P2BK", + { + "nonce": "da62796403af76c80cd6ce9153ed3746", + "data": "03ab...", + "tags": [["sigflag", "SIG_INPUTS"], ["r", "1f3a..."]] + } +] +``` + +Normalization for mint/signing (wallet-side): + +```json +[ + "P2PK", + { + "nonce": "da62796403af76c80cd6ce9153ed3746", + "data": "03ab...", + "tags": [["sigflag", "SIG_INPUTS"]] + } +] +``` + +Receiver derives k0 = p + r0 (mod n) and signs the normalized P2PK Secret per NUT-11. + +## Implementation notes (informative) + +- A reference TypeScript implementation is available in cashu-ts, which: + - Creates blinded pubkeys and tracks the corresponding r values in order. + - Emits proofs to receivers with Secret kind "P2BK" and an r tag. + - Normalizes Secrets back to kind "P2PK" (stripping r) for all mint interactions and hashes/signs over that normalized string. + - Derives signing keys k = p + r from provided base keys and r values when spending. + +[00]: 00.md +[03]: 03.md +[05]: 05.md +[06]: 06.md +[08]: 08.md +[10]: 10.md +[11]: 11.md diff --git a/README.md b/README.md index 48216bef..8a71bdb8 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,6 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [05][05] | Melting tokens | | [06][06] | Mint info | -### Optional - -| # | Description | Wallets | Mints | -| -------- | --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------ | | [07][07] | Token state check | [Nutshell][py], [Nutstash][ns], [cashu-ts][ts], [cdk-cli], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd], [nutmix] | | [08][08] | Overpaid Lightning fees | [Nutshell][py], [Nutstash][ns], [cashu-ts][ts], [cdk-cli], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd], [nutmix] | | [09][09] | Signature restore | [Nutshell][py], [cdk-cli], [Cashu.me][cashume], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd] | @@ -41,6 +37,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [23][23] | Payment Method: BOLT11 | [Nutshell][py], [cdk-cli] | [Nutshell][py], [cdk-mintd], [nutmix] | | [24][24] | HTTP 402 Payment Required | - | - | | [25][25] | Payment Method: BOLT12 | [cdk-cli], [cashu-ts][ts] | [cdk-mintd] | +| [26][26] | Pay-to-Blinded-Key (P2BK) | [cashu-ts][ts] | - | #### Wallets: From 850d8a01d97894f46a3e7a80034827032a4000b8 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:40:09 +0200 Subject: [PATCH 02/21] Apply suggestions from code review Co-authored-by: Rob Woodgate --- 26.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/26.md b/26.md index 71afa1a9..971b7797 100644 --- a/26.md +++ b/26.md @@ -62,7 +62,7 @@ Notes: Mints do not understand "P2BK". Wallets MUST transform a P2BK Secret into a standard P2PK Secret before using it in any request to a mint (swap/melt/etc.). Normalization is purely a string transformation: 1. Parse the JSON Secret [kind, meta]. If kind != "P2BK", return unchanged. -2. Remove the r tag from meta.tags if present. +2. Remove the r tag from meta.tags if present. If no tags remain, tags MUST be kept as an empty array (i.e: `tags: []`_ 3. Replace kind with "P2PK". 4. Re-serialize to JSON string. This string is the message to be signed for NUT-11 witnesses and is what wallets MUST send to mints. @@ -79,14 +79,14 @@ No other fields are changed. In particular, data, pubkeys, refund remain the bli - For each recipient lock/refund pubkey Pi, choose fresh ri ∈ [1, n − 1]. - Compute Pi' = Pi + ri·G. -- Construct a P2PK Secret as in NUT-11 but replace all pubkeys with their blinded forms. Blind the message as usual to obtain outputs and promises from the mint. +- Construct a P2PK Secret as in NUT-11 but replace all pubkeys with their blinded forms. The `tags` entry MUST be present, even if empty (i.e: `tags: []`). Blind the message as usual to obtain outputs and promises from the mint. - After unblinding signatures to proofs, rewrite the proof Secret to kind "P2BK" and append a tag ["r", r0, r1, …] carrying all ris in the order defined above. Deliver these P2BK proofs to the receiver. ### Receiver (spending P2BK) - Parse the P2BK Secret and extract r values. - Derive the signing private keys ki = (pi + ri) mod n for each of its keys pi that are expected to sign under NUT-11 rules (respecting locktime and multisig tags). -- Normalize the Secret to P2PK (strip r; set kind = "P2PK"). +- Normalize the Secret to P2PK (strip r; set kind = "P2PK"; ensure `tags` entry is retained if empty). - Produce Schnorr signature(s) over the normalized P2PK Secret string according to the applicable sigflag and multisig rules. - Submit the proofs to the mint. The mint verifies exactly as in NUT-11. @@ -112,7 +112,7 @@ No other fields are changed. In particular, data, pubkeys, refund remain the bli ## Compatibility - Backwards Compatibility: Mints require no changes. Wallets that do not understand P2BK will fail to parse the Secret kind and thus cannot accept such proofs. Senders SHOULD only send P2BK proofs to receivers known to support NUT-26. -- Interop with NUT-11: All NUT-11 tag semantics (sigflag, n_sigs, locktime, refund, n_sigs_refund) apply unchanged when used with blinded keys. +- Interop with NUT-11: All NUT-11 tag semantics (sigflag, n_sigs, locktime, refund, n_sigs_refund) apply unchanged when used with blinded keys. NUT-11 secrets intended for P2BK blinding MUST include the `tags` entry, even if empty (ie: `tags: []`) ## Mint info From 1b97691dfe28aa7b44d7b7fb9eb5d657ffe54597 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 3 Oct 2025 14:11:01 +0200 Subject: [PATCH 03/21] fix readme table --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8a71bdb8..4892b8fc 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [05][05] | Melting tokens | | [06][06] | Mint info | +### Optional +| # | Description | Wallets | Mints | +| -------- | --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------ | | [07][07] | Token state check | [Nutshell][py], [Nutstash][ns], [cashu-ts][ts], [cdk-cli], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd], [nutmix] | | [08][08] | Overpaid Lightning fees | [Nutshell][py], [Nutstash][ns], [cashu-ts][ts], [cdk-cli], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd], [nutmix] | | [09][09] | Signature restore | [Nutshell][py], [cdk-cli], [Cashu.me][cashume], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd] | From a21ceb31d2d26b1b4ac0636b1e8a97f5d7be37f9 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sun, 5 Oct 2025 15:39:41 +0200 Subject: [PATCH 04/21] NUT-26: document Schnorr x-only key derivation for blinded keys (try k=p+r and k=-p+r); fix normalization to preserve empty tags; clarify key encodings. Resolves -+p issue per PR #291 discussion. --- 26.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/26.md b/26.md index 971b7797..60ee76e5 100644 --- a/26.md +++ b/26.md @@ -13,15 +13,17 @@ This NUT defines Pay-to-Blinded-Key (P2BK), a wallet-to-wallet privacy extension ## Motivation - In NUT-11, the receiver public key(s) appear in cleartext in the Secret. On redemption, the mint learns the long-term public key(s) that control the ecash. -- P2BK hides those keys from the mint by blinding each receiver public key P with a random scalar r: P' = P + r·G. The receiver later derives the corresponding private key k = p + r (mod n) to sign per NUT-11. The mint only ever sees normal P2PK Secrets and signatures. +- P2BK hides those keys from the mint by blinding each receiver public key P with a random scalar r: P' = P + r·G. The receiver later derives a corresponding private key k from p and r (see below) to sign per NUT-11. The mint only ever sees normal P2PK Secrets and signatures. ## Definitions and notation - Curve: secp256k1 with base point G and order n. -- Unblinded pubkey (compressed hex): P. +- Unblinded pubkey: P. + - For SEC1-style keys, P is a 33-byte compressed SEC1 pubkey (hex). + - For BIP340 Schnorr/x-only contexts (e.g., Nostr), P is an x-only 32-byte pubkey (hex) with even-Y parity implied by BIP340. - Blinding scalar: r with 1 ≤ r ≤ n − 1. -- Blinded pubkey: P' = P + r·G (compressed hex). -- Derived private key: k = (p + r) mod n. +- Blinded pubkey: P' = P + r·G. Encoding matches P: compressed SEC1 (33-byte hex) for SEC1 keys; x-only (32-byte hex, even-Y) for BIP340 contexts. +- Derived private key: for SEC1 compressed pubkeys, k = (p + r) mod n. For x-only Schnorr pubkeys (e.g., Nostr), wallets MUST compute k1 = (p + r) mod n and k2 = (-p + r) mod n and select the candidate whose derived even-Y public key equals the blinded pubkey P'. ## Wire format @@ -62,7 +64,7 @@ Notes: Mints do not understand "P2BK". Wallets MUST transform a P2BK Secret into a standard P2PK Secret before using it in any request to a mint (swap/melt/etc.). Normalization is purely a string transformation: 1. Parse the JSON Secret [kind, meta]. If kind != "P2BK", return unchanged. -2. Remove the r tag from meta.tags if present. If no tags remain, tags MUST be kept as an empty array (i.e: `tags: []`_ +2. Remove the r tag from meta.tags if present. If no tags remain, tags MUST be kept as an empty array (i.e: `tags: []`). 3. Replace kind with "P2PK". 4. Re-serialize to JSON string. This string is the message to be signed for NUT-11 witnesses and is what wallets MUST send to mints. @@ -85,7 +87,9 @@ No other fields are changed. In particular, data, pubkeys, refund remain the bli ### Receiver (spending P2BK) - Parse the P2BK Secret and extract r values. -- Derive the signing private keys ki = (pi + ri) mod n for each of its keys pi that are expected to sign under NUT-11 rules (respecting locktime and multisig tags). +- Derive the signing private keys as follows: + - For SEC1 compressed pubkeys, ki = (pi + ri) mod n. + - For BIP340 Schnorr/x-only pubkeys, compute k1 = (pi + ri) mod n and k2 = (-pi + ri) mod n; select the candidate whose derived even-Y public key equals the blinded pubkey P'i in the Secret. - Normalize the Secret to P2PK (strip r; set kind = "P2PK"; ensure `tags` entry is retained if empty). - Produce Schnorr signature(s) over the normalized P2PK Secret string according to the applicable sigflag and multisig rules. - Submit the proofs to the mint. The mint verifies exactly as in NUT-11. From 8fccf246254e42f5408a0b425fe76f8fe9ef72dd Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sun, 5 Oct 2025 15:40:13 +0200 Subject: [PATCH 05/21] NUT-26: add implementation note on Schnorr-derived key selection (even-Y match). --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index 60ee76e5..a8bd8dc4 100644 --- a/26.md +++ b/26.md @@ -176,7 +176,7 @@ Receiver derives k0 = p + r0 (mod n) and signs the normalized P2PK Secret per NU - Creates blinded pubkeys and tracks the corresponding r values in order. - Emits proofs to receivers with Secret kind "P2BK" and an r tag. - Normalizes Secrets back to kind "P2PK" (stripping r) for all mint interactions and hashes/signs over that normalized string. - - Derives signing keys k = p + r from provided base keys and r values when spending. + - Derives signing keys: for SEC1, k = p + r; for BIP340/x-only, try k = p + r and k = -p + r and pick the one whose even-Y pubkey equals P'. [00]: 00.md [03]: 03.md From b51ed0c9f11043df94ae7609b8732a48de27ff24 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sun, 5 Oct 2025 15:40:32 +0200 Subject: [PATCH 06/21] NUT-26: clarify worked example derivation for BIP340 vs SEC1 keys; normalization must preserve tags: [].} --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index a8bd8dc4..052a9de9 100644 --- a/26.md +++ b/26.md @@ -168,7 +168,7 @@ Normalization for mint/signing (wallet-side): ] ``` -Receiver derives k0 = p + r0 (mod n) and signs the normalized P2PK Secret per NUT-11. +Receiver derives k0 from p and r as per key type: for SEC1, k0 = (p + r0) mod n; for BIP340/x-only, choose between (p + r0) mod n and (-p + r0) mod n by matching the even-Y pubkey to P0'. ## Implementation notes (informative) From ea6888e552f1d6af786ab93cd3f53b550347f790 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sun, 5 Oct 2025 16:29:58 +0200 Subject: [PATCH 07/21] prettier --- 26.md | 9 ++++++--- README.md | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/26.md b/26.md index 052a9de9..9bc3102a 100644 --- a/26.md +++ b/26.md @@ -56,8 +56,8 @@ Notes: - pubkeys holds zero or more additional blinded pubkeys P1', P2', … for locktime multisig. - refund holds zero or more blinded refund pubkeys R0', R1', … for refund multisig. - r is an array of the blinding scalars as fixed-length 32-byte hex strings (lowercase) without prefix. The order of r MUST match the concatenation order of all blinded keys present in the Secret: - 1) the primary lock key data, then 2) any keys in pubkeys, then 3) any keys in refund. - For example, if there are m lock keys in total (1 in data + pubkeys.length) and t refund keys, the r tag contains m + t entries: [r0, r1, …, r(m−1), r(m), …, r(m+t−1)]. + 1. the primary lock key data, then 2) any keys in pubkeys, then 3) any keys in refund. + For example, if there are m lock keys in total (1 in data + pubkeys.length) and t refund keys, the r tag contains m + t entries: [r0, r1, …, r(m−1), r(m), …, r(m+t−1)]. ## Normalization for mints (wallet MUST) @@ -150,7 +150,10 @@ P2BK Secret (SIG_INPUTS, no multisig): { "nonce": "da62796403af76c80cd6ce9153ed3746", "data": "03ab...", - "tags": [["sigflag", "SIG_INPUTS"], ["r", "1f3a..."]] + "tags": [ + ["sigflag", "SIG_INPUTS"], + ["r", "1f3a..."] + ] } ] ``` diff --git a/README.md b/README.md index 4892b8fc..e139615b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [06][06] | Mint info | ### Optional + | # | Description | Wallets | Mints | | -------- | --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------ | | [07][07] | Token state check | [Nutshell][py], [Nutstash][ns], [cashu-ts][ts], [cdk-cli], [Minibits], [macadamia] | [Nutshell][py], [cdk-mintd], [nutmix] | From 9127bb5bf1bbae51604ec8a2240e0d06a237aed3 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Mon, 6 Oct 2025 07:37:38 +0200 Subject: [PATCH 08/21] Update 26.md Co-authored-by: Rob Woodgate --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index 9bc3102a..84ce6e35 100644 --- a/26.md +++ b/26.md @@ -22,7 +22,7 @@ This NUT defines Pay-to-Blinded-Key (P2BK), a wallet-to-wallet privacy extension - For SEC1-style keys, P is a 33-byte compressed SEC1 pubkey (hex). - For BIP340 Schnorr/x-only contexts (e.g., Nostr), P is an x-only 32-byte pubkey (hex) with even-Y parity implied by BIP340. - Blinding scalar: r with 1 ≤ r ≤ n − 1. -- Blinded pubkey: P' = P + r·G. Encoding matches P: compressed SEC1 (33-byte hex) for SEC1 keys; x-only (32-byte hex, even-Y) for BIP340 contexts. +- Blinded pubkey: P' = P + r·G. Encoding is compressed SEC1 (33-byte hex). - Derived private key: for SEC1 compressed pubkeys, k = (p + r) mod n. For x-only Schnorr pubkeys (e.g., Nostr), wallets MUST compute k1 = (p + r) mod n and k2 = (-p + r) mod n and select the candidate whose derived even-Y public key equals the blinded pubkey P'. ## Wire format From e38faa0f8690bd8c7b6f35b2ab72605dfc8963ae Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Mon, 6 Oct 2025 07:37:56 +0200 Subject: [PATCH 09/21] Update 26.md Co-authored-by: Rob Woodgate --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index 84ce6e35..e8bd1138 100644 --- a/26.md +++ b/26.md @@ -20,7 +20,7 @@ This NUT defines Pay-to-Blinded-Key (P2BK), a wallet-to-wallet privacy extension - Curve: secp256k1 with base point G and order n. - Unblinded pubkey: P. - For SEC1-style keys, P is a 33-byte compressed SEC1 pubkey (hex). - - For BIP340 Schnorr/x-only contexts (e.g., Nostr), P is an x-only 32-byte pubkey (hex) with even-Y parity implied by BIP340. + - For BIP340 Schnorr/x-only contexts (e.g., Nostr), P is an x-only 32-byte pubkey (hex) with even-Y parity (per BIP340), prefixed with '02' for Cashu exosystem interoperability. - Blinding scalar: r with 1 ≤ r ≤ n − 1. - Blinded pubkey: P' = P + r·G. Encoding is compressed SEC1 (33-byte hex). - Derived private key: for SEC1 compressed pubkeys, k = (p + r) mod n. For x-only Schnorr pubkeys (e.g., Nostr), wallets MUST compute k1 = (p + r) mod n and k2 = (-p + r) mod n and select the candidate whose derived even-Y public key equals the blinded pubkey P'. From 77497d18224a566e7e5ed4ac669b2f63de67794e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 9 Oct 2025 17:17:54 +0200 Subject: [PATCH 10/21] NUT-26: document p2pk_r metadata and token carriage --- 26.md | 235 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 107 insertions(+), 128 deletions(-) diff --git a/26.md b/26.md index e8bd1138..520956a7 100644 --- a/26.md +++ b/26.md @@ -8,178 +8,157 @@ ## Summary -This NUT defines Pay-to-Blinded-Key (P2BK), a wallet-to-wallet privacy extension of [NUT-11][11] (P2PK). P2BK blinds the public keys contained in a P2PK Secret so the mint cannot learn the true recipient key material when issuing or redeeming ecash. The scheme requires no changes to mint behavior: wallets MUST normalize P2BK Secrets to standard P2PK Secrets before sending requests to a mint. All Schnorr signing and verification at the mint remains exactly as specified in NUT-11. +Pay-to-Blinded-Key (P2BK) extends [NUT-11][11] (P2PK) by letting senders blind the receiver’s public keys with fresh randomness before any mint interaction. Mints continue to see and sign canonical P2PK secrets; only wallet-to-wallet transfers carry the additional blinding metadata that allows the receiver to unblind the key material and perform the spend. ## Motivation -- In NUT-11, the receiver public key(s) appear in cleartext in the Secret. On redemption, the mint learns the long-term public key(s) that control the ecash. -- P2BK hides those keys from the mint by blinding each receiver public key P with a random scalar r: P' = P + r·G. The receiver later derives a corresponding private key k from p and r (see below) to sign per NUT-11. The mint only ever sees normal P2PK Secrets and signatures. +- In NUT-11, the receiver pubkey(s) appear in clear text. When the receiver redeems the proof the mint learns the long-term key material. +- P2BK hides those keys from the mint by blinding each receiver public key \(P\) with a scalar \(r\): \(P' = P + r \cdot G\). The receiver later reconstructs the signing key from \(p\) and \(r\) (see below) while presenting the mint with an ordinary P2PK secret. ## Definitions and notation -- Curve: secp256k1 with base point G and order n. -- Unblinded pubkey: P. - - For SEC1-style keys, P is a 33-byte compressed SEC1 pubkey (hex). - - For BIP340 Schnorr/x-only contexts (e.g., Nostr), P is an x-only 32-byte pubkey (hex) with even-Y parity (per BIP340), prefixed with '02' for Cashu exosystem interoperability. -- Blinding scalar: r with 1 ≤ r ≤ n − 1. -- Blinded pubkey: P' = P + r·G. Encoding is compressed SEC1 (33-byte hex). -- Derived private key: for SEC1 compressed pubkeys, k = (p + r) mod n. For x-only Schnorr pubkeys (e.g., Nostr), wallets MUST compute k1 = (p + r) mod n and k2 = (-p + r) mod n and select the candidate whose derived even-Y public key equals the blinded pubkey P'. +- Curve: secp256k1 with base point \(G\) and order \(n\). +- Unblinded pubkey: \(P\). + - SEC1 contexts: 33-byte compressed SEC1 pubkey (hex). + - BIP340 Schnorr/x-only contexts (e.g. Nostr): 32-byte x-only pubkey (hex) with even-Y parity (per BIP340). For Cashu interoperability wallets SHOULD prefix the x-only key with `02` when serialising to hex strings. +- Blinding scalar: \(r\) with \(1 \leq r \leq n-1\). +- Blinded pubkey: \(P' = P + r \cdot G\). Encoding is compressed SEC1 (33-byte hex). +- Derived private key: + - SEC1: \(k = (p + r) \bmod n\). + - BIP340/x-only: compute candidate keys \(k_1 = (p + r) \bmod n\) and \(k_2 = (-p + r) \bmod n\); select the one whose even-Y pubkey matches \(P'\). -## Wire format +## Proof metadata (`p2pk_r`) -Secret kind: "P2BK" - -P2BK introduces a new well-known Secret kind. It has the same structure as a NUT-10/11 P2PK Secret except for the kind string and an additional tag that carries the blinding scalars: +The `Proof` object defined in [NUT-00][00] is augmented with optional metadata: ```json -[ - "P2BK", - { - "nonce": "", - "data": "", - "tags": [ - ["sigflag", "SIG_INPUTS"], - ["pubkeys", "", ""], - ["n_sigs", ""], - ["locktime", ""], - ["refund", "", ""], - ["n_sigs_refund", ""], - ["r", "", "", ""] - ] - } -] +{ + "amount": int, + "id": hex_str, + "secret": str, + "C": hex_str, + "p2pk_r": Array[str] +} ``` -Notes: - -- data holds the primary blinded pubkey P0'. -- pubkeys holds zero or more additional blinded pubkeys P1', P2', … for locktime multisig. -- refund holds zero or more blinded refund pubkeys R0', R1', … for refund multisig. -- r is an array of the blinding scalars as fixed-length 32-byte hex strings (lowercase) without prefix. The order of r MUST match the concatenation order of all blinded keys present in the Secret: - 1. the primary lock key data, then 2) any keys in pubkeys, then 3) any keys in refund. - For example, if there are m lock keys in total (1 in data + pubkeys.length) and t refund keys, the r tag contains m + t entries: [r0, r1, …, r(m−1), r(m), …, r(m+t−1)]. - -## Normalization for mints (wallet MUST) - -Mints do not understand "P2BK". Wallets MUST transform a P2BK Secret into a standard P2PK Secret before using it in any request to a mint (swap/melt/etc.). Normalization is purely a string transformation: +- `p2pk_r` holds the blinding scalars as lowercase 64-character hex strings (32 bytes each). +- Ordering: the entries MUST align with the concatenation order of blinded keys inside the associated P2PK secret — first the primary lock key (`data`), then any extra lock keys under `pubkeys`, followed by any refund keys under `refund`. +- Absent metadata (`p2pk_r` omitted) indicates a regular unblinded P2PK proof. -1. Parse the JSON Secret [kind, meta]. If kind != "P2BK", return unchanged. -2. Remove the r tag from meta.tags if present. If no tags remain, tags MUST be kept as an empty array (i.e: `tags: []`). -3. Replace kind with "P2PK". -4. Re-serialize to JSON string. This string is the message to be signed for NUT-11 witnesses and is what wallets MUST send to mints. +Wallets MUST NOT expose `p2pk_r` to the mint. The field is strictly for wallet-to-wallet transport. -No other fields are changed. In particular, data, pubkeys, refund remain the blinded pubkeys P', R'. +### Token carriage of `p2pk_r` -## Signing and verification (unchanged at the mint) +All token serialisations that can transport proofs MUST carry the blinding metadata untouched if present. -- Signature scheme, sigflag semantics, multisig, locktime/refund behavior, and message aggregation (SIG_INPUTS, SIG_ALL) are as in NUT-11. All signatures are Schnorr signatures over the SHA256 hash of the normalized P2PK Secret string (and any aggregated outputs per NUT-11). -- Mints MUST NOT receive r values and MUST NOT receive Secrets of kind "P2BK". Wallets MUST perform normalization before sending any request to a mint. +#### Token V3 (JSON) -## Wallet behavior +Each proof MAY include the `p2pk_r` array verbatim. Wallets SHOULD preserve unknown fields, so no other changes to the Token V3 schema are necessary. -### Sender (building P2BK) +#### Token V4 (CBOR) -- For each recipient lock/refund pubkey Pi, choose fresh ri ∈ [1, n − 1]. -- Compute Pi' = Pi + ri·G. -- Construct a P2PK Secret as in NUT-11 but replace all pubkeys with their blinded forms. The `tags` entry MUST be present, even if empty (i.e: `tags: []`). Blind the message as usual to obtain outputs and promises from the mint. -- After unblinding signatures to proofs, rewrite the proof Secret to kind "P2BK" and append a tag ["r", r0, r1, …] carrying all ris in the order defined above. Deliver these P2BK proofs to the receiver. +Proof objects MAY include the short key `pr`, which mirrors `p2pk_r` but encodes each scalar as a CBOR `bytes` value for space efficiency. -### Receiver (spending P2BK) +The following real-world P2BK token (provided by Rob Woodgate) illustrates the encoding: -- Parse the P2BK Secret and extract r values. -- Derive the signing private keys as follows: - - For SEC1 compressed pubkeys, ki = (pi + ri) mod n. - - For BIP340 Schnorr/x-only pubkeys, compute k1 = (pi + ri) mod n and k2 = (-pi + ri) mod n; select the candidate whose derived even-Y public key equals the blinded pubkey P'i in the Secret. -- Normalize the Secret to P2PK (strip r; set kind = "P2PK"; ensure `tags` entry is retained if empty). -- Produce Schnorr signature(s) over the normalized P2PK Secret string according to the applicable sigflag and multisig rules. -- Submit the proofs to the mint. The mint verifies exactly as in NUT-11. - -## Determinism and canonicalization - -- The r tag MUST contain lowercase hex and be 64 hex chars per entry (32 bytes), matching the conventional encoding used for scalars elsewhere in Cashu. -- Wallets SHOULD ignore duplicate r entries and MUST deduplicate derived signing keys if the same ri appears multiple times. Ordering rules still apply to allow position-based mapping. -- Wallets MUST NOT include an empty r tag; if present, mints will never see it due to normalization, but receivers SHOULD treat an empty r tag as equivalent to absence (no blinding information). +``` +cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpWFhAWFzeQEUWyJQMlBLIix7Im5vbmNlIjoiNWYxN2U0MzU5YjFjZDAxYzkzNjQ4MGVkZGNjMGEzNzk2ZjJhYzM2MDVjNGIzOTkxZGE3YTQxY2UzNGE4MGY5OSIsImRhdGEiOiIwMzhiNTk0M2ZjMzY4ZjI1OWYzNTM5YTViN2FjMjE3ZjIzNzEwNzRkMzc1MDc3ZDMwNDZlMTk1NTkyYjI0Y2FjYTUiLCJ0YWdzIjpbWyJsb2NrdGltZSIsIjE3NTk5NjQzNDAiXSxbInJlZnVuZCIsIjAzNTJiOWFmNGJhMWRiMDliM2Y2Y2E1NDRhNmExY2M0OTQ1ZGRhZjY4MmZjOTMwMzYwYzVlZGU4NGQzZjNjNTBhYiJdXX1dYWNYIQKStW3NIERz0XR--cXYAkfmmo8iDigAc-2F8TE2MYhALGFko2FlWCDXvqR7-X6gJJiHa1T6eBaWHMnGMseti7OrXgwYGI3432FzWCBC6iVGhNhgqNPhA54qlYReHkNOMPHdRkPd2mlGykeB8GFyWCAsniO_fjxZSrh4lSelhNMkBZmMnDL1sdblE82YjYyjgmJwcoJYIHbeRz2-u06KCI0NMjMl75Vf5jnnmXcuONIg9ZVTefklWCAvZpnxoEhgKLdl9lFoU5bF-hVT8YsSmtrjO88OI0UUUw +``` -## Constraints and errors +Decoded structure (`mint` truncated for clarity): -- ri MUST satisfy 1 ≤ ri ≤ n − 1. Senders MUST use uniformly random ri and MUST NOT reuse ri across different blinded keys. -- The derived private key ki = (pi + ri) mod n MUST be non-zero. If ki = 0 (probability ~1/n), the receiver cannot sign; the remedy is to request a re-send with different randomness. -- Wallets MUST treat any P2BK Secret missing the r tag as a non-blinded P2PK Secret (i.e., treat like NUT-11 where P' are just arbitrary pubkeys). +```json +{ + "mint": "https://mint.minibits.cash/Bitcoin", + "unit": "sat", + "t": [ + { + "i": h'00500550f0494146', + "p": [ + { + "a": 1, + "s": "[\"P2PK\",{\"nonce\":\"5f17e4359b1cd01c936480eddcc0a3796f2ac3605c4b3991da7a41ce34a80f99\",\"data\":\"038b5943fc368f259f3539a5b7ac217f2371074d375077d3046e195592b24caca5\",\"tags\":[[\"locktime\",\"1759964340\"],[\"refund\",\"0352b9af4ba1db09b3f6ca544a6a1cc4945ddaf682fc930360c5ede84d3f3c50ab\"]]}]", + "c": h'0292b56dcd204473d1747ef9c5d80247e69a8f220e280073ed85f131363188402c', + "d": { + "e": h'd7bea47bf97ea02498876b54fa7816961cc9c632c7ad8bb3ab5e0c18188df8df', + "s": h'42ea254684d860a8d3e1039e2a95845e1e434e30f1dd4643ddda6946ca4781f0', + "r": h'2c9e23bf7e3c594ab8789527a584d32405998c9c32f5b1d6e513cd988d8ca382' + }, + "pr": [ + h'76de473dbebb4e8a088d0d323325ef955fe639e799772e38d220f5955379f925', + h'2f6699f1a0486028b765f651685396c5fa1553f18b129adae33bcf0e23451453' + ] + } + ] + } + ] +} +``` -## Security considerations +This example demonstrates that the `pr` entries preserve the blinding scalars for both the primary lock key and the refund key in order. -- Mint Privacy: Mints never see r nor unblinded P. They see only blinded pubkeys P' inside a standard P2PK Secret, and standard Schnorr signatures. This prevents mints from trivially linking P' to a receiver's long-term P if P is reused elsewhere. -- Freshness: Reusing `r`s across payments or across keys enables linkability across P' values. Senders MUST generate fresh `r`s for every blinded key instance. -- Receiver Key Hygiene: Receivers SHOULD avoid reusing the same base key p across many P2BK payments if linkability via off-chain correlation is a concern, although the mint cannot derive P. -- Metadata Leakage: The presence of P' alone does not reveal P without knowledge of r or p. The r tag is only exchanged wallet-to-wallet and MUST NOT be forwarded to mints or third parties. +## Wallet workflow -## Compatibility +### Sender (building P2BK proofs) -- Backwards Compatibility: Mints require no changes. Wallets that do not understand P2BK will fail to parse the Secret kind and thus cannot accept such proofs. Senders SHOULD only send P2BK proofs to receivers known to support NUT-26. -- Interop with NUT-11: All NUT-11 tag semantics (sigflag, n_sigs, locktime, refund, n_sigs_refund) apply unchanged when used with blinded keys. NUT-11 secrets intended for P2BK blinding MUST include the `tags` entry, even if empty (ie: `tags: []`) +1. For each receiver key \(P_i\) (lock and refund) draw fresh randomness \(r_i\). +2. Compute \(P'_i = P_i + r_i \cdot G\). +3. Build the P2PK secret as in NUT-11, substituting all pubkeys with the blinded versions \(P'_i\). Keep the `tags` array present even if empty. +4. Interact with the mint (mint/swap). The mint only ever sees the canonical P2PK secret. +5. After unblinding, attach the ordered scalar list to the resulting proofs as `p2pk_r` (or `pr` in Token V4) before handing them to the receiver. -## Mint info +### Receiver (spending P2BK proofs) -No new mint capability flag is required because mint behavior is unchanged. [NUT-06][06] remains the source of P2PK support (NUT-11). Wallet capability advertisement is out-of-scope for this NUT. +1. Parse the canonical P2PK secret embedded in the proof. +2. Read the accompanying `p2pk_r` scalars. Wallets SHOULD treat missing metadata as a regular P2PK proof. +3. Derive signing keys according to key type (SEC1 or BIP340/x-only, see above). +4. Produce Schnorr signatures over the unchanged P2PK secret per NUT-11 rules (`sigflag`, multisig, refund, etc.). +5. Submit the proofs to the mint. Verification is identical to NUT-11 and requires no mint changes. -## Worked example +## Determinism and canonicalisation -Receiver base pubkey P (compressed): +- Scalars MUST be formatted as lowercase 64-character hex strings (32 bytes). +- Wallets MUST preserve ordering and SHOULD de-duplicate derived signing keys if the same scalar appears multiple times. +- Omit the field entirely rather than emitting an empty `p2pk_r`/`pr` array. -``` -033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e -``` +## Constraints and errors -Sender samples r0 (hex): +- Each \(r_i\) MUST satisfy \(1 \leq r_i \leq n-1\); randomness MUST be uniform and MUST NOT be reused across blinded keys. +- If \(k_i = 0\) (probability ~1/n) the receiver cannot sign; they SHOULD request a resend with fresh randomness. +- Wallets SHOULD reject proofs that advertise `p2pk_r` but do not contain the corresponding blinded keys. -``` -1f3a... (64 hex chars) -``` +## Security considerations -Blinded lock key: +- Mint privacy: mints never learn the blinding scalars or the original pubkeys; spends appear identical to standard P2PK. +- Freshness: reusing scalars enables linkage across blinded keys. Always use fresh randomness per key. +- Receiver key hygiene: receivers MAY rotate base keys to reduce linkage through off-mint correlations. +- Metadata leakage: wallets MUST keep `p2pk_r` private between sender and receiver. -``` -P0' = P + r0·G -``` +## Compatibility -P2BK Secret (SIG_INPUTS, no multisig): +- Backwards compatibility: no mint changes are required. Legacy wallets simply ignore the optional metadata and process proofs as standard P2PK. +- Interoperability: all NUT-11 semantics (`sigflag`, multisig, locktime, refund) remain unchanged. -```json -[ - "P2BK", - { - "nonce": "da62796403af76c80cd6ce9153ed3746", - "data": "03ab...", - "tags": [ - ["sigflag", "SIG_INPUTS"], - ["r", "1f3a..."] - ] - } -] -``` +## Worked example -Normalization for mint/signing (wallet-side): +Using the sample proof above: -```json -[ - "P2PK", - { - "nonce": "da62796403af76c80cd6ce9153ed3746", - "data": "03ab...", - "tags": [["sigflag", "SIG_INPUTS"]] - } -] -``` +- Lock key (blinded): `038b5943fc368f259f3539a5b7ac217f2371074d375077d3046e195592b24caca5` +- Refund key (blinded): `0352b9af4ba1db09b3f6ca544a6a1cc4945ddaf682fc930360c5ede84d3f3c50ab` +- Scalars (hex): + - `r_0 = 76de473dbebb4e8a088d0d323325ef955fe639e799772e38d220f5955379f925` + - `r_1 = 2f6699f1a0486028b765f651685396c5fa1553f18b129adae33bcf0e23451453` -Receiver derives k0 from p and r as per key type: for SEC1, k0 = (p + r0) mod n; for BIP340/x-only, choose between (p + r0) mod n and (-p + r0) mod n by matching the even-Y pubkey to P0'. +During spend, the receiver selects the scalar corresponding to each blinded key and reconstructs the signing key (SEC1 or BIP340 rules) before creating Schnorr signatures over the canonical P2PK secret. The mint remains oblivious to the blinding data throughout. ## Implementation notes (informative) -- A reference TypeScript implementation is available in cashu-ts, which: - - Creates blinded pubkeys and tracks the corresponding r values in order. - - Emits proofs to receivers with Secret kind "P2BK" and an r tag. - - Normalizes Secrets back to kind "P2PK" (stripping r) for all mint interactions and hashes/signs over that normalized string. - - Derives signing keys: for SEC1, k = p + r; for BIP340/x-only, try k = p + r and k = -p + r and pick the one whose even-Y pubkey equals P'. +- Reference implementations such as `cashu-ts`: + - Track blinded pubkeys alongside their scalars. + - Emit proofs carrying `p2pk_r`/`pr` metadata. + - Reuse canonical P2PK secrets for all mint interactions. + - Derive signing keys via the SEC1 or BIP340 workflow described above. +- Token V4 encoders SHOULD keep `pr` values as raw byte strings to maximise CBOR compression. [00]: 00.md [03]: 03.md From 34ad20c4c917ccc8a670b09687a131ffd029c136 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 9 Oct 2025 17:36:38 +0200 Subject: [PATCH 11/21] fix --- 26.md | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/26.md b/26.md index 520956a7..cf78ff3b 100644 --- a/26.md +++ b/26.md @@ -13,19 +13,19 @@ Pay-to-Blinded-Key (P2BK) extends [NUT-11][11] (P2PK) by letting senders blind t ## Motivation - In NUT-11, the receiver pubkey(s) appear in clear text. When the receiver redeems the proof the mint learns the long-term key material. -- P2BK hides those keys from the mint by blinding each receiver public key \(P\) with a scalar \(r\): \(P' = P + r \cdot G\). The receiver later reconstructs the signing key from \(p\) and \(r\) (see below) while presenting the mint with an ordinary P2PK secret. +- P2BK hides those keys from the mint by blinding each receiver public key $P$ with a scalar $r$: $P' = P + r \cdot G$. The receiver later reconstructs the signing key from $p$ and $r$ (see below) while presenting the mint with an ordinary P2PK secret. ## Definitions and notation -- Curve: secp256k1 with base point \(G\) and order \(n\). -- Unblinded pubkey: \(P\). +- Curve: secp256k1 with base point $G$ and order $n$. +- Unblinded pubkey: $P$. - SEC1 contexts: 33-byte compressed SEC1 pubkey (hex). - BIP340 Schnorr/x-only contexts (e.g. Nostr): 32-byte x-only pubkey (hex) with even-Y parity (per BIP340). For Cashu interoperability wallets SHOULD prefix the x-only key with `02` when serialising to hex strings. -- Blinding scalar: \(r\) with \(1 \leq r \leq n-1\). -- Blinded pubkey: \(P' = P + r \cdot G\). Encoding is compressed SEC1 (33-byte hex). +- Blinding scalar: $r$ with $1 \leq r \leq n-1$. +- Blinded pubkey: $P' = P + r \cdot G$. Encoding is compressed SEC1 (33-byte hex). - Derived private key: - - SEC1: \(k = (p + r) \bmod n\). - - BIP340/x-only: compute candidate keys \(k_1 = (p + r) \bmod n\) and \(k_2 = (-p + r) \bmod n\); select the one whose even-Y pubkey matches \(P'\). + - SEC1: $k = (p + r) \bmod n$. + - BIP340/x-only: compute candidate keys $k_1 = (p + r) \bmod n$ and $k_2 = (-p + r) \bmod n$; select the one whose even-Y pubkey matches $P'$. ## Proof metadata (`p2pk_r`) @@ -101,9 +101,9 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo ### Sender (building P2BK proofs) -1. For each receiver key \(P_i\) (lock and refund) draw fresh randomness \(r_i\). -2. Compute \(P'_i = P_i + r_i \cdot G\). -3. Build the P2PK secret as in NUT-11, substituting all pubkeys with the blinded versions \(P'_i\). Keep the `tags` array present even if empty. +1. For each receiver key $P_i$ (lock and refund) draw fresh randomness $r_i$ +2. Compute $P'_i = P_i + r_i \cdot G$. +3. Build the P2PK secret as in NUT-11, substituting all pubkeys with the blinded versions $P'_i$. Keep the `tags` array present even if empty. 4. Interact with the mint (mint/swap). The mint only ever sees the canonical P2PK secret. 5. After unblinding, attach the ordered scalar list to the resulting proofs as `p2pk_r` (or `pr` in Token V4) before handing them to the receiver. @@ -123,20 +123,18 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo ## Constraints and errors -- Each \(r_i\) MUST satisfy \(1 \leq r_i \leq n-1\); randomness MUST be uniform and MUST NOT be reused across blinded keys. -- If \(k_i = 0\) (probability ~1/n) the receiver cannot sign; they SHOULD request a resend with fresh randomness. +- Each $r_i$ MUST satisfy $1 \leq r_i \leq n-1$; randomness MUST be uniform and MUST NOT be reused across blinded keys. - Wallets SHOULD reject proofs that advertise `p2pk_r` but do not contain the corresponding blinded keys. ## Security considerations - Mint privacy: mints never learn the blinding scalars or the original pubkeys; spends appear identical to standard P2PK. - Freshness: reusing scalars enables linkage across blinded keys. Always use fresh randomness per key. -- Receiver key hygiene: receivers MAY rotate base keys to reduce linkage through off-mint correlations. -- Metadata leakage: wallets MUST keep `p2pk_r` private between sender and receiver. +- Metadata leakage: wallets MUST keep `p2pk_r` private between sender and receiver (do not leak to the Mint). ## Compatibility -- Backwards compatibility: no mint changes are required. Legacy wallets simply ignore the optional metadata and process proofs as standard P2PK. +- Backwards compatibility: no mint changes are required. Legacy wallets can't redeem P2BK locked ecash, because they don't know how to produce the right key for the signature. - Interoperability: all NUT-11 semantics (`sigflag`, multisig, locktime, refund) remain unchanged. ## Worked example @@ -151,15 +149,6 @@ Using the sample proof above: During spend, the receiver selects the scalar corresponding to each blinded key and reconstructs the signing key (SEC1 or BIP340 rules) before creating Schnorr signatures over the canonical P2PK secret. The mint remains oblivious to the blinding data throughout. -## Implementation notes (informative) - -- Reference implementations such as `cashu-ts`: - - Track blinded pubkeys alongside their scalars. - - Emit proofs carrying `p2pk_r`/`pr` metadata. - - Reuse canonical P2PK secrets for all mint interactions. - - Derive signing keys via the SEC1 or BIP340 workflow described above. -- Token V4 encoders SHOULD keep `pr` values as raw byte strings to maximise CBOR compression. - [00]: 00.md [03]: 03.md [05]: 05.md From eecc05ec9b920be925c2e451f8ad4fd492dfd9ee Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:34:09 +0200 Subject: [PATCH 12/21] Update 26.md Co-authored-by: Rob Woodgate --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index cf78ff3b..904066f8 100644 --- a/26.md +++ b/26.md @@ -103,7 +103,7 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo 1. For each receiver key $P_i$ (lock and refund) draw fresh randomness $r_i$ 2. Compute $P'_i = P_i + r_i \cdot G$. -3. Build the P2PK secret as in NUT-11, substituting all pubkeys with the blinded versions $P'_i$. Keep the `tags` array present even if empty. +3. Build the P2PK secret as in NUT-11, substituting all pubkeys with the blinded versions $P'_i$. 4. Interact with the mint (mint/swap). The mint only ever sees the canonical P2PK secret. 5. After unblinding, attach the ordered scalar list to the resulting proofs as `p2pk_r` (or `pr` in Token V4) before handing them to the receiver. From 4fbbc3d0076ee96b3fe056092ff95ac79f84c7c8 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:35:52 +0200 Subject: [PATCH 13/21] Update 26.md Co-authored-by: Rob Woodgate --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index 904066f8..e4971147 100644 --- a/26.md +++ b/26.md @@ -20,7 +20,7 @@ Pay-to-Blinded-Key (P2BK) extends [NUT-11][11] (P2PK) by letting senders blind t - Curve: secp256k1 with base point $G$ and order $n$. - Unblinded pubkey: $P$. - SEC1 contexts: 33-byte compressed SEC1 pubkey (hex). - - BIP340 Schnorr/x-only contexts (e.g. Nostr): 32-byte x-only pubkey (hex) with even-Y parity (per BIP340). For Cashu interoperability wallets SHOULD prefix the x-only key with `02` when serialising to hex strings. + - BIP340 Schnorr/x-only contexts (e.g. Nostr): 32-byte x-only pubkey (hex) with even-Y parity (per BIP340). For Cashu interoperability wallets MUST prefix the x-only key with `02` when serialising to hex strings. - Blinding scalar: $r$ with $1 \leq r \leq n-1$. - Blinded pubkey: $P' = P + r \cdot G$. Encoding is compressed SEC1 (33-byte hex). - Derived private key: From 11a3d8c5f0e1e0c328bb5f9d6222f1aff9ff1405 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:36:32 +0200 Subject: [PATCH 14/21] Update 26.md Co-authored-by: Rob Woodgate --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index e4971147..a29b8f20 100644 --- a/26.md +++ b/26.md @@ -134,7 +134,7 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo ## Compatibility -- Backwards compatibility: no mint changes are required. Legacy wallets can't redeem P2BK locked ecash, because they don't know how to produce the right key for the signature. +- Backwards compatibility: no mint changes are required. Legacy wallets can't redeem P2BK locked ecash, because they don't know how to derive the right private key(s) for the signature. - Interoperability: all NUT-11 semantics (`sigflag`, multisig, locktime, refund) remain unchanged. ## Worked example From ddab200f4f3e0753bdc16646568e2629cab12b3c Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:36:52 +0200 Subject: [PATCH 15/21] Update 26.md Co-authored-by: Rob Woodgate --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index a29b8f20..49989002 100644 --- a/26.md +++ b/26.md @@ -124,7 +124,7 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo ## Constraints and errors - Each $r_i$ MUST satisfy $1 \leq r_i \leq n-1$; randomness MUST be uniform and MUST NOT be reused across blinded keys. -- Wallets SHOULD reject proofs that advertise `p2pk_r` but do not contain the corresponding blinded keys. +- Wallets SHOULD reject proofs that advertise `p2pk_r` but do not contain the corresponding blinded keys. There must be one entry in `p2pk_r` for every blinded key. ## Security considerations From 2373050efd51201c6451f31b99bc3c7ebb23714e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 13 Oct 2025 23:07:50 +0200 Subject: [PATCH 16/21] specify preserving `p2pk_r` ordering --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index 49989002..a15a7932 100644 --- a/26.md +++ b/26.md @@ -118,7 +118,7 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo ## Determinism and canonicalisation - Scalars MUST be formatted as lowercase 64-character hex strings (32 bytes). -- Wallets MUST preserve ordering and SHOULD de-duplicate derived signing keys if the same scalar appears multiple times. +- Wallets MUST preserve ordering of the `p2pk_r`/`pr` array. - Omit the field entirely rather than emitting an empty `p2pk_r`/`pr` array. ## Constraints and errors From c63aa7feda27502758e09b6321b95d25d8b3ba4a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 14 Oct 2025 15:36:46 +0200 Subject: [PATCH 17/21] payment request signalling for blinded keys --- 26.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/26.md b/26.md index a15a7932..74bb0db4 100644 --- a/26.md +++ b/26.md @@ -97,6 +97,60 @@ Decoded structure (`mint` truncated for clarity): This example demonstrates that the `pr` entries preserve the blinding scalars for both the primary lock key and the refund key in order. +## Payment request extension + +This NUT extends the `NUT10Option` object from [NUT-18][18] with an optional `b` field to signal P2BK support. + +### Extended NUT10Option schema + +```json +{ + "k": str, + "d": str, + "t": Array[Array[str, str]] , + "b": bool +} +``` + +Where `b` indicates whether the receiver supports Pay-to-Blinded-Key and requests the sender to blind the receiver's public keys specified in the `d` field. + +### Sender behavior + +When a payment request includes a P2PK locking condition in the `nut10` field with `b: true`, the sender SHOULD: + +1. Blind the receiver's public key(s) specified in the `nut10.d` field (and any keys in `nut10.t` refund tags) before minting/swapping +2. Include the blinding scalar(s) in the `p2pk_r` (or `pr` for Token V4) field of the resulting proofs +3. Send the P2BK-enhanced proofs to the receiver + +### Example payment request with P2BK + +```json +{ + "i": "c5f3d829", + "a": 21, + "u": "sat", + "m": ["https://mint.example.com"], + "d": "Payment for coffee", + "nut10": { + "k": "P2PK", + "d": "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", + "b": true + }, + "t": [ + { + "t": "post", + "a": "https://receiver.example.com/payment" + } + ] +} +``` + +In this example: +- The receiver signals P2BK support with `"b": true` inside the `nut10` object +- The receiver requires P2PK locking with their public key in the `d` field +- The sender will blind the public key `02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2` before interacting with the mint +- The sender will include the blinding scalar(s) in the `p2pk_r` field of the resulting proofs + ## Wallet workflow ### Sender (building P2BK proofs) @@ -156,3 +210,4 @@ During spend, the receiver selects the scalar corresponding to each blinded key [08]: 08.md [10]: 10.md [11]: 11.md +[18]: 18.md From 240699506c22a695ced3dbd258f36e3af3106e25 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 14 Oct 2025 15:37:15 +0200 Subject: [PATCH 18/21] prettier --- 26.md | 1 + 1 file changed, 1 insertion(+) diff --git a/26.md b/26.md index 74bb0db4..0c9ef2dd 100644 --- a/26.md +++ b/26.md @@ -146,6 +146,7 @@ When a payment request includes a P2PK locking condition in the `nut10` field wi ``` In this example: + - The receiver signals P2BK support with `"b": true` inside the `nut10` object - The receiver requires P2PK locking with their public key in the `d` field - The sender will blind the public key `02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2` before interacting with the mint From 203fb219929858f5c3278babc1d496d17166a0df Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 17 Oct 2025 10:25:26 +0200 Subject: [PATCH 19/21] move `b` flag --- 26.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/26.md b/26.md index 0c9ef2dd..6af26967 100644 --- a/26.md +++ b/26.md @@ -99,16 +99,21 @@ This example demonstrates that the `pr` entries preserve the blinding scalars fo ## Payment request extension -This NUT extends the `NUT10Option` object from [NUT-18][18] with an optional `b` field to signal P2BK support. +This NUT extends the `PaymentRequest` object from [NUT-18][18] with an optional `b` field to signal P2BK support. -### Extended NUT10Option schema +### Extended PaymentRequest schema ```json { - "k": str, - "d": str, - "t": Array[Array[str, str]] , - "b": bool + "i": str , + "a": int , + "u": str , + "s": bool , + "m": Array[str] , + "d": str , + "t": Array[Transport] , + "b": bool , + "nut10": NUT10Option , } ``` @@ -131,10 +136,10 @@ When a payment request includes a P2PK locking condition in the `nut10` field wi "u": "sat", "m": ["https://mint.example.com"], "d": "Payment for coffee", + "b": true, "nut10": { "k": "P2PK", "d": "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", - "b": true }, "t": [ { @@ -147,7 +152,7 @@ When a payment request includes a P2PK locking condition in the `nut10` field wi In this example: -- The receiver signals P2BK support with `"b": true` inside the `nut10` object +- The receiver signals P2BK support with `"b": true` - The receiver requires P2PK locking with their public key in the `d` field - The sender will blind the public key `02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2` before interacting with the mint - The sender will include the blinding scalar(s) in the `p2pk_r` field of the resulting proofs From 9e5f80847a7ffc2d56a9104f26dc24758c4ea609 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 17 Oct 2025 10:26:26 +0200 Subject: [PATCH 20/21] prettier --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index 6af26967..c85f0cfb 100644 --- a/26.md +++ b/26.md @@ -139,7 +139,7 @@ When a payment request includes a P2PK locking condition in the `nut10` field wi "b": true, "nut10": { "k": "P2PK", - "d": "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", + "d": "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" }, "t": [ { From f980167d48cac336c5d3e18507bd930ea456273c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 18 Oct 2025 21:59:01 +0200 Subject: [PATCH 21/21] remove robw reference --- 26.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/26.md b/26.md index c85f0cfb..adecaaa9 100644 --- a/26.md +++ b/26.md @@ -59,7 +59,7 @@ Each proof MAY include the `p2pk_r` array verbatim. Wallets SHOULD preserve unkn Proof objects MAY include the short key `pr`, which mirrors `p2pk_r` but encodes each scalar as a CBOR `bytes` value for space efficiency. -The following real-world P2BK token (provided by Rob Woodgate) illustrates the encoding: +The following P2BK token illustrates the encoding: ``` cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpWFhAWFzeQEUWyJQMlBLIix7Im5vbmNlIjoiNWYxN2U0MzU5YjFjZDAxYzkzNjQ4MGVkZGNjMGEzNzk2ZjJhYzM2MDVjNGIzOTkxZGE3YTQxY2UzNGE4MGY5OSIsImRhdGEiOiIwMzhiNTk0M2ZjMzY4ZjI1OWYzNTM5YTViN2FjMjE3ZjIzNzEwNzRkMzc1MDc3ZDMwNDZlMTk1NTkyYjI0Y2FjYTUiLCJ0YWdzIjpbWyJsb2NrdGltZSIsIjE3NTk5NjQzNDAiXSxbInJlZnVuZCIsIjAzNTJiOWFmNGJhMWRiMDliM2Y2Y2E1NDRhNmExY2M0OTQ1ZGRhZjY4MmZjOTMwMzYwYzVlZGU4NGQzZjNjNTBhYiJdXX1dYWNYIQKStW3NIERz0XR--cXYAkfmmo8iDigAc-2F8TE2MYhALGFko2FlWCDXvqR7-X6gJJiHa1T6eBaWHMnGMseti7OrXgwYGI3432FzWCBC6iVGhNhgqNPhA54qlYReHkNOMPHdRkPd2mlGykeB8GFyWCAsniO_fjxZSrh4lSelhNMkBZmMnDL1sdblE82YjYyjgmJwcoJYIHbeRz2-u06KCI0NMjMl75Vf5jnnmXcuONIg9ZVTefklWCAvZpnxoEhgKLdl9lFoU5bF-hVT8YsSmtrjO88OI0UUUw