Problem
NUT-13 enables deterministic derivation of proof secrets (0x00) and blinding factors (0x01) from a wallet seed, allowing full ecash recovery via NUT-09.
NUT-20 quote signing keys are not covered. All known implementations ( cdk (Rust) and Nutshell (Python) ) generate them randomly:
These keys exist only in the local database. After device loss, the user cannot produce the NUT-20 signature required to claim proofs from a PAID quote even with the correct mnemonic and the quote ID.
What this solves
A user who has:
- Their mnemonic (seed)
- A quote ID (from their records or provided by the mint operator)
Can currently recover on mints without NUT-20: just call mint(quote_id). Cannot currently recover on mints with NUT-20: the signing key is lost, the mint rejects with error 20008. With deterministic derivation, the wallet regenerates the same signing key from the seed and produces a valid signature. Recovery works on all mints.
Proposed derivation
Use the same HMAC-SHA256 pattern as NUT-13 with a separate domain string, since quote keys are not tied to a keyset:
message = b"Cashu_KDF_HMAC_SHA256_QUOTE" || quote_counter_bytes
quote_signing_key = HMAC_SHA256(seed, message)
Where:
- quote_counter_bytes is an unsigned 64-bit integer in big-endian format
- The 32-byte HMAC digest is used as a secp256k1 secret key
- The corresponding public key is sent to the mint per NUT-20
The wallet persists a quote_counter (incremented per mint quote) alongside keyset counters.
NUT-20 recommends a unique public key per quote. Deterministic derivation preserves this — each counter value produces a unique, unlinkable key.
This proposal covers key derivation only. With the quote ID, recovery works immediately.
Full automatic recovery (without knowing the quote ID) would require a mint endpoint to look up quotes by public key. That could be defined in a separate NUT.
Problem
NUT-13 enables deterministic derivation of proof secrets (0x00) and blinding factors (0x01) from a wallet seed, allowing full ecash recovery via NUT-09.
NUT-20 quote signing keys are not covered. All known implementations ( cdk (Rust) and Nutshell (Python) ) generate them randomly:
These keys exist only in the local database. After device loss, the user cannot produce the NUT-20 signature required to claim proofs from a PAID quote even with the correct mnemonic and the quote ID.
What this solves
A user who has:
Can currently recover on mints without NUT-20: just call mint(quote_id). Cannot currently recover on mints with NUT-20: the signing key is lost, the mint rejects with error 20008. With deterministic derivation, the wallet regenerates the same signing key from the seed and produces a valid signature. Recovery works on all mints.
Proposed derivation
Use the same HMAC-SHA256 pattern as NUT-13 with a separate domain string, since quote keys are not tied to a keyset:
message = b"Cashu_KDF_HMAC_SHA256_QUOTE" || quote_counter_bytes
quote_signing_key = HMAC_SHA256(seed, message)
Where:
The wallet persists a quote_counter (incremented per mint quote) alongside keyset counters.
NUT-20 recommends a unique public key per quote. Deterministic derivation preserves this — each counter value produces a unique, unlinkable key.
This proposal covers key derivation only. With the quote ID, recovery works immediately.
Full automatic recovery (without knowing the quote ID) would require a mint endpoint to look up quotes by public key. That could be defined in a separate NUT.