Keyset ID V2 #182

Merged
davidcaseria merged 62 commits from keyset-id-v2 into main 2026-01-11 09:04:57 +00:00
davidcaseria commented 2024-10-30 15:24:54 +00:00 (Migrated from github.com)

Summary

This PR introduces Keyset ID v2 (version byte 01) and updates the spec + vectors accordingly.

The Keyset ID v2 derivation is designed to be unambiguous across keysets by hashing both the key amounts and keyset metadata.

Changes

Keyset ID v2 (NUT-02)

  • Full keyset ID: 33 bytes (hex string length 66)
    • 01 (version byte) + SHA256(preimage) (32 bytes)
  • Short keyset ID (Token i field): first 8 bytes of the full keyset ID (hex string length 16)
    • Wallets MUST resolve short IDs to full IDs; ambiguous short IDs MUST fail parsing.

Derivation preimage (v2)

Preimage is constructed as UTF-8 bytes in this exact order:

  • For each key (sorted by amount ascending): append "{amount}:{pubkey_hex}"
  • Separate each amount:pubkey_hex pair with a comma (,)
  • Append "|unit:{unit}"
  • If input_fee_ppk is present and non-zero, append "|input_fee_ppk:{input_fee_ppk}"
    • If input_fee_ppk is omitted / null / 0, it MUST be omitted from the preimage.
  • If final_expiry is present and non-zero, append "|final_expiry:{final_expiry}"

Then id = "01" + sha256(preimage).hexdigest().

Deterministic secrets (NUT-13)

  • Version-based derivation: secret derivation method determined by keyset ID version.
    • 00: legacy BIP32 derivation
    • 01: HMAC-SHA256 derivation
  • HMAC-SHA256 message:
    • message = b"Cashu_KDF_HMAC_SHA256" || keyset_id_bytes || counter_k_bytes || derivation_type_byte

Spec/test-vector updates in this PR

  • Updated tests/02-tests.md V2 keyset IDs to match the new preimage rules (including input_fee_ppk behavior).
  • Updated 02.md examples to use fully specified vector keysets (no ellipses), so IDs are verifiable.
  • Updated tests/13-tests.md V2 secrets/r vectors to match the updated V2 keyset ID.
  • Added a small helper script tools/regenerate_vectors.py to regenerate/sanity-check the affected vectors.

Implementation Status

## Summary This PR introduces **Keyset ID v2** (version byte `01`) and updates the spec + vectors accordingly. The Keyset ID v2 derivation is designed to be **unambiguous** across keysets by hashing both the **key amounts** and **keyset metadata**. ## Changes ### Keyset ID v2 (NUT-02) - **Full keyset ID**: 33 bytes (hex string length 66) - `01` (version byte) + `SHA256(preimage)` (32 bytes) - **Short keyset ID** (Token `i` field): first 8 bytes of the full keyset ID (hex string length 16) - Wallets MUST resolve short IDs to full IDs; ambiguous short IDs MUST fail parsing. #### Derivation preimage (v2) Preimage is constructed as UTF-8 bytes in this exact order: - For each key (sorted by amount ascending): append `"{amount}:{pubkey_hex}"` - Separate each `amount:pubkey_hex` pair with a comma (`,`) - Append `"|unit:{unit}"` - If `input_fee_ppk` is present and **non-zero**, append `"|input_fee_ppk:{input_fee_ppk}"` - If `input_fee_ppk` is omitted / `null` / `0`, it MUST be omitted from the preimage. - If `final_expiry` is present and non-zero, append `"|final_expiry:{final_expiry}"` Then `id = "01" + sha256(preimage).hexdigest()`. ### Deterministic secrets (NUT-13) - **Version-based derivation**: secret derivation method determined by keyset ID version. - `00`: legacy BIP32 derivation - `01`: HMAC-SHA256 derivation - HMAC-SHA256 message: - `message = b"Cashu_KDF_HMAC_SHA256" || keyset_id_bytes || counter_k_bytes || derivation_type_byte` ## Spec/test-vector updates in this PR - Updated `tests/02-tests.md` V2 keyset IDs to match the new preimage rules (including `input_fee_ppk` behavior). - Updated `02.md` examples to use fully specified vector keysets (no ellipses), so IDs are verifiable. - Updated `tests/13-tests.md` V2 secrets/r vectors to match the updated V2 keyset ID. - Added a small helper script `tools/regenerate_vectors.py` to regenerate/sanity-check the affected vectors. ## Implementation Status - [ ] CDK https://github.com/cashubtc/cdk/pull/702 https://github.com/cashubtc/cdk/pull/1505 - [ ] cashu-ts https://github.com/cashubtc/cashu-ts/pull/458 - [ ] Nutshell https://github.com/cashubtc/nutshell/pull/798/ - [ ] gonuts - [ ] nutmix https://github.com/lescuer97/nutmix/pull/215
prusnak (Migrated from github.com) reviewed 2024-10-30 15:35:49 +00:00
@ -64,0 +74,4 @@
An example implementation in Python:
```python
prusnak (Migrated from github.com) commented 2024-10-30 15:35:48 +00:00

I am wondering ... since we already depend for CBOR (for Token v4), maybe we can use CBOR here too instead of this custom serialization?

I am wondering ... since we already depend for CBOR (for Token v4), maybe we can use CBOR here too instead of this custom serialization?
prusnak (Migrated from github.com) reviewed 2024-10-30 15:40:00 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
prusnak (Migrated from github.com) commented 2024-10-30 15:39:59 +00:00

What happens to the tokens when a mint generates a new keyset that collides with the already emitted tokens?

Let's say the old keyset id is 01cb2343...., the tokens use 01cb in the i field and then a new keyset is created which has id 01cb948.... The chance of this happening is 1:256 (around 0.4%) - if you generate more keysets, the chance is significantly higher.

What happens to the tokens when a mint generates a new keyset that collides with the already emitted tokens? Let's say the old keyset id is `01cb2343....`, the tokens use `01cb` in the `i` field and then a new keyset is created which has id `01cb948...`. The chance of this happening is 1:256 (around 0.4%) - if you generate more keysets, the chance is significantly higher.
a1denvalu3 (Migrated from github.com) reviewed 2024-10-30 16:30:26 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
a1denvalu3 (Migrated from github.com) commented 2024-10-30 16:30:26 +00:00

I strongly agree. I think we should leave at least 7 bytes of id in the token, like before.
(e.g. 01cb2343d31f10ff)

I strongly agree. I think we should leave at least 7 bytes of id in the token, like before. (e.g. `01cb2343d31f10ff`)
davidcaseria (Migrated from github.com) reviewed 2024-10-30 19:22:49 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
davidcaseria (Migrated from github.com) commented 2024-10-30 19:22:49 +00:00

What's the 95% case for how long it takes between a token being created and redeemed (i.e., the proofs in the token swapped)? I guess it's on the order of an hour, maybe longer if you're sending it to a new user who isn't active. What are the realistic odds that a keyset is created that also happens to collide with the same unit within that hour? That's why I thought this was acceptable.

On the other hand, given that this is still only a small reduction in token size, I'm fine with keeping the 7 bytes.

However, as I've stated before, I'm in favor of anything we can do to reduce the token size since it's a common issue. Maybe a compromise of 3 bytes instead of 7 bytes is worth it?

What's the 95% case for how long it takes between a token being created and redeemed (i.e., the proofs in the token swapped)? I guess it's on the order of an hour, maybe longer if you're sending it to a new user who isn't active. What are the realistic odds that a keyset is created that also happens to collide with the same unit within that hour? That's why I thought this was acceptable. On the other hand, given that this is still only a small reduction in token size, I'm fine with keeping the 7 bytes. However, as I've stated before, I'm in favor of anything we can do to reduce the token size since it's a common issue. Maybe a compromise of 3 bytes instead of 7 bytes is worth it?
davidcaseria (Migrated from github.com) reviewed 2024-10-30 19:24:09 +00:00
@ -64,0 +74,4 @@
An example implementation in Python:
```python
davidcaseria (Migrated from github.com) commented 2024-10-30 19:24:08 +00:00

