Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f3f0280
Define Keyset ID V2
davidcaseria Oct 30, 2024
61af756
Address PR feedback
davidcaseria Nov 1, 2024
d3d01d6
Fixups
davidcaseria Nov 1, 2024
ae8f6d1
Fix error and typo
davidcaseria Nov 7, 2024
abae74b
Clarify keyset ID usage in NUT-13
davidcaseria Nov 19, 2024
4668545
Clarify abbreviated keyset id length
davidcaseria Nov 25, 2024
51473dc
keyset final expiration + commit final expiration
a1denvalu3 Feb 1, 2025
e936d02
remove note about expiry best practice
a1denvalu3 Feb 2, 2025
dbc8042
Merge pull request #1 from lollerfirst/keyset-final-expiry
davidcaseria Feb 2, 2025
9c774ff
Clarify the role of s_id
davidcaseria Apr 9, 2025
07d296f
02-tests
a1denvalu3 Apr 29, 2025
2f19d3b
02-test-vectors
a1denvalu3 Apr 29, 2025
33e2ff0
13-test-vector
a1denvalu3 Apr 29, 2025
06e2522
Merge pull request #3 from lollerfirst/add-v2-test-vectors
davidcaseria Apr 29, 2025
eba95c5
Update 13.md
davidcaseria May 5, 2025
0f3cef6
Apply suggestions from code review
davidcaseria May 5, 2025
cf03fc8
new secret derivation
a1denvalu3 Jul 28, 2025
bf468d6
critical -> caution
a1denvalu3 Jul 28, 2025
336660e
add code examples for new generation
a1denvalu3 Jul 28, 2025
42f5ae2
Merge pull request #4 from lollerfirst/new-secret-derivation
davidcaseria Jul 29, 2025
3478261
Merge remote-tracking branch 'upstream/main' into keyset-id-v2
davidcaseria Jul 29, 2025
12dd688
Merge remote-tracking branch 'origin/keyset-id-v2' into keyset-id-v2
davidcaseria Jul 29, 2025
fb02a9b
Remove utf-8 encoding from KDF
davidcaseria Jul 29, 2025
57b9db8
Remove VSCode settings
davidcaseria Jul 29, 2025
1bd8c32
Fix prettier
davidcaseria Jul 29, 2025
56fb2ae
Require wallets to retain counter when upgrading from legacy derivation
davidcaseria Jul 30, 2025
69d3fd4
Update NUT-13 to version secret derivation by keyset ID
davidcaseria Jul 30, 2025
f05902f
Fix prettier
davidcaseria Jul 30, 2025
419d21d
update secret derivation (again)
a1denvalu3 Aug 6, 2025
6d9b255
Merge pull request #5 from lollerfirst/derivation-type-byte
davidcaseria Aug 6, 2025
9118f4b
update 13 test vectors
a1denvalu3 Aug 18, 2025
e427448
Update 13.md
davidcaseria Aug 18, 2025
7e15c44
Merge pull request #6 from lollerfirst/update-13-test-vectors
davidcaseria Aug 18, 2025
d87f4b2
Fix prettier
davidcaseria Aug 18, 2025
8295a9b
consensus update
a1denvalu3 Aug 28, 2025
b25abaa
Apply suggestions from code review
davidcaseria Aug 28, 2025
b5b33fa
Merge branch 'keyset-id-v2' into consensus-update
davidcaseria Aug 28, 2025
3789b4f
Merge pull request #7 from lollerfirst/consensus-update
davidcaseria Aug 28, 2025
ed78be4
Fix prettier
davidcaseria Aug 28, 2025
e1da7b1
Merge remote-tracking branch 'upstream/main' into keyset-id-v2
davidcaseria Aug 28, 2025
eed0ae2
update test vectors
a1denvalu3 Sep 1, 2025
caca29f
Apply suggestions from code review
davidcaseria Sep 2, 2025
1b6a372
Merge pull request #8 from lollerfirst/update-test-vectors
davidcaseria Sep 8, 2025
6075d35
add final_expiry to GetKeysResponse
d4rp4t Sep 20, 2025
1d1d39f
Merge pull request #9 from d4rp4t/keyset-id-v2
davidcaseria Sep 23, 2025
25e7c35
Update test vectors
d4rp4t Sep 23, 2025
6e785b2
Merge pull request #10 from d4rp4t/keyset-id-v2
davidcaseria Sep 23, 2025
ba1626a
revert separators in ID derivation
a1denvalu3 Sep 24, 2025
1efb5a4
Merge pull request #11 from lollerfirst/revert-separators
davidcaseria Sep 24, 2025
869d76e
Merge remote-tracking branch 'upstream/main' into keyset-id-v2
davidcaseria Dec 18, 2025
9f5a747
Remove redundant comments
davidcaseria Dec 18, 2025
282ea77
edits + modulo for derivation
callebtc Jan 2, 2026
720e5a5
prettier
callebtc Jan 2, 2026
da9fa4d
move short ID to 00.md
callebtc Jan 2, 2026
aca39cd
wording
callebtc Jan 2, 2026
97b9573
restore shorter
callebtc Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions 00.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbey

