-
Notifications
You must be signed in to change notification settings - Fork 75
NUT-XX: Pay-to-Blinded-Key (P2BK) #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
38bc16a
NUT-26: Pay-to-Blinded-Key (P2BK): formalize public key blinding atop…
a1denvalu3 850d8a0
Apply suggestions from code review
a1denvalu3 1b97691
fix readme table
a1denvalu3 a21ceb3
NUT-26: document Schnorr x-only key derivation for blinded keys (try …
a1denvalu3 8fccf24
NUT-26: add implementation note on Schnorr-derived key selection (eve…
a1denvalu3 b51ed0c
NUT-26: clarify worked example derivation for BIP340 vs SEC1 keys; no…
a1denvalu3 ea6888e
prettier
a1denvalu3 9127bb5
Update 26.md
a1denvalu3 e38faa0
Update 26.md
a1denvalu3 77497d1
NUT-26: document p2pk_r metadata and token carriage
a1denvalu3 34ad20c
fix
a1denvalu3 d3a2770
Merge pull request #1 from lollerfirst/pr291
a1denvalu3 eecc05e
Update 26.md
a1denvalu3 4fbbc3d
Update 26.md
a1denvalu3 11a3d8c
Update 26.md
a1denvalu3 ddab200
Update 26.md
a1denvalu3 2373050
specify preserving `p2pk_r` ordering
a1denvalu3 c63aa7f
payment request signalling for blinded keys
a1denvalu3 2406995
prettier
a1denvalu3 203fb21
move `b` flag
a1denvalu3 9e5f808
prettier
a1denvalu3 f980167
remove robw reference
a1denvalu3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| # NUT-26: Pay-to-Blinded-Key (P2BK) | ||
|
|
||
| `optional` | ||
|
|
||
| `depends on: NUT-11` | ||
|
|
||
| --- | ||
|
|
||
| ## Summary | ||
|
|
||
| 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 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$. | ||
| - 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 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: | ||
| - 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`) | ||
|
|
||
| The `Proof` object defined in [NUT-00][00] is augmented with optional metadata: | ||
|
|
||
| ```json | ||
| { | ||
| "amount": int, | ||
| "id": hex_str, | ||
| "secret": str, | ||
| "C": hex_str, | ||
| "p2pk_r": Array[str] <optional> | ||
| } | ||
| ``` | ||
|
|
||
| - `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. | ||
|
|
||
| Wallets MUST NOT expose `p2pk_r` to the mint. The field is strictly for wallet-to-wallet transport. | ||
|
|
||
| ### Token carriage of `p2pk_r` | ||
|
|
||
| All token serialisations that can transport proofs MUST carry the blinding metadata untouched if present. | ||
|
|
||
| #### Token V3 (JSON) | ||
|
|
||
| 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. | ||
|
|
||
| #### Token V4 (CBOR) | ||
|
|
||
| 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 P2BK token illustrates the encoding: | ||
|
|
||
| ``` | ||
| cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpWFhAWFzeQEUWyJQMlBLIix7Im5vbmNlIjoiNWYxN2U0MzU5YjFjZDAxYzkzNjQ4MGVkZGNjMGEzNzk2ZjJhYzM2MDVjNGIzOTkxZGE3YTQxY2UzNGE4MGY5OSIsImRhdGEiOiIwMzhiNTk0M2ZjMzY4ZjI1OWYzNTM5YTViN2FjMjE3ZjIzNzEwNzRkMzc1MDc3ZDMwNDZlMTk1NTkyYjI0Y2FjYTUiLCJ0YWdzIjpbWyJsb2NrdGltZSIsIjE3NTk5NjQzNDAiXSxbInJlZnVuZCIsIjAzNTJiOWFmNGJhMWRiMDliM2Y2Y2E1NDRhNmExY2M0OTQ1ZGRhZjY4MmZjOTMwMzYwYzVlZGU4NGQzZjNjNTBhYiJdXX1dYWNYIQKStW3NIERz0XR--cXYAkfmmo8iDigAc-2F8TE2MYhALGFko2FlWCDXvqR7-X6gJJiHa1T6eBaWHMnGMseti7OrXgwYGI3432FzWCBC6iVGhNhgqNPhA54qlYReHkNOMPHdRkPd2mlGykeB8GFyWCAsniO_fjxZSrh4lSelhNMkBZmMnDL1sdblE82YjYyjgmJwcoJYIHbeRz2-u06KCI0NMjMl75Vf5jnnmXcuONIg9ZVTefklWCAvZpnxoEhgKLdl9lFoU5bF-hVT8YsSmtrjO88OI0UUUw | ||
| ``` | ||
|
|
||
| Decoded structure (`mint` truncated for clarity): | ||
|
|
||
| ```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' | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| 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 `PaymentRequest` object from [NUT-18][18] with an optional `b` field to signal P2BK support. | ||
|
|
||
| ### Extended PaymentRequest schema | ||
|
|
||
| ```json | ||
| { | ||
| "i": str <optional>, | ||
| "a": int <optional>, | ||
| "u": str <optional>, | ||
| "s": bool <optional>, | ||
| "m": Array[str] <optional>, | ||
| "d": str <optional>, | ||
| "t": Array[Transport] <optional>, | ||
| "b": bool <optional>, | ||
| "nut10": NUT10Option <optional>, | ||
| } | ||
| ``` | ||
|
|
||
| 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", | ||
| "b": true, | ||
| "nut10": { | ||
| "k": "P2PK", | ||
| "d": "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" | ||
| }, | ||
| "t": [ | ||
| { | ||
| "t": "post", | ||
| "a": "https://receiver.example.com/payment" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| In this example: | ||
|
|
||
| - 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 | ||
|
|
||
| ## Wallet workflow | ||
|
|
||
| ### 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$. | ||
| 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. | ||
|
|
||
| ### Receiver (spending P2BK proofs) | ||
|
|
||
| 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. | ||
|
|
||
| ## Determinism and canonicalisation | ||
|
|
||
| - Scalars MUST be formatted as lowercase 64-character hex strings (32 bytes). | ||
| - 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 | ||
|
|
||
| - 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. There must be one entry in `p2pk_r` for every blinded key. | ||
|
|
||
| ## 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. | ||
| - Metadata leakage: wallets MUST keep `p2pk_r` private between sender and receiver (do not leak to the Mint). | ||
a1denvalu3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ## Compatibility | ||
|
|
||
| - 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 | ||
|
|
||
| Using the sample proof above: | ||
|
|
||
| - Lock key (blinded): `038b5943fc368f259f3539a5b7ac217f2371074d375077d3046e195592b24caca5` | ||
| - Refund key (blinded): `0352b9af4ba1db09b3f6ca544a6a1cc4945ddaf682fc930360c5ede84d3f3c50ab` | ||
| - Scalars (hex): | ||
| - `r_0 = 76de473dbebb4e8a088d0d323325ef955fe639e799772e38d220f5955379f925` | ||
| - `r_1 = 2f6699f1a0486028b765f651685396c5fa1553f18b129adae33bcf0e23451453` | ||
|
|
||
| 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. | ||
|
|
||
| [00]: 00.md | ||
| [03]: 03.md | ||
| [05]: 05.md | ||
| [06]: 06.md | ||
| [08]: 08.md | ||
| [10]: 10.md | ||
| [11]: 11.md | ||
| [18]: 18.md | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.