What would the structure of the CBOR be?

What would the structure of the CBOR be?
callebtc commented 2024-10-31 15:54:29 +00:00 (Migrated from github.com)

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.
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.
ok300 (Migrated from github.com) reviewed 2024-11-05 18:10:09 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
ok300 (Migrated from github.com) commented 2024-11-05 18:10:08 +00:00

... the abbreviated id MUST be at least eight bytes (i.e., the version byte and first byte of the hash)

8 != 1 + 1

> ... the abbreviated `id` **MUST** be at least eight bytes (i.e., the version byte and first byte of the hash) 8 != 1 + 1
ok300 (Migrated from github.com) reviewed 2024-11-05 18:19:41 +00:00
@ -58,3 +58,3 @@
#### Keyset ID version
#### Keyset ID V2
ok300 (Migrated from github.com) commented 2024-11-05 18:19:41 +00:00

Once optimization could be to change b"unit:sat" -> b"u:sat" or even just b"sat".
(Edit: nevermind that, it will all be hashed in the end, so reducing the length of what's inside won't matter)

Might also be worth specifying the unit should be lowercased first.

~~Once optimization could be to change `b"unit:sat"` -> `b"u:sat"` or even just `b"sat"`.~~ _(Edit: nevermind that, it will all be hashed in the end, so reducing the length of what's inside won't matter)_ Might also be worth specifying the unit should be lowercased first.
davidcaseria (Migrated from github.com) reviewed 2024-11-07 19:42:18 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
davidcaseria (Migrated from github.com) commented 2024-11-07 19:42:17 +00:00

Oops. Thanks!

Oops. Thanks!
davidcaseria (Migrated from github.com) reviewed 2024-11-07 19:44:21 +00:00
@ -58,3 +58,3 @@
#### Keyset ID version
#### Keyset ID V2
davidcaseria (Migrated from github.com) commented 2024-11-07 19:44:21 +00:00

I don't think we have specified the units as case-insensitive yet. We should probably also do that if we include this change, right?

I don't think we have specified the units as case-insensitive yet. We should probably also do that if we include this change, right?
a1denvalu3 (Migrated from github.com) reviewed 2024-11-17 16:35:19 +00:00
@ -126,3 +170,3 @@
},
{
"id": "0042ade98b2a370a",
"id": "012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8",
a1denvalu3 (Migrated from github.com) commented 2024-11-17 16:35:19 +00:00

What happens if a wallet that does not yet support V2 keysets makes a GET v1/keys/{8-byte-keyset-id} request?

What happens if a wallet that does not yet support V2 keysets makes a `GET v1/keys/{8-byte-keyset-id}` request?
davidcaseria (Migrated from github.com) reviewed 2024-11-19 12:00:03 +00:00
@ -126,3 +170,3 @@
},
{
"id": "0042ade98b2a370a",
"id": "012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8",
davidcaseria (Migrated from github.com) commented 2024-11-19 12:00:03 +00:00

A request with a keyset ID prefix 00 should still be supported. A wallet that doesn't support keyset IDs with a prefix of 01 shouldn't get to where they are making this request since the keyset ID parsing will have already failed if the wallet is checking the version.

If a wallet doesn't check the version and sends a request for an 8-byte keyset ID from a token, we can consider having the mint resolve it for the wallet from the URL. But that feels like an optional requirement for the mint.

Does that address your question?

A request with a keyset ID prefix `00` should still be supported. A wallet that doesn't support keyset IDs with a prefix of `01` shouldn't get to where they are making this request since the keyset ID parsing will have already failed if the wallet is checking the version. If a wallet doesn't check the version and sends a request for an 8-byte keyset ID from a token, we can consider having the mint resolve it for the wallet from the URL. But that feels like an optional requirement for the mint. Does that address your question?
ok300 (Migrated from github.com) reviewed 2024-11-22 09:45:09 +00:00
@ -58,3 +58,3 @@
#### Keyset ID version
#### Keyset ID V2
ok300 (Migrated from github.com) commented 2024-11-22 09:45:09 +00:00

Since the keyset ID commits to the unit, then it would make sense IMO.

If a client derives the keyset ID using a different capitalization, or if the mint changes the capitalization later on, the keyset ID would be invalid.

Since the keyset ID commits to the unit, then it would make sense IMO. If a client derives the keyset ID using a different capitalization, or if the mint changes the capitalization later on, the keyset ID would be invalid.
callebtc (Migrated from github.com) reviewed 2024-11-24 12:06:07 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
callebtc (Migrated from github.com) commented 2024-11-24 12:06:07 +00:00

This seems too vague and leaves room for interpretation. It could simply say that the token must include the first 8 bytes / 16 hex characters of the keyset ID it originates from.

This seems too vague and leaves room for interpretation. It could simply say that the token must include the first 8 bytes / 16 hex characters of the keyset ID it originates from.
callebtc (Migrated from github.com) reviewed 2024-11-24 12:08:15 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
callebtc (Migrated from github.com) commented 2024-11-24 12:08:15 +00:00

We could make it more explicit and call it s_id which is id_hex[:16] or id_bytes[:8].

We could make it more explicit and call it `s_id` which is `id_hex[:16]` or `id_bytes[:8]`.
davidcaseria (Migrated from github.com) reviewed 2024-11-25 16:26:20 +00:00
@ -58,3 +58,3 @@
#### Keyset ID version
#### Keyset ID V2
davidcaseria (Migrated from github.com) commented 2024-11-25 16:26:20 +00:00

@callebtc what do you think about units being case-insensitive and hashing the lowercase unit value?

@callebtc what do you think about units being case-insensitive and hashing the lowercase unit value?
a1denvalu3 commented 2025-02-01 14:22:09 +00:00 (Migrated from github.com)

Should we maybe add a paragraph to this that describes the way the Mint handles proofs with Short IDv2?
The Mint calculates each keyset's short IDv2 in addition to IDv2 and uses the short IDv2s for lookups in cases where it receives proofs with short IDv2.

Alternatively a substitution could be performed by the client, but this means that it has to request v1/keys so it's an additional more request before a swap.

Should we maybe add a paragraph to this that describes the way the Mint handles proofs with Short IDv2? The Mint calculates each keyset's short IDv2 in addition to IDv2 and uses the short IDv2s for lookups in cases where it receives proofs with short IDv2. Alternatively a substitution could be performed by the client, but this means that it has to request `v1/keys` so it's an additional more request before a swap.
prusnak commented 2025-02-01 14:51:01 +00:00 (Migrated from github.com)

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?

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 commented 2025-02-02 01:37:34 +00:00 (Migrated from github.com)

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

@prusnak this was suggested to enable wallets to store proofs without referencing mint URLs. @thesimplekid @ok300 do you think this is still necessary?
thesimplekid commented 2025-02-03 10:05:20 +00:00 (Migrated from github.com)

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.

> 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.
thesimplekid (Migrated from github.com) reviewed 2025-02-03 10:06:08 +00:00
@ -58,3 +58,3 @@
#### Keyset ID version
#### Keyset ID V2
thesimplekid (Migrated from github.com) commented 2025-02-03 10:06:08 +00:00

We should specify we hash the lower case.

We should specify we hash the lower case.
prusnak commented 2025-02-03 11:01:49 +00:00 (Migrated from github.com)

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.

> 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 commented 2025-02-07 16:41:29 +00:00 (Migrated from github.com)

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.

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.
a1denvalu3 commented 2025-03-31 09:08:38 +00:00 (Migrated from github.com)

@davidcaseria 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
@davidcaseria 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
a1denvalu3 commented 2025-04-02 12:58:27 +00:00 (Migrated from github.com)
CDK draft: https://github.com/cashubtc/cdk/pull/702
prusnak commented 2025-04-02 13:19:28 +00:00 (Migrated from github.com)

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?

> 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?
a1denvalu3 commented 2025-04-02 13:32:48 +00:00 (Migrated from github.com)

@prusnak For simmetry w.r.t. V1 so that the length is still a power of 2. I think it's less confusing if we keep the same process and only change what is needed to be changed.

@prusnak For simmetry w.r.t. V1 so that the length is still a power of 2. I think it's less confusing if we keep the same process and only change what is needed to be changed.
prusnak commented 2025-04-02 14:29:08 +00:00 (Migrated from github.com)

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.

> 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 commented 2025-04-02 17:14:12 +00:00 (Migrated from github.com)

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.

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 `Proof`s 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 `id`s are resolvable), the wallet **MUST** error. The full keyset `id` is only needed when the wallet interacts with the mint.
ok300 (Migrated from github.com) reviewed 2025-04-09 03:17:20 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
ok300 (Migrated from github.com) commented 2025-04-09 03:17:20 +00:00
### Short ID