### V4 tokens

V4 tokens are a space-efficient way of serializing tokens using the CBOR binary format. All field are single characters and hex strings are encoded in binary. V4 tokens can only hold proofs from a single mint.
V4 tokens are a space-efficient way of serializing tokens using the CBOR binary format. All keys are single characters and hex strings are encoded in binary. V4 tokens can only hold proofs from a single mint.

##### Version

Expand All @@ -224,27 +224,27 @@ cashuB[base64_token_cbor]

##### Token format

The deserialized `base64_token_cbor` is a JSON of the same form as a TokenV4 but with shorter keys and data represented as binary data (`bytes`) instead of hex strings (`hex_str`). Note that we have expanded what is called `Proofs` in TokenV3 (called `p` here with TokenV4) showing that its values are also different from the TokenV3 serialization.
For readability, the structure of a TokenV4 object is shown below in its equivalent JSON form.

```json
{
"m": str, // mint URL
"u": str, // unit
"d": str <optional>, // memo
"u": str, // currency unit
"d": str <optional>, // optional memo
"t": [
{
"i": bytes, // keyset ID
"i": bytes, // keyset ID (short or long form)
"p": [ // proofs with this keyset ID
{
"a": int, // amount
"s": str, // secret
"c": bytes, // signature
"d": { <optional> // DLEQ proof
"d": { <optional> // optional DLEQ proof
"e": bytes,
"s": bytes,
"r": bytes
},
"w": str <optional> // witness
"w": str <optional> // optional witness
},
...
]
Expand All @@ -254,11 +254,28 @@ The deserialized `base64_token_cbor` is a JSON of the same form as a TokenV4 but
}
```

`m` is the mint URL. The mint URL must be stripped of any trailing slashes (`/`). `u` is the currency unit of the token keysets (see [Keysets][01] for supported units), and `d` is an optional text memo from the sender.
`m` is the mint URL. The mint URL **MUST** be normalized by stripping any trailing slashes (`/`). `u` is the currency unit of the token keysets. Supported units are defined in [Keysets][01]. `d` is an optional, human-readable memo provided by the sender.

`i` is the keyset ID of the proofs in `p`, which is an array of `Proof` objects without the `id` field. We extracted the keyset ID `id` from each proof and grouped all proofs by their keyset ID `i` one level above (in `p`).
Within the `t` (token) array, `i` denotes the keyset ID associated with the proofs contained in `p` which are grouped by `i`. `p` is an array of `Proof` objects with the original keyset ID field `id` omitted. All proofs in the corresponding `p` array MUST belong to the same keyset ID.

Note that all fields of the `bytes` type encode hex strings in the original representation of `Proof`'s.
Unless otherwise stated, fields of type `bytes` represent byte strings in the CBOR encoding. In the original JSON representation of `Proof` objects, these values are encoded as hexadecimal strings. Implementations MUST convert between hex strings and raw byte arrays when translating between JSON and CBOR representations.

Optional fields MAY be omitted if not present. Receivers MUST ignore unknown fields to preserve forward compatibility.

