Skip to content

Conversation

@davidcaseria
Copy link
Contributor

@davidcaseria davidcaseria commented Oct 30, 2024

Summary

This PR introduces keyset ID version 2 and updates the key derivation function to improve security and eliminate mint ambiguity.

Changes

Keyset ID v2 (NUT-02)

  • Full keyset ID: 33 bytes (version 01 + 32-byte SHA256 hash)
  • Token keyset ID: 8 bytes (version byte + first 7 bytes of hash)
  • Keyset derivation now includes unit and optional final expiry timestamp
  • Wallets expand abbreviated token IDs to full keyset IDs
  • Added final_expiry field for keyset expiration

Versioned Secret Derivation (NUT-13)

  • Version-based derivation: Secret derivation method determined by keyset ID version
  • Legacy support: Keyset version 00 uses BIP32 derivation (backward compatible)
  • Keyset version 01 uses HMAC-SHA512 derivation
  • Wallets determine correct method by inspecting keyset version
  • Uses HMAC_SHA512(seed, "Cashu_KDF_HMAC_SHA512" || keyset_id_bytes || counter_bytes)

Implementation Status

@callebtc
Copy link
Contributor

Thank you!

Please:

  • add a verbose description in the PR what this is about
  • we should keep at least 7 bytes, as others have mentioned
  • this PR seems to replace the old keyset ID instead of adding a new version. Everything would break. We should instead add a new additional version.

@davidcaseria davidcaseria marked this pull request as ready for review November 1, 2024 19:34
@davidcaseria davidcaseria changed the title Define Keyset ID V2 Keyset ID V2 Nov 1, 2024
@prusnak
Copy link
Collaborator

prusnak commented Feb 1, 2025

Quite honest, I am not sure such small change justifies creating a new keyset version and the headache attached to it.

Maybe we should keep this improvement in mind and apply it together with some future proposal that changes something significant?

@davidcaseria
Copy link
Contributor Author

@prusnak this was suggested to enable wallets to store proofs without referencing mint URLs. @thesimplekid @ok300 do you think this is still necessary?

@thesimplekid
Copy link
Collaborator

this was suggested to enable wallets to store proofs without referencing mint URLs.

The longer keyset ids to reduce the chance of a collision and the keyset expiry are things we've talked about awhile and I would like to see, I think it makes sense to move forward with this. I cant think of anything else we wanted to include that we should hold this for, but if there is I am open to it.

@prusnak
Copy link
Collaborator

prusnak commented Feb 3, 2025

the keyset expiry are things we've talked about awhile and I would like to see, I think it makes sense to move forward with this

I agree. This is exactly the kind of change I was mentioning when I said to "justify" creating a new keyset version.

@ok300
Copy link
Contributor

ok300 commented Feb 7, 2025

A few NITs, otherwise it's an ACK from me:

The specified behavior is a bit ambiguous in 2 places.

The keyset id is in each Proof so it can be used by wallets to identify which mint and keyset it was generated from.

This doesn't say if it's the long or short ID. It should probably say "short ID" or rename the variable to s_id for clarity.

To save space, a Token, as defined in [NUT-00][00], SHOULD contain an abbreviated version of the keyset id in the Proof, (the s_id).

This makes it sound preferable, but optional to use the shorter s_id in the proof, whereas the PR text indicates it's mandatory, since the Token size is kept unchanged.

@prusnak
Copy link
Collaborator

prusnak commented Apr 2, 2025

I think it would be better if the long keyset ID was 32-bytes:

  • Keyset version byte
  • the first 31-bytes out of the 32-bytes SHA-256 digest

Why?

@prusnak
Copy link
Collaborator

prusnak commented Apr 2, 2025

I think it's less confusing if we keep the same process and only change what is needed to be changed.

It is non-standard and does not bring any single benefit imo. Only headache.

@ok300
Copy link
Contributor

ok300 commented Apr 2, 2025

IMO there is an ambiguity in the new spec:

To save space, a Token, as defined in [NUT-00][00], SHOULD contain an abbreviated version of the keyset id in the Proof, (the s_id).

This implies the Proofs in a Token could use either the short ID or the long ID.