The short keyset ID (`s_id`) is defined as the abbreviated representation of the full Keyset ID. The length of the `s_id` is eight bytes (i.e., the version byte and the first seven bytes of the hash: `id_bytes[:8]` or `id_hex[:16]`).

The `s_id` is only relevant for wallets, when encoding and decoding `Tokens`. To save space, a `Token`, as defined in [NUT-00][00], **SHOULD** use the short keyset ID `s_id` in the `Proof`. The recommended exception to that is when the `Proofs` reference a keyset where the `s_id` is ambiguous on that mint; in that case, the sending wallet **SHOULD** instead use the full keyset ID. Receiving wallets **MUST** therefore be able to decode both `Tokens` with a short keyset ID, as well as those with a full keyset ID.

When receiving a `Token` with a `s_id`, a wallet **MUST** resolve the abbreviated `s_id` to the full `id`. If the `s_id` is ambiguous (i.e., multiple keyset `id`s are resolvable on the same mint), the wallet **MUST** error. Otherwise, if the `Token` is successfully parsed, the wallet **SHOULD** save the full-length keyset `id` with proofs in its database.

The mint is unaware of the `s_id`. All API calls exposed by the mint use the full keyset ID.

As discussed, the ambiguity mentioned above ( https://github.com/cashubtc/nuts/pull/182#issuecomment-2773223419 ) is intentional and the spec should allow for both short and long Keyset IDs in the Token.

@davidcaseria I re-worded the paragraph that originally confused me. It now spells out in more detail who should use the s_id when.

```suggestion ### Short ID The short keyset ID (`s_id`) is defined as the abbreviated representation of the full Keyset ID. The length of the `s_id` is eight bytes (i.e., the version byte and the first seven bytes of the hash: `id_bytes[:8]` or `id_hex[:16]`). The `s_id` is only relevant for wallets, when encoding and decoding `Tokens`. To save space, a `Token`, as defined in [NUT-00][00], **SHOULD** use the short keyset ID `s_id` in the `Proof`. The recommended exception to that is when the `Proofs` reference a keyset where the `s_id` is ambiguous on that mint; in that case, the sending wallet **SHOULD** instead use the full keyset ID. Receiving wallets **MUST** therefore be able to decode both `Tokens` with a short keyset ID, as well as those with a full keyset ID. When receiving a `Token` with a `s_id`, a wallet **MUST** resolve the abbreviated `s_id` to the full `id`. If the `s_id` is ambiguous (i.e., multiple keyset `id`s are resolvable on the same mint), the wallet **MUST** error. Otherwise, if the `Token` is successfully parsed, the wallet **SHOULD** save the full-length keyset `id` with proofs in its database. The mint is unaware of the `s_id`. All API calls exposed by the mint use the full keyset ID. ``` As discussed, the ambiguity mentioned above ( https://github.com/cashubtc/nuts/pull/182#issuecomment-2773223419 ) is intentional and the spec should allow for both short and long Keyset IDs in the `Token`. @davidcaseria I re-worded the paragraph that originally confused me. It now spells out in more detail who should use the `s_id` when.
ok300 (Migrated from github.com) approved these changes 2025-04-09 11:17:03 +00:00
ok300 (Migrated from github.com) reviewed 2025-04-14 05:58:31 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
ok300 (Migrated from github.com) commented 2025-04-14 05:58:31 +00:00

NIT: Duplicate line, it's already included in the last part of the paragraph on L27 above.

```suggestion ``` NIT: Duplicate line, it's already included in the last part of the paragraph on L27 above.
a1denvalu3 (Migrated from github.com) approved these changes 2025-04-29 10:56:38 +00:00
a1denvalu3 commented 2025-05-01 17:21:58 +00:00 (Migrated from github.com)

Good to merge?? Who's ACK-ing?

Good to merge?? Who's ACK-ing?
thesimplekid (Migrated from github.com) requested changes 2025-05-02 13:17:43 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
thesimplekid (Migrated from github.com) commented 2025-05-02 13:14:11 +00:00
A keyset `id` is an identifier for a specific keyset. It can be derived by anyone who knows the set of public keys of a mint. Wallets **MAY** compute the keyset `id` for a given keyset themselves to confirm that the mint is supplying the correct keyset ID (see below).
```suggestion A keyset `id` is an identifier for a specific keyset. It can be derived by anyone who knows the set of public keys of a mint. Wallets **MAY** compute the keyset `id` for a given keyset themselves to confirm that the mint is supplying the correct keyset ID (see below). ```
thesimplekid (Migrated from github.com) commented 2025-05-02 13:15:33 +00:00
When receiving a `Token` with a `s_id`, a wallet **MUST** resolve the abbreviated `s_id` to the full `id`. If the `s_id` is ambiguous (i.e., multiple keyset `id`s are resolvable on the same mint), the wallet **MUST** error. If the `Token` is successfully parsed, the wallet **SHOULD** save the full-length keyset `id` with proofs in its database.
```suggestion When receiving a `Token` with a `s_id`, a wallet **MUST** resolve the abbreviated `s_id` to the full `id`. If the `s_id` is ambiguous (i.e., multiple keyset `id`s are resolvable on the same mint), the wallet **MUST** error. If the `Token` is successfully parsed, the wallet **SHOULD** save the full-length keyset `id` with proofs in its database. ```
@ -64,0 +65,4 @@
```
1 - sort public keys by their amount in ascending numerical order
2 - concatenate each amount and its corresponding lowercase public key hex string (as "amount:publickey_hex") to a single byte array, separating each pair with a comma (",")
3 - add the lowercase UTF8-encoded unit string prefixed with "|unit:" to the byte array (e.g. "|unit:sat")
thesimplekid (Migrated from github.com) commented 2025-05-02 13:10:06 +00:00
3 - add the lowercase unit string to the byte array (e.g. "unit:sat")
```suggestion 3 - add the lowercase unit string to the byte array (e.g. "unit:sat") ```
@ -20,0 +40,4 @@
3. `secret = hmac_digest` and `blinding_factor = hmac_digest % N`.
### Code Examples
thesimplekid (Migrated from github.com) commented 2025-05-02 13:08:21 +00:00
The integer representation `keyset_id_int` of a keyset is calculated from its [hexadecimal ID][02] which has a length of 8 bytes or 16 hex characters. First, we convert the hex string to a big-endian sequence of bytes. This value is then modulo reduced by `2^31 - 1` to arrive at an integer that is a unique identifier `keyset_id_int`. Keyset IDs with version prefix `01` **MUST** be shortened to the first 8 bytes before conversion.

If all do not do this seeds will not be transferable between implementations.

```suggestion The integer representation `keyset_id_int` of a keyset is calculated from its [hexadecimal ID][02] which has a length of 8 bytes or 16 hex characters. First, we convert the hex string to a big-endian sequence of bytes. This value is then modulo reduced by `2^31 - 1` to arrive at an integer that is a unique identifier `keyset_id_int`. Keyset IDs with version prefix `01` **MUST** be shortened to the first 8 bytes before conversion. ``` If all do not do this seeds will not be transferable between implementations.
thesimplekid commented 2025-05-05 21:46:19 +00:00 (Migrated from github.com)
ACK 0f3cef68eaab308d07a441f55ff37540f864faa5
thesimplekid commented 2025-05-16 15:09:32 +00:00 (Migrated from github.com)

closes #229

closes #229
thesimplekid commented 2025-06-19 15:08:06 +00:00 (Migrated from github.com)
Merged in cdk https://github.com/cashubtc/cdk/pull/702
thesimplekid (Migrated from github.com) approved these changes 2025-06-19 15:08:33 +00:00
a1denvalu3 commented 2025-07-30 16:40:17 +00:00 (Migrated from github.com)

@davidcaseria I think the safest option is to version the secret derivation (like we're doing with the keysets) and then specify that wallets should follow:

  • version 1 (legacy, with BIP32) for keysets with ID v1
  • version 2 (the new one, with HMAC-SHA512) for keysets with ID v2

This way wallets have immediate way of knowing which derivation to use (or was used, in the case of restore) when generating secrets for a particular keyset, and we eliminate all "counter"-based issues.

@davidcaseria I think the safest option is to version the secret derivation (like we're doing with the keysets) and then specify that wallets should follow: - version 1 (legacy, with BIP32) for keysets with ID v1 - version 2 (the new one, with HMAC-SHA512) for keysets with ID v2 This way wallets have immediate way of knowing which derivation to use (or was used, in the case of restore) when generating secrets for a particular keyset, and we eliminate all "counter"-based issues.
davidcaseria (Migrated from github.com) reviewed 2025-07-31 11:35:57 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
davidcaseria (Migrated from github.com) commented 2025-07-31 11:35:49 +00:00

Change to SHOULD

Change to **SHOULD**
Egge21M (Migrated from github.com) reviewed 2025-08-18 08:47:33 +00:00
@ -12,11 +12,207 @@ In this document, we describe the process that allows wallets to recover their e
Egge21M (Migrated from github.com) commented 2025-08-18 08:47:08 +00:00

Shouldn't these both be hmac_digest[:32] as the derivation_type_byte now creates different outputs based on derivation type?

Shouldn't these both be hmac_digest[:32] as the `derivation_type_byte` now creates different outputs based on derivation type?
a1denvalu3 (Migrated from github.com) reviewed 2025-08-18 08:49:06 +00:00
@ -12,11 +12,207 @@ In this document, we describe the process that allows wallets to recover their e
a1denvalu3 (Migrated from github.com) commented 2025-08-18 08:49:06 +00:00

Yes you're correct.

Yes you're correct.
a1denvalu3 (Migrated from github.com) reviewed 2025-08-18 08:50:48 +00:00
@ -12,11 +12,207 @@ In this document, we describe the process that allows wallets to recover their e
a1denvalu3 (Migrated from github.com) commented 2025-08-18 08:50:48 +00:00
3. `secret = hmac_digest[:32]` or `blinding_factor = hmac_digest[:32]`.

@davidcaseria

```suggestion 3. `secret = hmac_digest[:32]` or `blinding_factor = hmac_digest[:32]`. ``` @davidcaseria
a1denvalu3 commented 2025-08-18 09:48:03 +00:00 (Migrated from github.com)
added test vectors: https://github.com/davidcaseria/nuts/pull/6
callebtc (Migrated from github.com) reviewed 2025-08-28 08:42:47 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
callebtc (Migrated from github.com) commented 2025-08-28 08:42:47 +00:00
When receiving a `Token` with a `s_id`, wallet **MUST** resolve the abbreviated `s_id` to the full `id` to parse the `Proofs` contained in it. If the `s_id` is ambiguous (i.e., multiple keyset `id`s are resolvable on the same mint), the wallet **MUST** error. 
```suggestion When receiving a `Token` with a `s_id`, wallet **MUST** resolve the abbreviated `s_id` to the full `id` to parse the `Proofs` contained in it. If the `s_id` is ambiguous (i.e., multiple keyset `id`s are resolvable on the same mint), the wallet **MUST** error. ```
callebtc (Migrated from github.com) reviewed 2025-08-28 08:46:39 +00:00
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
callebtc (Migrated from github.com) commented 2025-08-28 08:46:38 +00:00
Wallets use `s_id` to save space when encoding and decoding `Tokens` as defined in [NUT-00][00]. The recommended exception to that is when the `s_id` is ambiguous; in that case, the sending wallet **SHOULD** instead use the full keyset ID. Receiving wallets **MUST** be able to decode both `Tokens` with a short keyset ID, as well as those with a full keyset ID.
```suggestion Wallets use `s_id` to save space when encoding and decoding `Tokens` as defined in [NUT-00][00]. The recommended exception to that is when the `s_id` is ambiguous; in that case, the sending wallet **SHOULD** instead use the full keyset ID. Receiving wallets **MUST** be able to decode both `Tokens` with a short keyset ID, as well as those with a full keyset ID. ```
prusnak commented 2025-08-28 09:00:42 +00:00 (Migrated from github.com)

Important technical note:

You are using hmac-sha512 and then using only the first 256 bits of the result.

If hmac-sha256 is used, then you can use the whole result and sha256 is MUCH more friendly for 32-bit embedded devices such as hardware wallets or smartcards. Furthermore, often these devices contain accelerators for sha256 but not for sha512.

Processor SHA-256 cycles/block SHA-512 cycles/block
32-bit ~800 ~2500
64-bit ~600 ~800

I suggest changing hmac-sha512 to hmac-sha256 for this reason

Important technical note: You are using hmac-sha512 and then using only the first 256 bits of the result. If hmac-sha256 is used, then you can use the whole result and sha256 is MUCH more friendly for 32-bit embedded devices such as hardware wallets or smartcards. Furthermore, often these devices contain accelerators for sha256 but not for sha512. | Processor | SHA-256 cycles/block | SHA-512 cycles/block | | ------- | ------- | ------- | | 32-bit | ~800 | ~2500 | | 64-bit | ~600 | ~800 | I suggest changing hmac-sha512 to hmac-sha256 for this reason
prusnak (Migrated from github.com) reviewed 2025-08-28 09:34:37 +00:00
@ -64,0 +63,4 @@
Keyset IDs are derived from public data. To derive the keyset ID of a keyset, execute the following steps:
```
1 - sort public keys by their amount in ascending numerical order
prusnak (Migrated from github.com) commented 2025-08-28 09:34:37 +00:00

can we add delimiters here?

they won't change the size of the result and make it so much better for debugging implementations

example

    sorted_keys = dict(sorted(keys.items()))
    keyset_id_bytes = b",".join([p.serialize() for p in sorted_keys.values()])
    keyset_id_bytes += b"|unit:sat"
    keyset_id_bytes += b"|final_expiry:"+str(1896187313).encode("utf-8")
can we add delimiters here? they won't change the size of the result and make it so much better for debugging implementations example ```suggestion sorted_keys = dict(sorted(keys.items())) keyset_id_bytes = b",".join([p.serialize() for p in sorted_keys.values()]) keyset_id_bytes += b"|unit:sat" keyset_id_bytes += b"|final_expiry:"+str(1896187313).encode("utf-8") ```
prusnak (Migrated from github.com) reviewed 2025-08-28 09:35:48 +00:00
@ -12,11 +12,207 @@ In this document, we describe the process that allows wallets to recover their e
prusnak (Migrated from github.com) commented 2025-08-28 09:35:48 +00:00

let's make it obvious this is a binary zero or one and not ascii zero or one:

     - `0x00` for secrets
     - `0x01` for blinded messages
let's make it obvious this is a binary zero or one and not ascii zero or one: ```suggestion - `0x00` for secrets - `0x01` for blinded messages ```
asmogo (Migrated from github.com) reviewed 2025-08-28 10:01:37 +00:00
asmogo (Migrated from github.com) left a comment

Suggesting minor changes.
Updated KDF with hmac_digest_secret and hmac_digest_blinding_factor.

Suggesting minor changes. Updated KDF with hmac_digest_secret and hmac_digest_blinding_factor.
@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on
asmogo (Migrated from github.com) commented 2025-08-28 08:48:50 +00:00
The mint is unaware of the `s_id`. All API endpoints exposed by the mint **MUST** use the full keyset ID.
```suggestion The mint is unaware of the `s_id`. All API endpoints exposed by the mint **MUST** use the full keyset ID. ```
@ -64,0 +63,4 @@
Keyset IDs are derived from public data. To derive the keyset ID of a keyset, execute the following steps:
```
1 - sort public keys by their amount in ascending numerical order
asmogo (Migrated from github.com) commented 2025-08-28 08:53:40 +00:00
Everybody can derive a keyset ID from the keysets of a mint. The keyset ID is a lower-case hex string. To derive the keyset ID of a keyset, execute the following steps:
```suggestion Everybody can derive a keyset ID from the keysets of a mint. The keyset ID is a lower-case hex string. To derive the keyset ID of a keyset, execute the following steps: ```
asmogo (Migrated from github.com) commented 2025-08-28 08:54:05 +00:00
2 - concatenate all public keys to a single byte array
```suggestion 2 - concatenate all public keys to a single byte array ```
@ -82,0 +112,4 @@
> [!CRITICAL]
> Wallet implementations should reject any attempt at importing new keysets which IDs
> collide with any of the previously added keysets.
asmogo (Migrated from github.com) commented 2025-08-28 08:58:55 +00:00
The final expiry **MAY** be `null` if the keyset has no final-expiry.
```suggestion The final expiry **MAY** be `null` if the keyset has no final-expiry. ```
asmogo (Migrated from github.com) commented 2025-08-28 08:39:46 +00:00

why are we calling this final?

why are we calling this `final`?
@ -12,11 +12,207 @@ In this document, we describe the process that allows wallets to recover their e
asmogo (Migrated from github.com) commented 2025-08-28 09:03:57 +00:00
In order to do this, the wallet keeps track of a `counter_k` for each `keyset_k` it uses. The index `k` indicates that the wallet **MUST** keep track of a separate counter for each keyset `k` it uses. The wallet **MUST** keep track of multiple keysets for every mint it interacts with.
```suggestion In order to do this, the wallet keeps track of a `counter_k` for each `keyset_k` it uses. The index `k` indicates that the wallet **MUST** keep track of a separate counter for each keyset `k` it uses. The wallet **MUST** keep track of multiple keysets for every mint it interacts with. ```
asmogo (Migrated from github.com) commented 2025-08-28 09:14:53 +00:00
2. `hmac_digest_secret = HMAC_SHA512(seed, secret_message)` and `hmac_digest_blinding_factor = HMAC_SHA512(seed, blinding_factor_message)`, where `HMAC_SHA512` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC), using SHA-512 as the hashing algorithm.
3. `secret = hmac_digest_secret[:32]` or `blinding_factor = hmac_digest_blinding_factor[:32]`.
```suggestion 2. `hmac_digest_secret = HMAC_SHA512(seed, secret_message)` and `hmac_digest_blinding_factor = HMAC_SHA512(seed, blinding_factor_message)`, where `HMAC_SHA512` is the [hash-based message authentication code](https://en.wikipedia.org/wiki/HMAC), using SHA-512 as the hashing algorithm. 3. `secret = hmac_digest_secret[:32]` or `blinding_factor = hmac_digest_blinding_factor[:32]`. ```
@ -14,3 +14,3 @@
The wallet generates a `private_key` derived from a 12-word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet uses the `private_key`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates.
The wallet generates a `seed` derived from a 12-word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet uses the `seed`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates.
asmogo (Migrated from github.com) commented 2025-08-28 09:02:47 +00:00
The wallet **MUST** generate a `seed` derived from a 12 - 24 word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet **MUST** use the `seed`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates.
```suggestion The wallet **MUST** generate a `seed` derived from a 12 - 24 word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet **MUST** use the `seed`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates. ```
a1denvalu3 (Migrated from github.com) reviewed 2025-08-28 11:09:29 +00:00
a1denvalu3 (Migrated from github.com) commented 2025-08-28 11:09:29 +00:00

When I first wrote this what I imagined could happen is at some point we'll want other expiry fields (for example active_expiry to signal when this keyset is expected to be made inactive). final_expiry is stronger and signals a potential deletion from the DB.

When I first wrote this what I imagined could happen is at some point we'll want other expiry fields (for example `active_expiry` to signal when this keyset is expected to be made inactive). `final_expiry` is stronger and signals a potential deletion from the DB.
a1denvalu3 commented 2025-08-28 11:20:30 +00:00 (Migrated from github.com)

sha256 is MUCH more friendly for 32-bit embedded devices such as hardware wallets or smartcards.

This might be a good reason to change to SHA-256.

> sha256 is MUCH more friendly for 32-bit embedded devices such as hardware wallets or smartcards. This might be a good reason to change to SHA-256.
davidcaseria (Migrated from github.com) reviewed 2025-08-28 22:43:59 +00:00
@ -14,3 +14,3 @@
The wallet generates a `private_key` derived from a 12-word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet uses the `private_key`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates.
The wallet generates a `seed` derived from a 12-word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet uses the `seed`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates.
davidcaseria (Migrated from github.com) commented 2025-08-28 22:43:59 +00:00

I think this suggestion was nacked in previous conversations.

I think this suggestion was nacked in previous conversations.
prusnak (Migrated from github.com) reviewed 2025-09-01 08:56:38 +00:00
prusnak (Migrated from github.com) left a comment
No description provided.
We need to change SHA512 -> SHA256 in domain separation purpose strings.
a1denvalu3 commented 2025-09-06 08:51:25 +00:00 (Migrated from github.com)
@davidcaseria https://github.com/davidcaseria/nuts/pull/8
d4rp4t commented 2025-09-20 16:11:18 +00:00 (Migrated from github.com)

I believe final_expiry should be included in GetKeysResponse. Without it, the user cannot derive the keyset ID directly from this response and is forced to make an additional API call to fetch the keysets.

https://github.com/davidcaseria/nuts/pull/9

I believe final_expiry should be included in GetKeysResponse. Without it, the user cannot derive the keyset ID directly from this response and is forced to make an additional API call to fetch the keysets. https://github.com/davidcaseria/nuts/pull/9
robwoodgate commented 2025-09-20 16:35:20 +00:00 (Migrated from github.com)

Further to @d4rp4t comment, the '/v1/keys' endpoint should ideally return ALL keyset fields for the active or selected keyset... this allows a consumer to call just ONE endpoint and get the full keyset data - fees, expiry, keys etc etc

That way, the '/v1/keysets' endpoint can just be called when historic or inactive keyset data is required

Further to @d4rp4t comment, the '/v1/keys' endpoint should ideally return ALL keyset fields for the active or selected keyset... this allows a consumer to call just ONE endpoint and get the full keyset data - fees, expiry, keys etc etc That way, the '/v1/keysets' endpoint can just be called when historic or inactive keyset data is required
callebtc commented 2025-09-23 18:17:37 +00:00 (Migrated from github.com)

Further to @d4rp4t comment, the '/v1/keys' endpoint should ideally return ALL keyset fields for the active or selected keyset... this allows a consumer to call just ONE endpoint and get the full keyset data - fees, expiry, keys etc etc

That way, the '/v1/keysets' endpoint can just be called when historic or inactive keyset data is required

I think this is out of scope for this PR.

Nevertheless: wallets should hit /keysets for metadata, and /keys for the actual keys.

> Further to @d4rp4t comment, the '/v1/keys' endpoint should ideally return ALL keyset fields for the active or selected keyset... this allows a consumer to call just ONE endpoint and get the full keyset data - fees, expiry, keys etc etc > > That way, the '/v1/keysets' endpoint can just be called when historic or inactive keyset data is required I think this is out of scope for this PR. Nevertheless: wallets should hit `/keysets` for metadata, and `/keys` for the actual keys.
callebtc (Migrated from github.com) reviewed 2025-09-23 18:19:23 +00:00
@ -64,0 +63,4 @@
Keyset IDs are derived from public data. To derive the keyset ID of a keyset, execute the following steps:
```
1 - sort public keys by their amount in ascending numerical order
callebtc (Migrated from github.com) commented 2025-09-23 18:19:23 +00:00
    if final_expiry:
        keyset_id_bytes += b"|final_expiry:"+str(1896187313).encode("utf-8")

this should be conditional on whether final_expiry is set.

```suggestion if final_expiry: keyset_id_bytes += b"|final_expiry:"+str(1896187313).encode("utf-8") ``` this should be conditional on whether `final_expiry` is set.
callebtc (Migrated from github.com) reviewed 2025-09-23 18:45:42 +00:00
@ -64,0 +63,4 @@
Keyset IDs are derived from public data. To derive the keyset ID of a keyset, execute the following steps:
```
1 - sort public keys by their amount in ascending numerical order
callebtc (Migrated from github.com) commented 2025-09-23 18:45:41 +00:00

Commit #b25abaa changed the keyset ID calculation to include a separator but test vectors and implementations didn't. Needs to be resolved.

Commit #b25abaa changed the keyset ID calculation to include a separator but test vectors and implementations didn't. Needs to be resolved.
callebtc (Migrated from github.com) requested changes 2025-09-23 18:45:54 +00:00
callebtc (Migrated from github.com) left a comment

Commit #b25abaa changed the keyset ID calculation to include a separator but test vectors and implementations didn't. Needs to be resovled.

Commit #b25abaa changed the keyset ID calculation to include a separator but test vectors and implementations didn't. Needs to be resovled.
a1denvalu3 commented 2025-09-23 22:54:11 +00:00 (Migrated from github.com)

I propose two mutually exclusive courses of action for resolving #b25abaa ID calculation:

  1. We drop the separators: since the keys are serialized to bytes, the result would be unreadable anyway;
  2. We keep the separators, and change the wording around so that the keys are serialized then converted into hex strings. That way it actually makes sense to have separators.

Course of action 2 also requires changes across all implementations and test vectors.

I propose two mutually exclusive courses of action for resolving #b25abaa ID calculation: 1. We drop the separators: since the keys are serialized to bytes, the result would be unreadable anyway; 2. We keep the separators, and change the wording around so that the keys are serialized then converted into hex strings. That way it actually makes sense to have separators. Course of action 2 also requires changes across all implementations and test vectors.
thesimplekid commented 2025-09-24 08:17:36 +00:00 (Migrated from github.com)

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

This makes sense to me.

> We drop the separators: since the keys are serialized to bytes, the result would be unreadable anyway; This makes sense to me.
prusnak commented 2025-09-24 09:55:58 +00:00 (Migrated from github.com)

I prefer option 2.

If this has been already implemented in CDK or other implementations, I can live with 1. But we should know better and not push discussed drafts into production before they are accepted.

I prefer option 2. If this has been already implemented in CDK or other implementations, I can live with 1. But we should know better and not push discussed drafts into production before they are accepted.
robwoodgate commented 2025-09-24 10:10:29 +00:00 (Migrated from github.com)

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

> 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 commented 2025-09-24 12:28:12 +00:00 (Migrated from github.com)

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 |).

> 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 `|`).
a1denvalu3 commented 2025-09-24 12:32:56 +00:00 (Migrated from github.com)

I'm also favorable to 1.

I'm also favorable to 1.
callebtc (Migrated from github.com) reviewed 2025-09-24 12:36:28 +00:00
@ -12,11 +12,207 @@ In this document, we describe the process that allows wallets to recover their e
callebtc (Migrated from github.com) commented 2025-09-24 12:36:27 +00:00

redundant comments?

redundant comments?
a1denvalu3 commented 2025-09-24 13:07:53 +00:00 (Migrated from github.com)
https://github.com/davidcaseria/nuts/pull/11
thesimplekid commented 2025-10-18 22:27:38 +00:00 (Migrated from github.com)
ACK 1efb5a4b50a1f97475212523e40706870f88eec0
d4rp4t (Migrated from github.com) approved these changes 2025-10-18 22:31:00 +00:00
SatsAndSports commented 2025-10-26 22:46:41 +00:00 (Migrated from github.com)

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 (https://github.com/cashubtc/nuts/pull/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: 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 (https://github.com/cashubtc/nuts/pull/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 commented 2025-11-17 13:51:51 +00:00 (Migrated from github.com)

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.

> 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 commented 2025-12-16 15:35:21 +00:00 (Migrated from github.com)
ACK https://github.com/cashubtc/nuts/commit/1efb5a4b50a1f97475212523e40706870f88eec0
a1denvalu3 commented 2025-12-30 11:10:05 +00:00 (Migrated from github.com)
ACK https://github.com/cashubtc/nuts/pull/182/commits/9f5a7471523f04e2deab8d3f0a535dc60e3f26cd
thesimplekid commented 2025-12-30 11:52:23 +00:00 (Migrated from github.com)
ACK 9f5a7471523f04e2deab8d3f0a535dc60e3f26cd
asmogo (Migrated from github.com) approved these changes 2025-12-30 14:52:23 +00:00
callebtc (Migrated from github.com) reviewed 2026-01-03 09:16:45 +00:00
@ -102,12 +323,14 @@ If the wallet used the restore endpoint [NUT-09][09] for regenerating the `Proof
### Restoring batches
callebtc (Migrated from github.com) commented 2026-01-03 09:16:45 +00:00

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

Seems unnecessary to try both methods, assuming that we transition to the new KDF only for V2 keysets
asmogo (Migrated from github.com) approved these changes 2026-01-03 09:30:06 +00:00
thesimplekid (Migrated from github.com) approved these changes 2026-01-03 10:46:11 +00:00
thesimplekid (Migrated from github.com) left a comment
ACK 97b957304c2bd2c23eb2c03c278c10f19ce11693
robwoodgate (Migrated from github.com) reviewed 2026-01-03 13:22:01 +00:00
robwoodgate (Migrated from github.com) left a comment

Just flagging one area of concern.

Just flagging one area of concern.
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
robwoodgate (Migrated from github.com) commented 2026-01-03 13:21:12 +00:00

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?

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?
callebtc (Migrated from github.com) reviewed 2026-01-06 13:54:50 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
callebtc (Migrated from github.com) commented 2026-01-06 13:54:49 +00:00

This is a really good point. It would change the derivation algorithm and all test vectors etc.

This is a really good point. It would change the derivation algorithm and all test vectors etc.
prusnak (Migrated from github.com) reviewed 2026-01-06 14:08:37 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
prusnak (Migrated from github.com) commented 2026-01-06 14:08:37 +00:00

Not sure what is the state of implementation and mainly usage of Keyset ID v2, but I think it is worth the change if the usage is non-existent or minimal.

If we will be changing this, I would suggest to include my suggestion which was dismissed earlier: https://github.com/cashubtc/nuts/pull/182#discussion_r2306837257

Not sure what is the state of implementation and mainly usage of Keyset ID v2, but I think it is worth the change if the usage is non-existent or minimal. If we will be changing this, I would suggest to include my suggestion which was dismissed earlier: https://github.com/cashubtc/nuts/pull/182#discussion_r2306837257
Egge21M commented 2026-01-06 15:34:59 +00:00 (Migrated from github.com)

Re: Derivation and invalid results (k > N or k == 0)

Usually you would k % N to get the value into range, but because of limitations of some implementations this is not possible. BIP-32 defines

In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.)

I think this should be our default for all derivations too. It requires implementations to bubble up that error to the application layer to properly increment counters (e.g. counter 2 is requested, but produces an invalid result, so counter 3 will be consumed), but that is a sound way to handle this edge case

Re: Derivation and invalid results (k > N or k == 0) Usually you would `k % N` to get the value into range, but because of limitations of some implementations this is not possible. BIP-32 defines > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.) I think this should be our default for all derivations too. It requires implementations to bubble up that error to the application layer to properly increment counters (e.g. counter 2 is requested, but produces an invalid result, so counter 3 will be consumed), but that is a sound way to handle this edge case
robwoodgate commented 2026-01-06 15:55:27 +00:00 (Migrated from github.com)

Re: Derivation and invalid results (k > N or k == 0)

In an ideal world, we would reduce mod N, which then reduces the failure to r = 0 only, a 1 in 2^256 event equivalent to guessing a private key.

But if we cannot do this reliably in all implementations, it may be "prudent" to throw for values > N, specifically in the case of SHA256 digests, as it removes the miniscule risk of some implementations not being able to derive blinding factors for certain hashes... which Murphy's law says will be the most expensive mistake possible.

Maybe that's why BIP32 specified as discard and retry, not reduce %N.

Really wish mod n wasn't an issue, the single subtract is so elegant.

> Re: Derivation and invalid results (k > N or k == 0) In an ideal world, we would reduce mod N, which then reduces the failure to `r = 0` only, a 1 in 2^256 event equivalent to guessing a private key. But if we cannot do this reliably in all implementations, it may be "prudent" to throw for values > N, specifically in the case of SHA256 digests, as it removes the miniscule risk of some implementations not being able to derive blinding factors for certain hashes... which Murphy's law says will be the most expensive mistake possible. Maybe that's why BIP32 specified as discard and retry, not reduce %N. Really wish mod n wasn't an issue, the single subtract is so elegant.
robwoodgate commented 2026-01-06 15:59:45 +00:00 (Migrated from github.com)

Cross referencing the NUT-00 conventions PR, which currently warns against doing MOD N. https://github.com/cashubtc/nuts/pull/322

Will need to update this if we proceed with the the proposed blinding scheme here.

Cross referencing the NUT-00 conventions PR, which currently warns against doing MOD N. https://github.com/cashubtc/nuts/pull/322 Will need to update this if we proceed with the the proposed blinding scheme here.
a1denvalu3 (Migrated from github.com) reviewed 2026-01-07 10:30:18 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
a1denvalu3 (Migrated from github.com) commented 2026-01-07 10:30:18 +00:00

@callebtc @prusnak @robwoodgate https://github.com/a1denvalu3/nutshell/pull/6/changes

I have a sub-branch of my Nutshell IDv2 implementation with the modified derivation scheme and adjusted test vectors IF we want to go ahead with this.

@callebtc @prusnak @robwoodgate https://github.com/a1denvalu3/nutshell/pull/6/changes I have a sub-branch of my Nutshell IDv2 implementation with the modified derivation scheme and adjusted test vectors **IF** we want to go ahead with this.
SatsAndSports (Migrated from github.com) reviewed 2026-01-07 14:56:27 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
SatsAndSports (Migrated from github.com) commented 2026-01-07 14:56:27 +00:00

And maybe include the feerate input_fee_ppk in the id too?

In the mint changes the feerate, that might break some pre-signed transactions, and therefore I'd like to discourage changing of feerates.

If a pre-signed swap spends from a keyset, and the mint changes the input_fee_ppk of that keyset before the swap is executed, the swap might fail because it won't have enough inputs to cover the outputs

If the mint wants to change the feerate, they could de-activate the keyset and create a new keyset with the new feerate

And maybe include the feerate `input_fee_ppk` in the id too? In the mint changes the feerate, that might break some pre-signed transactions, and therefore I'd like to discourage changing of feerates. If a pre-signed swap spends from a keyset, and the mint changes the `input_fee_ppk` of that keyset before the swap is executed, the swap might fail because it won't have enough inputs to cover the outputs If the mint wants to change the feerate, they could de-activate the keyset and create a new keyset with the new feerate
SatsAndSports (Migrated from github.com) reviewed 2026-01-07 15:18:46 +00:00
SatsAndSports (Migrated from github.com) commented 2026-01-07 15:18:46 +00:00

This implies that, if a keyset has no final expiry, then the response must include final_expiry: null. But I guess we want to make that optional? i.e. wallets won't expect the final_expiry field to be present

Can we make that explicit somewhere here? i.e. The mint may omit input_fee_ppk if the fee is 0, or maybe may omit the final_expiry if there is no expiry

This implies that, if a keyset has no final expiry, then the response must include `final_expiry: null`. But I guess we want to make that optional? i.e. wallets won't expect the `final_expiry` field to be present Can we make that explicit somewhere here? i.e. The mint may omit `input_fee_ppk` if the fee is 0, or ~maybe~ may omit the `final_expiry` if there is no expiry
SatsAndSports (Migrated from github.com) reviewed 2026-01-07 15:20:53 +00:00
SatsAndSports (Migrated from github.com) commented 2026-01-07 15:20:53 +00:00

How about including one v1 keyset in this example response, if only to remind wallet authors that mints are likely to include both v1 and v2 keysets?

How about including one v1 keyset in this example response, if only to remind wallet authors that mints are likely to include both v1 and v2 keysets?
SatsAndSports (Migrated from github.com) reviewed 2026-01-07 16:07:04 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
SatsAndSports (Migrated from github.com) commented 2026-01-07 16:07:03 +00:00

A bit pedantic, but as the amounts from /v1/keys are strings, not numbers, we should be clear about the order:

1 - sort public keys by their amount in ascending order. As the amount in the mint's /v1/keys response is a string, e.g. `"32"`, we sort by increasing numerical - not lexicographical - order
A bit pedantic, but as the amounts from `/v1/keys` are strings, not numbers, we should be clear about the order: ```suggestion 1 - sort public keys by their amount in ascending order. As the amount in the mint's /v1/keys response is a string, e.g. `"32"`, we sort by increasing numerical - not lexicographical - order ```
callebtc (Migrated from github.com) reviewed 2026-01-08 04:52:08 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
callebtc (Migrated from github.com) commented 2026-01-08 04:52:07 +00:00

f we will be changing this, I would suggest to include my suggestion which was dismissed earlier: #182 (comment)

yes! we need to add separators if we append the amounts as well, let's combine both changes

> f we will be changing this, I would suggest to include my suggestion which was dismissed earlier: [#182 (comment)](https://github.com/cashubtc/nuts/pull/182#discussion_r2306837257) yes! we need to add separators if we append the amounts as well, let's combine both changes
callebtc (Migrated from github.com) reviewed 2026-01-08 04:56:22 +00:00
callebtc (Migrated from github.com) commented 2026-01-08 04:56:22 +00:00

that's how we usually indicate that a value is optional through the spec.

is it an equivalent "json" to omit a key if the value is null? i.e.{a: 1, b: null} == {a: 1} ?

that's how we usually indicate that a value is optional through the spec. is it an equivalent "json" to omit a key if the value is `null`? i.e.`{a: 1, b: null} == {a: 1} `?
callebtc (Migrated from github.com) reviewed 2026-01-08 05:01:58 +00:00
callebtc (Migrated from github.com) commented 2026-01-08 05:01:58 +00:00

I think it's fine, since we're deprecating v1 with this.

I think it's fine, since we're deprecating v1 with this.
callebtc commented 2026-01-08 06:10:34 +00:00 (Migrated from github.com)

I think all points should be addressed now.

I think all points should be addressed now.
robwoodgate commented 2026-01-08 08:12:18 +00:00 (Migrated from github.com)

@callebtc - the changes are looking great. I think we are there!
Just one housekeeping comment - should verify_02.py be placed under tools instead of in the root folder?

@callebtc - the changes are looking great. I think we are there! Just one housekeeping comment - should `verify_02.py` be placed under tools instead of in the root folder?
thesimplekid (Migrated from github.com) reviewed 2026-01-08 09:33:16 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
thesimplekid (Migrated from github.com) commented 2026-01-08 09:33:15 +00:00

And maybe include the feerate input_fee_ppk in the id too?

In the mint changes the feerate, that might break some pre-signed transactions, and therefore I'd like to discourage changing of feerates.

This would be a deviation from current spec as mints currently are allowed to change fee of a past keysets. Though I'm not sure if nutshell allows it, cdk doesn't it rotates to a new keyset as you suggested but that was an implementation choice not a spec requirement.

I think there are arguments for and against allowing mints to change keyset fees that are unrelated to the id.

> And maybe include the feerate input_fee_ppk in the id too? > In the mint changes the feerate, that might break some pre-signed transactions, and therefore I'd like to discourage changing of feerates. This would be a deviation from current spec as mints currently are allowed to change fee of a past keysets. Though I'm not sure if nutshell allows it, cdk doesn't it rotates to a new keyset as you suggested but that was an implementation choice not a spec requirement. I think there are arguments for and against allowing mints to change keyset fees that are unrelated to the id.
robwoodgate (Migrated from github.com) reviewed 2026-01-08 09:55:11 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
robwoodgate (Migrated from github.com) commented 2026-01-08 09:55:11 +00:00

And maybe include the feerate input_fee_ppk in the id too?

In the mint changes the feerate, that might break some pre-signed transactions, and therefore I'd like to discourage changing of feerates.
I think there are arguments for and against allowing mints to change keyset fees that are unrelated to the id.

FYI. Calle's latest iteration has fee baked in.

> > And maybe include the feerate input_fee_ppk in the id too? > > > In the mint changes the feerate, that might break some pre-signed transactions, and therefore I'd like to discourage changing of feerates. > I think there are arguments for and against allowing mints to change keyset fees that are unrelated to the id. FYI. Calle's latest iteration has fee baked in.
lescuer97 commented 2026-01-08 10:17:50 +00:00 (Migrated from github.com)

should we add a couple test vectors for the preimage?

should we add a couple test vectors for the preimage?
robwoodgate (Migrated from github.com) approved these changes 2026-01-08 11:36:02 +00:00
robwoodgate (Migrated from github.com) left a comment

ACK @ 6e785b2524

Epic work, I think this is a much stronger spec now.

ACK @ 6e785b2524e73a5d17bfd845b26df91e16a36aaa Epic work, I think this is a much stronger spec now.
prusnak (Migrated from github.com) reviewed 2026-01-08 11:47:03 +00:00
@ -64,3 +93,4 @@
V1 keysets are 8 bytes long, including a version byte prefix `00`.
```
1 - sort public keys by their amount in ascending order
prusnak (Migrated from github.com) commented 2026-01-08 11:47:03 +00:00

Looking at this code it occurred to me, that this works fine only if there are no duplicate entries in key values of keys. And this seems to be the case since keys is expected to be a dict. (otherwise the keys would need to be a list of tuples instead of a dict).

However, does the keyset spec disallow multiple public keys with the same denomination? I don't think we should allow this, but in that case, we should add this limitation to the keyset spec. Or is such limitation already in the keyset spec?

Looking at this code it occurred to me, that this works fine only if there are no duplicate entries in key values of `keys`. And this seems to be the case since `keys` is expected to be a dict. (otherwise the `keys` would need to be a list of tuples instead of a dict). However, does the keyset spec disallow multiple public keys with the same denomination? I don't think we should allow this, but in that case, we should add this limitation to the keyset spec. Or is such limitation already in the keyset spec?
thesimplekid (Migrated from github.com) reviewed 2026-01-08 12:31:34 +00:00
thesimplekid (Migrated from github.com) commented 2026-01-08 12:31:34 +00:00

I think it's worth noting that including the fee in the ID now means that mints cannot change the fee of past keysets. This to some extent reduces the mint's ability to dynamically change fees in response to a DoS, for example, as the first operation will still have the lower fee, only the second operation will incur the higher fee of the new keyset. I am not necessarily against this change; I just think we should be aware of it and make sure we are making this tradeoff intentionally.

I think it's worth noting that including the fee in the ID now means that mints cannot change the fee of past keysets. This to some extent reduces the mint's ability to dynamically change fees in response to a DoS, for example, as the first operation will still have the lower fee, only the second operation will incur the higher fee of the new keyset. I am not necessarily against this change; I just think we should be aware of it and make sure we are making this tradeoff intentionally.
callebtc (Migrated from github.com) approved these changes 2026-01-08 17:41:40 +00:00
asmogo (Migrated from github.com) reviewed 2026-01-08 20:48:49 +00:00
@ -56,11 +56,41 @@ Notice that since transactions can spend inputs from different keysets, the sum
asmogo (Migrated from github.com) commented 2026-01-08 20:36:19 +00:00

We should explicitly mention that these keys must be lowercase because SHA-256 is case-sensitive, while hex is not.

2 - concatenate each amount and its corresponding lowercase public key hex string (as "amount:publickey_hex") to a single byte array, separating each pair with a comma (",")
We should explicitly mention that these keys must be lowercase because SHA-256 is case-sensitive, while hex is not. ```suggestion 2 - concatenate each amount and its corresponding lowercase public key hex string (as "amount:publickey_hex") to a single byte array, separating each pair with a comma (",") ```
Sign in to join this conversation.
No description provided.