Deterministic pay to public key generation #331

Open
lescuer97 wants to merge 3 commits from lescuer97/p2pk_recovery into main
lescuer97 commented 2026-01-19 14:42:20 +00:00 (Migrated from github.com)

Adds a standard way to generate keys for wallets to generate private keys for usage in P2PK operations.

Adds a standard way to generate keys for wallets to generate private keys for usage in P2PK operations. - [ ] cdk https://github.com/cashubtc/cdk/pull/1466 - [ ] coco https://github.com/cashubtc/coco/pull/121 - [ ] macadamia
prusnak (Migrated from github.com) reviewed 2026-01-19 14:42:20 +00:00
thesimplekid (Migrated from github.com) reviewed 2026-01-19 14:42:20 +00:00
a1denvalu3 (Migrated from github.com) reviewed 2026-01-19 14:42:20 +00:00
a1denvalu3 (Migrated from github.com) reviewed 2026-01-19 14:46:23 +00:00
@ -42,0 +47,4 @@
- 129373': Purpose picked for P2PK derivation.
- 10': Account for generating private keys for usage in P2PK.
- {counter}: Incrementing counter encoded as an unsigned 64-bit integer in big-endian format.
a1denvalu3 (Migrated from github.com) commented 2026-01-19 14:46:04 +00:00

is this using BIP32? Should be more clear.

is this using BIP32? Should be more clear.
a1denvalu3 (Migrated from github.com) reviewed 2026-01-19 14:48:27 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
a1denvalu3 (Migrated from github.com) commented 2026-01-19 14:48:28 +00:00
`m/129372'/10'/0'/0'/{counter}`:

not using per-keyset derivation. are we sure about this?

``` `m/129372'/10'/0'/0'/{counter}`: ``` not using per-keyset derivation. are we sure about this?
lescuer97 (Migrated from github.com) reviewed 2026-01-19 14:50:12 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
lescuer97 (Migrated from github.com) commented 2026-01-19 14:50:12 +00:00

yes, because it's for locking up proofs it should be more general

yes, because it's for locking up proofs it should be more general
robwoodgate commented 2026-01-19 16:54:57 +00:00 (Migrated from github.com)

Keysets v2 is moving away from a BIP32 style derivation scheme, and using a KDF instead. Would it not be better to align with this approach, eg using a domain separator like b"Cashu_KDF_HMAC_SHA256_P2PK" or b"Cashu_P2PK_v1"?

Also, I assume the counter is separate to the one used for secrets/blinded messages. If so, this should be made clearer.

Finally, related to @a1denvalu3's comment - what happens if the derived key is out of range? Should it be discarded (as per BIP-32) or reduced modulo N?

Keysets v2 is moving away from a BIP32 style derivation scheme, and using a KDF instead. Would it not be better to align with this approach, eg using a domain separator like `b"Cashu_KDF_HMAC_SHA256_P2PK"` or `b"Cashu_P2PK_v1"`? Also, I assume the counter is separate to the one used for secrets/blinded messages. If so, this should be made clearer. Finally, related to @a1denvalu3's comment - what happens if the derived key is out of range? Should it be discarded (as per BIP-32) or reduced modulo N?
lescuer97 (Migrated from github.com) reviewed 2026-01-20 14:07:31 +00:00
@ -42,0 +47,4 @@
- 129373': Purpose picked for P2PK derivation.
- 10': Account for generating private keys for usage in P2PK.
- {counter}: Incrementing counter encoded as an unsigned 64-bit integer in big-endian format.
lescuer97 (Migrated from github.com) commented 2026-01-20 14:07:31 +00:00

yes, I'll make it clearer

yes, I'll make it clearer
prusnak commented 2026-01-20 14:20:44 +00:00 (Migrated from github.com)

Keysets v2 is moving away from a BIP32 style derivation scheme, and using a KDF instead. Would it not be better to align with this approach, eg using a domain separator like b"Cashu_KDF_HMAC_SHA256_P2PK" or b"Cashu_P2PK_v1"?

Good point 👍

> Keysets v2 is moving away from a BIP32 style derivation scheme, and using a KDF instead. Would it not be better to align with this approach, eg using a domain separator like `b"Cashu_KDF_HMAC_SHA256_P2PK"` or `b"Cashu_P2PK_v1"`? Good point 👍
lescuer97 commented 2026-01-21 12:59:43 +00:00 (Migrated from github.com)

