Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions 26.md
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).

## 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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:

Expand Down