#### Short keyset ID

To reduce the size of the `i` field and the overall Token encoding, wallets **MAY** use the short keyset ID representation (`s_id`).

The short keyset ID is defined as the first 8 bytes of the full 33-byte keyset ID:

- Byte form: `s_id = id_bytes[:8]`
- Hex form: `s_id = id_hex_str[:16]`

Wallets receiving a Token **MUST** support both short and full keyset ID representations. When a short keyset ID is encountered, the wallet **MUST** resolve it to the corresponding full keyset ID before processing the contained `Proof` objects.

If a short keyset ID resolves to more than one known full keyset ID, the identifier is considered ambiguous. In this case, the wallet **MUST** fail token parsing and return an error.

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

##### Example

Expand Down
3 changes: 3 additions & 0 deletions 01.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Response `GetKeysResponse` of `Bob`:
{
"id": <keyset_id_hex_str>,
"unit": <currency_unit_str>,
"final_expiry": <unix_timestamp_int|null>
"keys": {
<amount_int>: <public_key_str>,
...
Expand All @@ -80,6 +81,7 @@ Response `GetKeysResponse` of `Bob`:
{
"id": "009a1f293253e41e",
"unit": "sat",
"final_expiry": 1896187313,
"keys": {
"1": "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104",
"2": "03b0f36d6d47ce14df8a7be9137712c42bcdd960b19dd02f1d4a9703b1f31d7513",
Expand All @@ -100,6 +102,7 @@ Note that for a keyset in an ISO 4217 currency, the key amounts represent values
{
"id": "00a2f293253e41f9",
"unit": "usd",
"final_expiry": 1896187313,
"keys": {
"1": "0229...e101", // 1 cent (0.01 USD)
"2": "03c0...7512", // 2 cents (0.02 USD)
Expand Down
79 changes: 60 additions & 19 deletions 02.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ A mint can have multiple keysets at the same time. For example, it could have on

### Keyset ID

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 **CAN** compute the keyset `id` for a given keyset by themselves to confirm that the mint is supplying the correct keyset ID (see below).
The keyset `id` is the identifier for a specific keyset and can be derived from the public keys and the metadata of a keyset. Wallets **SHOULD** compute the keyset `id` for a given keyset themselves to verify that the mint is using the correct keyset in its responses.

The keyset `id` is in each `Proof` so it can be used by wallets to identify which mint and keyset it was generated from. The keyset field `id` is also present in the `BlindedMessages` sent to the mint and `BlindSignatures` returned from the mint (see [NUT-00][00]).
The keyset `id` is part of each `Proof` object which allows wallets to identify which mint and keyset it was generated from. The keyset field `id` is also contained in the `BlindedMessages` sent to the mint and in the `BlindSignatures` returned from it. It is also included in a serialized Cashu `Token` (see [NUT-00][00]).

### Active keysets

Expand Down Expand Up @@ -56,11 +56,36 @@ Notice that since transactions can spend inputs from different keysets, the sum

### Deriving the keyset ID

#### Keyset ID version
#### Keyset ID V2

Keyset IDs have a version byte (two hexadecimal characters). The currently used version byte is `00`.
Keyset IDs are 33 byte hex strings with a version byte (two hexadecimal characters). The currently used version byte is `01`.

The mint and the wallets of its users can derive a keyset ID from the keyset of the mint. The keyset ID is a lower-case hex string. To derive the keyset ID of a keyset, execute the following steps:
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 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"
Comment on lines +66 to +71
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?

Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Collaborator

@prusnak prusnak Jan 6, 2026

Choose a reason for hiding this comment

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

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: #182 (comment)

```

An example implementation in Python:

```python
def derive_keyset_id_v2(keys: Dict[int, PublicKey], unit: str, final_expiry: Optional[int]) -> str:
sorted_keys = dict(sorted(keys.items()))
keyset_id_bytes = b"".join([p.serialize() for p in sorted_keys.values()])
keyset_id_bytes += b"unit:"+unit.encode("utf-8")
if final_expiry:
keyset_id_bytes += b"final_expiry:"+str(final_expiry).encode("utf-8")
return "01" + hashlib.sha256(keyset_id_bytes).hexdigest()
```

##### V1 Keysets (deprecated)

V1 keysets are 8 bytes long, including a version byte prefix `00`.

```
1 - sort public keys by their amount in ascending order
Expand All @@ -73,12 +98,24 @@ The mint and the wallets of its users can derive a keyset ID from the keyset of
An example implementation in Python:

```python
def derive_keyset_id(keys: Dict[int, PublicKey]) -> str:
def derive_keyset_id_v1(keys: Dict[int, PublicKey]) -> str:
sorted_keys = dict(sorted(keys.items()))
pubkeys_concat = b"".join([p.serialize() for p in sorted_keys.values()])
return "00" + hashlib.sha256(pubkeys_concat).hexdigest()[:14]
```

> [!CRITICAL]
> Wallet implementations should reject any attempt at importing new keysets which IDs
> collide with any of the previously added keysets.

### Keyset Final Expiry

A unix epoch number for a future point in time that represents the final expiry of the keyset. After the keyset's final expiry, the Mint is no longer obliged to fulfill promises signed with the keys from that keyset.

This effectively implies that the Mint can irrevocably remove all of the nullifiers (`Y` values/spent ecash) associated with the expired keyset.

The final expiry is optional and **MAY** be omitted or `null` if the keyset has no final-expiry.

## Example: Get mint keysets

A wallet can ask the mint for a list of all keysets via the `GET /v1/keysets` endpoint.
Expand All @@ -105,6 +142,7 @@ Response `GetKeysetsResponse` of `Bob`:
"unit": <str>,
"active": <bool>,
"input_fee_ppk": <int|null>,
"final_expiry": <int|null>
},
...
]
Expand All @@ -119,22 +157,25 @@ Here, `id` is the keyset ID, `unit` is the unit string (e.g. "sat") of the keyse
{
"keysets": [
{
"id": "009a1f293253e41e",
"id": "01c9c20fb8b348b389e296227c6cc7a63f77354b7388c720dbba6218f720f9b785",
"unit": "sat",
"active": True,
"input_fee_ppk": 100
"active": true,
"input_fee_ppk": 100,
"final_expiry": 1896187313
},
{
"id": "0042ade98b2a370a",
"id": "0188432103b12cec6361587d92bdfb798079c58b1c828c561b4daec6f4d465a810",
"unit": "sat",
"active": False,
"input_fee_ppk": 100
"active": false,
"input_fee_ppk": 100,
"final_expiry": 1896187313
},
{
"id": "00c074b96c7e2b0e",
"id": "01d0257bde6ff4cd55e49318a824bbe67e2f9faa248ff108203b5fe46581b14ffc",
"unit": "usd",
"active": True,
"input_fee_ppk": 100
"active": true,
"input_fee_ppk": 100,
"final_expiry": 1896187313
}
]
}
Expand All @@ -148,24 +189,24 @@ To receive the public keys of a specific keyset, a wallet can call the `GET /v1/

Request of `Alice`:

We request the keys for the keyset `009a1f293253e41e`.
We request the keys for the keyset `01c9c20fb8b348b389e296227c6cc7a63f77354b7388c720dbba6218f720f9b785`.

```http
GET https://mint.host:3338/v1/keys/009a1f293253e41e
GET https://mint.host:3338/v1/keys/01c9c20fb8b348b389e296227c6cc7a63f77354b7388c720dbba6218f720f9b785
```

With curl:

```bash
curl -X GET https://mint.host:3338/v1/keys/009a1f293253e41e
curl -X GET https://mint.host:3338/v1/keys/01c9c20fb8b348b389e296227c6cc7a63f77354b7388c720dbba6218f720f9b785
```

Response of `Bob` (same as [NUT-01][01]):

```json
{
"keysets": [{
"id": "009a1f293253e41e",
"id": "01c9c20fb8b348b389e296227c6cc7a63f77354b7388c720dbba6218f720f9b785",
"unit": "sat",
"keys": {
"1": "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104",
Expand Down
Loading