@robwoodgate @a1denvalu3 just changed the wording around the NUT so it's a bit more clear.

@robwoodgate @a1denvalu3 just changed the wording around the NUT so it's a bit more clear.
robwoodgate (Migrated from github.com) approved these changes 2026-01-22 21:04:43 +00:00
robwoodgate (Migrated from github.com) left a comment

Suggestion below to tighten the language, otherwise ACK.

Suggestion below to tighten the language, otherwise ACK.
robwoodgate (Migrated from github.com) reviewed 2026-01-22 21:05:36 +00:00
robwoodgate (Migrated from github.com) left a comment

Suggestion to tighten language, otherwise concept ACK

Suggestion to tighten language, otherwise concept ACK
@ -42,0 +49,4 @@
- 10': Account for generating private keys for usage in P2PK.
- {counter}: Incrementing counter encoded as an unsigned 64-bit integer in big-endian format.
This will allow wallets to swap proof that are still locked to a public key during a restore process.
robwoodgate (Migrated from github.com) commented 2026-01-22 21:05:09 +00:00
Wallets can deterministically generate private keys for P2PK using the following BIP32 derivation path: 

`m/129372'/10'/0'/0'/{counter}`

Where:
- Purpose' = `129372'` (UTF-8 for 🥜)
- Coin type' = `10'` - reserved for generating private keys.
- `{counter}` is an Incrementing counter, encoded as an unsigned 64-bit integer in big-endian format.

This allows wallets to swap Proofs still locked to a corresponding public key during a restore process.

In line with BIP-32, if the resulting private key is out of range (`> N`), it should be discarded.
```suggestion Wallets can deterministically generate private keys for P2PK using the following BIP32 derivation path: `m/129372'/10'/0'/0'/{counter}` Where: - Purpose' = `129372'` (UTF-8 for 🥜) - Coin type' = `10'` - reserved for generating private keys. - `{counter}` is an Incrementing counter, encoded as an unsigned 64-bit integer in big-endian format. This allows wallets to swap Proofs still locked to a corresponding public key during a restore process. In line with BIP-32, if the resulting private key is out of range (`> N`), it should be discarded. ```
prusnak (Migrated from github.com) reviewed 2026-01-22 22:13:07 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
prusnak (Migrated from github.com) commented 2026-01-22 22:13:07 +00:00

Can you explain why the last element is not hardened?

Is the xpub of m/129372'/10'/0'/0' ever shared anywhere?

If it is not shared, then instead of hardening it, I propose we change the scheme to Cashu_KDF_HMAC_SHA256 (same as used in Keyset v2, since BIP32 used in Keyset v1 is deprecated).

Can you explain why the last element is not hardened? Is the xpub of `m/129372'/10'/0'/0'` ever shared anywhere? If it is not shared, then instead of hardening it, I propose we change the scheme to `Cashu_KDF_HMAC_SHA256` (same as used in Keyset v2, since BIP32 used in Keyset v1 is deprecated).
lescuer97 (Migrated from github.com) reviewed 2026-01-26 14:54:23 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
lescuer97 (Migrated from github.com) commented 2026-01-26 14:54:23 +00:00

We are following what Bitcoin does when it comes to key derivation for locking.

there is no reason right now for sharing an XPUB but can't guarantee that in the future.

The reason we want to use bip32 is because Bip32 is specifically made for this case.The Cashu_KDF_HMAC_SHA256 scheme is used because of aggregation to avoid certain issues when generating the keyset id.

This keys are never aggregated and are use individually.

We are following what Bitcoin does when it comes to key derivation for locking. there is no reason right now for sharing an XPUB but can't guarantee that in the future. The reason we want to use bip32 is because Bip32 is specifically made for this case.The `Cashu_KDF_HMAC_SHA256` scheme is used because of aggregation to avoid certain issues when generating the keyset id. This keys are never aggregated and are use individually.
prusnak (Migrated from github.com) reviewed 2026-01-26 15:07:01 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
prusnak (Migrated from github.com) commented 2026-01-26 15:07:00 +00:00

The reason we want to use bip32 is because Bip32 is specifically made for this case.

No, it's not. BIP32 usecase is when you need to share a pubkey for a certain key subtree. Unless you need this requirement, going with Cashu_KDF_HMAC_SHA256 is simply faster and easier.

> The reason we want to use bip32 is because Bip32 is specifically made for this case. No, it's not. BIP32 usecase is when you need to share a pubkey for a certain key subtree. Unless you need this requirement, going with `Cashu_KDF_HMAC_SHA256` is simply faster and easier.
Egge21M (Migrated from github.com) reviewed 2026-01-26 16:39:44 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
Egge21M (Migrated from github.com) commented 2026-01-26 16:39:44 +00:00

@prusnak if we get the option to query the mint for quotes connected to a pubkey via NUT-20, public key derivation would become more meaningful than it currently is. I can not think of a usecase right now, but with this, extended keys would be able to essentially create a watch-only wallet of a wallets quotes, without spending from it.

So either we think about proper usecases for this for 2 weeks and go with HMAC if we can't find any, or we take the performance L, go with BIP32 and hope someone finds a usecase some day in the future

@prusnak if we get the option to query the mint for quotes connected to a pubkey via NUT-20, public key derivation would become more meaningful than it currently is. I can not think of a usecase right now, but with this, extended keys would be able to essentially create a watch-only wallet of a wallets quotes, without spending from it. So either we think about proper usecases for this for 2 weeks and go with HMAC if we can't find any, or we take the performance L, go with BIP32 and hope someone finds a usecase some day in the future
prusnak (Migrated from github.com) reviewed 2026-01-26 20:53:10 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
prusnak (Migrated from github.com) commented 2026-01-26 20:53:10 +00:00

if we want to have a watch-only wallet eventually which should show p2pk tokens too, this seems like a good use-case. ACK

if we want to have a watch-only wallet eventually which should show p2pk tokens too, this seems like a good use-case. ACK
thesimplekid (Migrated from github.com) reviewed 2026-03-27 14:17:56 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
thesimplekid (Migrated from github.com) commented 2026-03-27 14:13:17 +00:00

Think coco and cdk are not using 129372

The following BIP32 derivation path for derivation of the key: `m/129372'/10'/0'/0'/{counter}`:

- 129373': Registered SLIP-0044 coin type for Cashu.
Think coco and cdk are not using 129372 ```suggestion The following BIP32 derivation path for derivation of the key: `m/129372'/10'/0'/0'/{counter}`: - 129373': Registered SLIP-0044 coin type for Cashu. ```
thesimplekid (Migrated from github.com) commented 2026-03-27 14:16:52 +00:00
Wallet are able to generate private keys in a deterministic way to have proofs locked to them.
```suggestion Wallet are able to generate private keys in a deterministic way to have proofs locked to them. ```
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
3. `secret = hmac_digest` and `blinding_factor = hmac_digest % N`.
#### P2PK Derivation
thesimplekid (Migrated from github.com) commented 2026-03-27 14:16:00 +00:00

