Deterministic pay to public key generation#331
Conversation
|
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 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? |
Good point 👍 |
|
@robwoodgate @a1denvalu3 just changed the wording around the NUT so it's a bit more clear. |
robwoodgate
left a comment
There was a problem hiding this comment.
Suggestion to tighten language, otherwise concept ACK
There was a problem hiding this 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.
This comment has been minimized.
This comment has been minimized.
robwoodgate
left a comment
There was a problem hiding this comment.
Following offline discussion, and the fact this is already live, I've closed #384.
|
Cashu-TS support now added: |
…697) # NUTS: - cashubtc/nuts#331 - cashubtc/nuts#373 ## Summary Adds BIP-32 derivation of deterministic signing keys under `m/129373'/{purpose}'/0'/0'/{counter}` (non-hardened counter), as defined for: - [NUT-11](https://github.com/cashubtc/nuts/blob/main/11.md): P2PK keys (purpose index `10`) - [NUT-20](https://github.com/cashubtc/nuts/blob/main/20.md): quote locking keys (purpose index `20`) ## API ```ts export type Bip32KeyPurpose = 'P2PK' | 'QuoteLock'; deriveKeyPair(seed, purpose, counter): { pubkey: string; privkey: string } // one-shot createKeyPairDeriver(seed, purpose): (counter) => { pubkey: string; privkey: string } // cached, for loops ``` `purpose` is passed by name (`'P2PK'` / `'QuoteLock'`). Both return hex `pubkey`/`privkey` that drop straight into the existing lock/quote and `signP2PKProofs` APIs with zero conversion: ```ts const { pubkey, privkey } = deriveKeyPair(seed, 'P2PK', counter); await wallet.send(64, proofs, undefined, { send: { type: 'p2pk', options: { pubkey } } }); await wallet.send(64, lockedProofs, { privkey }); const { pubkey: qp, privkey: qk } = deriveKeyPair(seed, 'QuoteLock', counter); const quote = await wallet.createLockedMintQuote(amount, qp); await wallet.mintProofs('bolt11', amount, quote, { privkey: qk }); ``` `createKeyPairDeriver` caches the shared parent derivation, so restore scans cost a single non-hardened child derivation per counter (~5x faster than re-walking the full path) while still returning the ready-to-use hex keypair: ```ts const derive = createKeyPairDeriver(seed, 'P2PK'); for (let counter = 0; counter < gapLimit; counter++) { if (derive(counter).pubkey === targetPubkey) break; // matched } ``` ## Scope Crypto primitive only. Counter allocation and the quote/proof-to-counter mapping stay with the consumer; they need that mapping regardless, so a wallet-owned counter would add coupling without removing consumer state. No NUT-20 module changes needed: `createLockedMintQuote`/locked mint already accept the pubkey/privkey directly.
| - {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. | ||
|
|
There was a problem hiding this comment.
| In line with BIP-32, if the resulting private key is out of range (`> N`), it should be discarded. |
Adds a standard way to generate keys for wallets to generate private keys for usage in P2PK operations.