Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyset ID V2 #182

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 37 additions & 13 deletions 02.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ 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).
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 by themselves to confirm that the mint is supplying the correct keyset ID (see below).

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

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`). The length of the `s_id` **MUST** be eight bytes (i.e., the version byte and the first seven bytes of the hash: `id_bytes[:8]` or `id_hex[:16]`). A wallet **MUST** resolve the abbreviated keyset `id` to the full `id`. 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.

A Wallet **SHOULD** save the full-length keyset `id` with proofs in its database.

### Active keysets

Mints can have multiple keysets at the same time but **MUST** have at least one `active` keyset (see [NUT-01][01]). The `active` property determines whether the mint allows generating new ecash from this keyset. `Proofs` from inactive keysets with `active=false` are still accepted as inputs but new outputs (`BlindedMessages` and `BlindSignatures`) **MUST** be from `active` keysets only.
Expand Down Expand Up @@ -56,12 +60,32 @@ Notice that since transactions can spend inputs from different keysets, the sum

### Deriving the keyset ID

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

Keyset IDs have a version byte (two hexadecimal characters). The currently used version byte is `00`.
Keyset IDs have 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:

```
1 - sort public keys by their amount in ascending order
2 - concatenate all public keys to one byte array
3 - add the unit string to the byte array (e.g. "unit:sat")
4 - HASH_SHA256 the concatenated byte array
5 - prefix it with a keyset ID version byte
```

An example implementation in Python:

```python
def derive_keyset_id(keys: Dict[int, PublicKey]) -> 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:sat"
Comment on lines +82 to +83
Copy link
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Author

Choose a reason for hiding this comment

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

What would the structure of the CBOR be?

Copy link
Contributor

@ok300 ok300 Nov 5, 2024

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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

return "01" + hashlib.sha256(keyset_id_bytes).hexdigest()
```

##### Version 0

```
1 - sort public keys by their amount in ascending order
2 - concatenate all public keys to one byte array
Expand Down Expand Up @@ -119,21 +143,21 @@ 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,
"active": true,
"input_fee_ppk": 100
},
{
"id": "0042ade98b2a370a",
"id": "0188432103b12cec6361587d92bdfb798079c58b1c828c561b4daec6f4d465a810",
"unit": "sat",
"active": False,
"active": false,
"input_fee_ppk": 100
},
{
"id": "00c074b96c7e2b0e",
"id": "01d0257bde6ff4cd55e49318a824bbe67e2f9faa248ff108203b5fe46581b14ffc",
"unit": "usd",
"active": True,
"active": true,
"input_fee_ppk": 100
}
]
Expand All @@ -148,24 +172,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`.
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Author

Choose a reason for hiding this comment

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

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?


```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
2 changes: 1 addition & 1 deletion 13.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The wallet starts with `counter_k := 0` upon encountering a new keyset and incre

#### Keyset ID

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`.
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` should be shortened to the first 8 bytes before conversion.

Example in Python:

Expand Down