I wonder of this should just be a new nut would be easier to track who supports it and think we should avoid changing existing nuts where we can especially widely supported ones like nut13. Though it would be a very small nut.

I wonder of this should just be a new nut would be easier to track who supports it and think we should avoid changing existing nuts where we can especially widely supported ones like nut13. Though it would be a very small nut.
lescuer97 (Migrated from github.com) reviewed 2026-03-27 14:24:48 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
3. `secret = hmac_digest` and `blinding_factor = hmac_digest % N`.
#### P2PK Derivation
lescuer97 (Migrated from github.com) commented 2026-03-27 14:24:48 +00:00

I thought about the same but I don't think it's needed in this case. Maybe if we start making more deterministic generation we would move it there.

I thought about the same but I don't think it's needed in this case. Maybe if we start making more deterministic generation we would move it there.
lescuer97 (Migrated from github.com) reviewed 2026-03-27 14:44:18 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
lescuer97 (Migrated from github.com) commented 2026-03-27 14:44:18 +00:00

Thank you, I forgot about changing this I also changed the tests to the correct ones.

Thank you, I forgot about changing this I also changed the tests to the correct ones.
robwoodgate (Migrated from github.com) requested changes 2026-05-28 12:31:16 +00:00
robwoodgate (Migrated from github.com) left a comment

Honestly, I think we should use NUT-13's v2 HMAC-SHA256 KDF rather than introducing a new BIP32 path.

The KDF defined for v2 exists precisely because BIP32 path traversal is really expensive, and a P2PK private key only needs a uniform secp256k1 scalar, which the same single-HMAC + single-subtraction reduction (bias ~2⁻¹²⁸) gives you directly.

Quick numbers from a pure-JS noble benchmark (cashu-ts, per counter):