Is there any reason where the long ID makes sense?

If not, I'd suggest to change that SHOULD to a MUST.


The only reason I can think of is to avoid short ID collisions, but that's already covered by another section:

If the abbreviated s_id is ambiguous (i.e., multiple keyset ids are resolvable), the wallet MUST error. The full keyset id is only needed when the wallet interacts with the mint.

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
@robwoodgate
Copy link
Contributor

I think this is out of scope for this PR.

Fair point - was just that adding final_expiry to keys in this PR reminded me of it.

I've raised a seperate issue for consideration: #289

@callebtc
Copy link
Contributor

But we should know better and not push discussed drafts into production before they are accepted.

agreed

  • We drop the separators: since the keys are serialized to bytes, the result would be unreadable anyway;

having two different separators is ugly and we're only going to hash the result anyway.

I'd also vote for 1 in favor of dropping all separators (, and |).

13.md Outdated
Comment on lines 168 to 170
// Derive using BIP32 paths:
// secret_path = `m/129372'/0'/{keysetIdInt}'/{counterK}'/0`
// r_path = `m/129372'/0'/{keysetIdInt}'/{counterK}'/1`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant comments?

@thesimplekid
Copy link
Collaborator

ACK 1efb5a4

@SatsAndSports
Copy link

SatsAndSports commented Oct 26, 2025

Update: this comment is now OUTDATED. See @callebtc below

Could we add an active_at_least_until field in active keysets?

As the SIG_ALL message change (#302) commits the outputs to a particular keyset, and holders of (partially-) signed transactions might want to be able to hold the transactions for a longer time, they would like some confidence that an active keyset in their output will remain active

One example of this is the Cashu Channel, where - like in Lightning - you want users to keep their transactions away from mint until their are ready to close the channel.

@callebtc
Copy link
Contributor

Could we add an active_at_least_until field in active keysets?

As the SIG_ALL message change (#302) commits the outputs to a particular keyset, and holders of (partially-) signed transactions might want to be able to hold the transactions for a longer time, they would like some confidence that an active keyset in their output will remain active

One example of this is the Cashu Channel, where - like in Lightning - you want users to keep their transactions away from mint until their are ready to close the channel.

Update: SIG_ALL does not contain the keyset ID anymore, I think we can mark this comment as outdated.

@asmogo
Copy link

asmogo commented Dec 16, 2025

ACK 1efb5a4

@thesimplekid
Copy link
Collaborator

ACK 9f5a747

13.md Outdated
Comment on lines 339 to 343
- Set `counter = 0`
- Restore `Proofs` in batches of 100 using the legacy derivation, and increment `counter`
- Repeat until three consecutive batches are returned empty
- Reset `counter` to the value at the last successful restore + 1
- Restore `Proofs` in batches of 100 using the HMAC-SHA256 derivation, and increment `counter`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems unnecessary to try both methods, assuming that we transition to the new KDF only for V2 keysets

Copy link
Collaborator

@thesimplekid thesimplekid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 97b9573

Copy link
Contributor

@robwoodgate robwoodgate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just flagging one area of concern.

Comment on lines +66 to +71
1 - sort public keys by their amount in ascending order
2 - concatenate all public keys to a single byte array
3 - add the lowercase UTF8-encoded unit string to the byte array (e.g. "unit:sat")
4 - If a final expiration is specified, add the UTF8-encoded string (e.g "final_expiry:1896187313")
4 - HASH_SHA256 the concatenated byte array
5 - prefix it with a keyset ID version byte "01"
Copy link
Contributor

@robwoodgate robwoodgate Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyset ID derivation doesn't protect/guarantee the amounts have not been tampered with, only that the amounts are still ordered the same.

What stops a mint from changing the amounts bound to each key to devalue and "inflate" away their liabilities?

eg: 1, 2, 4, 8, 16, 32, 64

could become: 1, 2, 3, 4, 8, 16, 32
or maybe even: 1, 1, 1, 1, 1, 1, 1

Perhaps we should concat all the amounts in order too... or maybe just adding the sum of key amounts would be enough to reduce the risk of serious damage?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants