{"id":"954a737f8423705f09e7fcea5e91a064dac3ef41e205a03c2523e38d5e218165","pubkey":"d1678e7ef965374bbea308a1215609a78376dc158277a7d657680f9d5efd5c38","created_at":1782645004,"kind":30817,"tags":[["d","nut-28"],["title","NUT-28: Pay to Blinded Key (P2BK)"],["summary","Blinding the receiver's public key on each payment, so the mint cannot link several of them to the same person."],["s","optional"],["t","cashu"],["t","ecash"],["t","nut"],["alt","A specification: NUT-28: Pay to Blinded Key (P2BK)"],["client","openspecs-import"],["published_at","1771246416"],["proxy","https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/28.md","web"],["x","4ab1ad11242a4cb2242b528a1f04b7aa610c549bb318dc1ef9001d953aee46d0"]],"content":"# NUT-28: Pay-to-Blinded-Key (P2BK)\n\n`optional`\n\n`depends on: NUT-11`\n\n---\n\n## Summary\n\nThis NUT describes Pay-to-Blinded-Key (P2BK), which extends the [NUT-11][11] (P2PK) spending conditions. By implication, it also extends [NUT-14][14] (HTLC).\n\nP2BK preserves privacy by blinding each NUT-11 receiver pubkey `P` with an ECDH-derived scalar `rᵢ`. Both sides can deterministically derive the same `rᵢ` from their own keys, but a third party cannot. This improves user privacy by preventing the mint from linking multiple P2PK spends by the same party.\n\n## ECDH Shared Secret (Zx)\n\nElliptic-curve Diffie–Hellman (ECDH) allows two parties to create an x-coordinate shared secret (`Zx`) by combining their private key with the public key of the other party: `Zx = x(epG) = x(eP) = x(pE)`.\n\nFor P2BK, the sender creates an ephemeral keypair (private key: `e`, public key: `E`), which protects the privacy of their usual long-lived public key. They then calculate the shared secret by combining the ephemeral private key (`e`) and the receiver's long-lived public key (`P`).\n\nThe receiver calculates the same shared secret `Zx` using their private key (`p`) and the ephemeral public key (`E`), which is supplied by the sender in the [proof metadata](#proof-object-extension).\n\nThe shared secret `Zx` is then used to derive the blinded public keys.\n\n## Deriving Blinded Public Keys\n\nPer NUT-11, there are up to 11 locking 'slots' in the order: `[data, ...pubkeys, ...refund]`.\n\nSlot 0 is the `data` tag. Slots 1-10 can be any combination of `pubkeys` and `refund` keys.\n\nEach public key in the NUT-11 proof is permanently blinded using a deterministic blinding scalar (`rᵢ`), where `i` is the _slot index_.\n\nThe blinding scalar for each slot is calculated as:\n\n```\nrᵢ = SHA-256( DOMAIN_SEPARATOR || Zx || i_byte)\n```\n\nWhere:\n\n- `DOMAIN_SEPARATOR` constant byte string `b\"Cashu_P2BK_v1\"`\n- `Zx` is the ECDH shared secret (`eP` for sender, `pE` for receiver).\n- `i_byte` is the single unsigned byte representation of `i`: (`0x00` to `0x0A`)\n- `||` denotes concatenation\n\nIf `rᵢ` is not in the range `1 ≤ rᵢ ≤ n−1`, retry once with an extra `0xff` byte appended to the hash input as follows:\n\n```\nrᵢ = SHA-256( b\"Cashu_P2BK_v1\" || Zx || i_byte || 0xff )\n```\n\nIf `rᵢ` is still not in the range `1 ≤ rᵢ ≤ n−1`, abort and discard the ephemeral keypair.\n\nFinally, the public key (`P`) for slot `i` is blinded (`P'`) as follows:\n\n```\nP' = P + rᵢG\n```\n\n### Example\n\nBelow is an example implementation in TypeScript.\n\n```ts\nfunction deriveP2BKBlindingTweakFromECDH(\n  point: WeierstrassPoint<bigint>, // E or P\n  scalar: bigint, // p or e\n  slotIndex: number, // i\n): bigint {\n  // Calculate x-only ECDH shared point (Zx)\n  const Zx = point.multiply(scalar).toBytes(true).slice(1);\n  const iByte = new Uint8Array([slotIndex & 0xff]);\n  // Derive deterministic blinding factor (r):\n  // Note: bytesToNumber does NOT reduce modulo n\n  let r: bigint = bytesToNumber(sha256(Bytes.concat(P2BK_DST, Zx, iByte)));\n  if (r === 0n || r >= secp256k1.Point.CURVE().n) {\n    // Very unlikely to get here!\n    r = bytesToNumber(\n      sha256(Bytes.concat(P2BK_DST, Zx, iByte, new Uint8Array([0xff]))),\n    );\n    if (r === 0n || r >= secp256k1.Point.CURVE().n) {\n      // Astronomically unlikely to get here!\n      throw new Error(\"P2BK: tweak derivation failed\");\n    }\n  }\n  return r;\n}\n```\n\nFor detailed examples of slot blinding, see the [test vectors][tests].\n\n> [!IMPORTANT]\n> All receiver keys **MUST** be in compressed SEC1 format (33 bytes) before ECDH and blinding. \\\n> The sender **MUST add an '02' prefix** to BIP-340 x-only pubkeys (eg Nostr).\n\n## Proof Object Extension\n\nEach proof adds a single new metadata field:\n\n```jsonc\n{\n  \"amount\": int,\n  \"id\": hex_str,\n  \"secret\": str,          // still [\"P2PK\", {...}]\n  \"C\": hex_str,\n  \"p2pk_e\": hex_str       // NEW: 33-byte SEC1 compressed ephemeral public key E\n}\n```\n\n- `p2pk_e` contains the sender's ephemeral pubkey (`E`) used for blinding\n- All pubkeys inside the `\"P2PK\"` secret are the blinded forms `P'`\n- The mint sees standard P2PK data and remains unaware of the blinding\n- For Token V4 encoding, the `p2pk_e` field is named `pe`, and `E` is encoded as a 33 byte CBOR bstr\n\n## Deriving Private Keys\n\nWith P2BK, the NUT-11 public locking keys are permanently blinded. The mint sees only the blinded public keys, and expects signatures from the corresponding private key.\n\nThe receiver must therefore derive the correct blinded private key (`k`). Because BIP-340 lifts public keys to even-Y parity, there are two possible derivation paths:\n\n- Standard derivation: `k = (p + rᵢ) mod n`\n- Negated derivation: `k = (-p + rᵢ) mod n`\n\nWhere `p` is the receiver's long lived private key.\n\nTo decide which derivation to use, the receiver calculates their natural pubkey (`pG`) and compares the parity to their actual pubkey (`P`).\n\nIf the parity matches, use standard derivation, otherwise use negated derivation.\n\nThe fastest way to do this in a wallet is to unblind, verify the key is a match, then select derivation by parity:\n\na. compute `Rᵢ = rᵢG` \\\nb. unblind `P = P' − Rᵢ` \\\nc. verify `x(P) == x(pG)` \\\nd. use standard derivation if `parity(P) == parity(pG)`, otherwise use negated derivation\n\n## Sender Workflow\n\n1. Generate a fresh random scalar `e` and compute `E = eG`\n2. For **each receiver key** `P`, compute: \\\n   a. Unique shared secret for this key: `Zx = x(eP)` \\\n   b. Slot index `i` in `[data, ...pubkeys, ...refund]` \\\n   c. Blinding scalar: `rᵢ = SHA-256(b\"Cashu_P2BK_v1\" || Zx || i_byte)` \\\n   d. Blinded Public Key: `P' = P + rᵢG`\n3. Build the canonical P2PK secret with the blinded `P'` keys in their slots.\n4. Interact with the mint normally; the mint never learns `P` or `rᵢ`\n5. Include `p2pk_e = E` in the final proof\n\n> [!IMPORTANT]\n> Use a fresh ephemeral keypair (`e` / `E`) for each new output, so that every proof has\n> unique blinded keys and a unique `E` in the `Proof.p2pk_e` field.\n>\n> In the case of `SIG_ALL`, the **SAME** ephemeral keypair **MUST** be used for all\n> outputs, as all `SIG_ALL` proof secrets must have IDENTICAL `data` and `tags` fields.\n\n## Receiver Workflow\n\n1. Read `E` from `proof.p2pk_e` and the key slot order index `i` from `[data, ...pubkeys, ...refund]`\n2. Calculate your unique shared secret: `Zx = x(pE)`\n3. For each slot `i`, compute: \\\n   a. Blinding scalar: `rᵢ = SHA-256(b\"Cashu_P2BK_v1\" || Zx || i_byte)` \\\n   b. Compute `Rᵢ = rᵢG` \\\n   c. Unblind `P = P' − Rᵢ` \\\n   d. Verify `x(P) == x(pG)`. If it does not match, this `P'` is not for this private key, skip it. \\\n   e. Derive the secret key using:\n   - standard derivation if `parity(P) == parity(pG)`\n   - negative derivation otherwise\n4. Remove the `p2pk_e` field from the proof\n5. Sign with the derived private keys and spend as an ordinary P2PK proof\n\n> [!NOTE]\n> Each receiver can only calculate their OWN shared secret (`pE`), because a shared secret requires either the receiver's private key (`pE`) or the sender's ephemeral private key (`eP`).\n\n[11]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cnz43282n\n[14]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cngcq0g8r\n[tests]: https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/tests/28-tests.md\n","sig":"4b8a6df303d14631564c7b4b3a27b0fb92eddc0fbe0b9b9dd0601264f174aad3f90cd6d3e30e150926e557acb26aefef0f0e66ed9922cb181d1e88e2fcd46c20"}