Pattern                                          Cost/key   vs HMAC
HMAC-SHA256                                       ~165 µs    1.0×
BIP-32 parent-cached, HDKey.publicKey             ~172 µs    1.0×    natural restore-loop code
BIP-32 master-cached, full path per derive        ~862 µs    5.2×    natural single-derive code
BIP-32 cold, no caching                          ~1216 µs    7.4×    worst case

In a restore situation, the wallet can cache the parent path derivation and get close to parity with HMAC, but if using just a cached master key (for regular wallet ops), the slowdown is 5x. In other words, BIP-32 needs careful handling to avoid performance tanking.

There is also no special need for BIP32 here: P2PK keys are derived wallet-side from a hot seed, so there's no hardware-wallet or chain-code benefit.

Mirroring the v2 construction — something like:

HMAC_SHA256(seed, "Cashu_KDF_HMAC_SHA256_NUT11" || counter_u64_be) reduced mod n

would keep a single KDF across all of NUT-13 and lines up with the direction v2 was already moving.

Honestly, I think we should use NUT-13's v2 HMAC-SHA256 KDF rather than introducing a new BIP32 path. The KDF defined for v2 exists precisely because BIP32 path traversal is really expensive, and a P2PK private key only needs a uniform secp256k1 scalar, which the same single-HMAC + single-subtraction reduction (bias ~2⁻¹²⁸) gives you directly. Quick numbers from a pure-JS noble benchmark (cashu-ts, per counter): ``` Pattern Cost/key vs HMAC HMAC-SHA256 ~165 µs 1.0× BIP-32 parent-cached, HDKey.publicKey ~172 µs 1.0× natural restore-loop code BIP-32 master-cached, full path per derive ~862 µs 5.2× natural single-derive code BIP-32 cold, no caching ~1216 µs 7.4× worst case ``` In a restore situation, the wallet can cache the parent path derivation and get close to parity with HMAC, but if using just a cached master key (for regular wallet ops), the slowdown is 5x. In other words, BIP-32 needs careful handling to avoid performance tanking. There is also no special need for BIP32 here: P2PK keys are derived wallet-side from a hot seed, so there's no hardware-wallet or chain-code benefit. Mirroring the v2 construction — something like: ``` HMAC_SHA256(seed, "Cashu_KDF_HMAC_SHA256_NUT11" || counter_u64_be) reduced mod n ``` would keep a single KDF across all of NUT-13 and lines up with the direction v2 was already moving.
robwoodgate (Migrated from github.com) reviewed 2026-05-28 13:44:08 +00:00
@ -39,6 +39,18 @@ The HMAC-SHA256 KDF is built as the following:
2. `hmac_digest = HMAC_SHA256(seed, message)`, where `HMAC_SHA256` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC) using SHA-256 as the hashing algorithm.
robwoodgate (Migrated from github.com) commented 2026-05-28 13:44:08 +00:00

The use case is double hypothetical - relying on wallets to export XPUBS and an endpoint that doesn't exist. BIP32 is a slowdown vs HMAC, a performance L to soak up forever.

And any lookup by pubkey would either be limited (pubkeys are SUPPOSED to be single use for privacy) or would likely allow the mint to link pubkeys (eg in a batch lookup scenario).

The use case is double hypothetical - relying on wallets to export XPUBS and an endpoint that doesn't exist. BIP32 is a slowdown vs HMAC, a performance L to soak up forever. And any lookup by pubkey would either be limited (pubkeys are SUPPOSED to be single use for privacy) or would likely allow the mint to link pubkeys (eg in a batch lookup scenario).
robwoodgate commented 2026-05-29 16:14:08 +00:00 (Migrated from github.com)

I have proposed a HMAC-SHA256 KDF derived alternative, which would close this PR

I have proposed a [HMAC-SHA256 KDF derived alternative](https://github.com/cashubtc/nuts/pull/384), which would close this PR
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin lescuer97/p2pk_recovery:lescuer97/p2pk_recovery
git switch lescuer97/p2pk_recovery

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff lescuer97/p2pk_recovery
git switch lescuer97/p2pk_recovery
git rebase main
git switch main
git merge --ff-only lescuer97/p2pk_recovery
git switch lescuer97/p2pk_recovery
git rebase main
git switch main
git merge --no-ff lescuer97/p2pk_recovery
git switch main
git merge --squash lescuer97/p2pk_recovery
git switch main
git merge --ff-only lescuer97/p2pk_recovery
git switch main
git merge lescuer97/p2pk_recovery
git push origin main
Sign in to join this conversation.
No description provided.