{"id":"39eeb0f8ee6f54eb36ba34b5066c90e04a746ff39f49c89c6670f40bf6112e1b","pubkey":"d1678e7ef965374bbea308a1215609a78376dc158277a7d657680f9d5efd5c38","created_at":1768769200,"kind":30817,"tags":[["d","nut-02"],["title","NUT-02: Keysets and fees"],["summary","Keysets: how an id is derived from one, how a mint rotates them, and the input fee a wallet pays when spending ecash from one."],["s","mandatory"],["t","cashu"],["t","ecash"],["t","nut"],["alt","A specification: NUT-02: Keysets and fees"],["client","openspecs-import"],["published_at","1674849737"],["proxy","https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/02.md","web"],["x","be2077ef0f7b40e97311d33b0db9caf4d4a03abf7f4042e3440dcaa171348ac7"]],"content":"# NUT-02: Keysets and fees\n\n`mandatory`\n\n---\n\nA keyset is a set of public keys that the mint `Bob` generates and shares with its users. It refers to the set of public keys that each correspond to the amount values that the mint supports (e.g. `1, 2, 4, 8, ...`) respectively.\n\nEach keyset indicates its keyset `id`, the currency `unit`, whether the keyset is `active`, and an `input_fee_ppk` that determines the fees for spending ecash from this keyset.\n\nA mint can have multiple keysets at the same time. For example, it could have one keyset for each currency `unit` that it supports. Wallets should support multiple keysets. They must respect the `active` and the `input_fee_ppk` properties of the keysets they use.\n\n## Keyset properties\n\n### Keyset ID\n\nThe 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.\n\nThe 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]).\n\n### Active keysets\n\nMints 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.\n\nTo rotate keysets, a mint can generate a new active keyset and inactivate an old one. If the `active` flag of an old keyset is set to `false`, no new ecash from this keyset can be generated and the outstanding ecash supply of that keyset can be taken out of circulation as wallets rotate their ecash to active keysets.\n\nWallets **SHOULD** prioritize swaps with `Proofs` from inactive keysets (see [NUT-03][03]) so they can quickly get rid of them. Wallets **CAN** swap their entire balance from an inactive keyset to an active one as soon as they detect that the keyset was inactivated. When constructing outputs for a transaction, wallets **MUST** choose only `active` keysets (see [NUT-00][00]).\n\n### Fees\n\nKeysets indicate the fee `input_fee_ppk` that is charged when a `Proof` of that keyset is spent as an input to a transaction. The fee is given in parts per thousand (ppk) per input measured in the `unit` of the keyset. The total fee for a transaction is the sum of all fees per input rounded up to the next larger integer (that that can be represented with the keyset).\n\nAs an example, we construct a transaction spending 3 inputs (`Proofs`) from a keyset with unit `sat` and `input_fee_ppk` of `100`. A fee of `100 ppk` means `0.1 sat` per input. The sum of the individual fees are 300 ppk for this transaction. Rounded up to the next smallest denomination, the mint charges `1 sat` in total fees, i.e. `fees = ceil(0.3) == 1`. In this case, the fees for spending 1-10 inputs is 1 sat, 11-20 inputs is 2 sat and so on.\n\n#### Wallet transaction construction\n\nWhen constructing a transaction with ecash `inputs` (example: `/v1/swap` or `/v1/melt`), wallets **MUST** add fees to the inputs or, vice versa, subtract from the outputs. The mint checks the following equation:\n\n```python\nsum(inputs) - fees == sum(outputs)\n```\n\nHere, `sum(inputs)` and `sum(outputs)` mean the sum of the amounts of the inputs and outputs respectively. `fees` is calculated from the sum of each input's fee and rounded up to the next larger integer:\n\n```python\ndef fees(inputs: List[Proof]) -> int:\n  sum_fees = 0\n  for proof in inputs:\n    sum_fees += keysets[proof.id].input_fee_ppk\n  return (sum_fees + 999) // 1000\n```\n\nHere, the `//` operator in `(sum_fees + 999) // 1000` denotes an integer division operator (aka floor division operator) that rounds down `sum_fees + 999` to the next lower integer. Alternatively, we could round up the sum using a floating point division with `ceil(sum_fees / 1000)` although it is not recommended to do so due to the non-deterministic behavior of floating point division.\n\nNotice that since transactions can spend inputs from different keysets, the sum considers the fee for each `Proof` indexed by the keyset ID individually.\n\n### Deriving the keyset ID\n\n#### Keyset ID V2\n\nKeyset IDs are 33 byte hex strings with a version byte (two hexadecimal characters). The currently used version byte is `01`.\n\nKeyset IDs are derived from public data. To derive the keyset ID of a keyset, execute the following steps:\n\n```\n1 - sort public keys by their amount in ascending numerical order\n2 - 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 (\",\")\n3 - add the lowercase UTF8-encoded unit string prefixed with \"|unit:\" to the byte array (e.g. \"|unit:sat\")\n4 - If input_fee_ppk is specified and non-zero, add the UTF8-encoded string prefixed with \"|input_fee_ppk:\" (e.g. \"|input_fee_ppk:100\"). If input_fee_ppk is omitted, null, or 0, it MUST be omitted from the preimage.\n5 - If a final expiration is specified, add the UTF8-encoded string prefixed with \"|final_expiry:\" (e.g. \"|final_expiry:1896187313\")\n6 - HASH_SHA256 the concatenated byte array\n7 - prefix it with a keyset ID version byte \"01\"\n```\n\nAn example implementation in Python:\n\n```python\ndef derive_keyset_id_v2(keys: Dict[int, PublicKey], unit: str, input_fee_ppk: Optional[int], final_expiry: Optional[int]) -> str:\n    sorted_keys = dict(sorted(keys.items()))\n    keyset_id_bytes = b\",\".join(\n        [f\"{k}:{v.serialize()}\".encode(\"utf-8\") for k, v in sorted_keys.items()]\n    )\n    keyset_id_bytes += f\"|unit:{unit}\".encode(\"utf-8\")\n    if input_fee_ppk is not None and input_fee_ppk != 0:\n        keyset_id_bytes += f\"|input_fee_ppk:{input_fee_ppk}\".encode(\"utf-8\")\n    if final_expiry is not None and final_expiry != 0:\n        keyset_id_bytes += f\"|final_expiry:{final_expiry}\".encode(\"utf-8\")\n    return \"01\" + hashlib.sha256(keyset_id_bytes).hexdigest()\n```\n\n##### V1 Keysets (deprecated)\n\nV1 keysets are 8 bytes long, including a version byte prefix `00`.\n\n```\n1 - sort public keys by their amount in ascending order\n2 - concatenate all public keys to one byte array\n3 - HASH_SHA256 the concatenated public keys\n4 - take the first 14 characters of the hex-encoded hash\n5 - prefix it with a keyset ID version byte\n```\n\nAn example implementation in Python:\n\n```python\ndef derive_keyset_id_v1(keys: Dict[int, PublicKey]) -> str:\n    sorted_keys = dict(sorted(keys.items()))\n    pubkeys_concat = b\"\".join([p.serialize() for p in sorted_keys.values()])\n    return \"00\" + hashlib.sha256(pubkeys_concat).hexdigest()[:14]\n```\n\n> [!CRITICAL]\n> Wallet implementations should reject any attempt at importing new keysets which IDs\n> collide with any of the previously added keysets.\n\n### Keyset Final Expiry\n\nA 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.\n\nThis effectively implies that the Mint can irrevocably remove all of the nullifiers (`Y` values/spent ecash) associated with the expired keyset.\n\nThe final expiry is optional and **MAY** be omitted or `null` if the keyset has no final-expiry.\n\n## Example: Get mint keysets\n\nA wallet can ask the mint for a list of all keysets via the `GET /v1/keysets` endpoint.\n\nRequest of `Alice`:\n\n```http\nGET https://mint.host:3338/v1/keysets\n```\n\nWith curl:\n\n```bash\ncurl -X GET https://mint.host:3338/v1/keysets\n```\n\nResponse `GetKeysetsResponse` of `Bob`:\n\n```json\n{\n  \"keysets\": [\n    {\n      \"id\": <hex_str>,\n      \"unit\": <str>,\n      \"active\": <bool>,\n      \"input_fee_ppk\": <int|null>,\n      \"final_expiry\": <int|null>\n    },\n    ...\n  ]\n}\n```\n\nHere, `id` is the keyset ID, `unit` is the unit string (e.g. \"sat\") of the keyset, `active` indicates whether new ecash can be minted with this keyset, and `input_fee_ppk` is the fee (per thousand units) to spend one input spent from this keyset. If `input_fee_ppk` is not given, we assume it to be `0`.\n\n### Example response\n\n```json\n{\n  \"keysets\": [\n    {\n      \"id\": \"015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a\",\n      \"unit\": \"sat\",\n      \"active\": true,\n      \"input_fee_ppk\": 100,\n      \"final_expiry\": 2059210353\n    },\n    {\n      \"id\": \"012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8\",\n      \"unit\": \"sat\",\n      \"active\": false,\n      \"input_fee_ppk\": 0,\n      \"final_expiry\": null\n    }\n  ]\n}\n```\n\n## Requesting public keys for a specific keyset\n\nTo receive the public keys of a specific keyset, a wallet can call the `GET /v1/keys/{keyset_id}` endpoint where `keyset_id` is the keyset ID.\n\n### Example\n\nRequest of `Alice`:\n\nWe request the keys for the keyset `015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a`.\n\n```http\nGET https://mint.host:3338/v1/keys/015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a\n```\n\nWith curl:\n\n```bash\ncurl -X GET https://mint.host:3338/v1/keys/015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a\n```\n\nResponse of `Bob` (same as [NUT-01][01]):\n\n```json\n{\n  \"keysets\": [{\n    \"id\": \"009a1f293253e41e\",\n    \"unit\": \"sat\",\n    \"active\": true,\n    \"input_fee_ppk\": 100,\n    \"keys\": {\n        \"1\": \"02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104\",\n        \"2\": \"03b0f36d6d47ce14df8a7be9137712c42bcdd960b19dd02f1d4a9703b1f31d7513\",\n        \"4\": \"0366be6e026e42852498efb82014ca91e89da2e7a5bd3761bdad699fa2aec9fe09\",\n        \"8\": \"0253de5237f189606f29d8a690ea719f74d65f617bb1cb6fbea34f2bc4f930016d\",\n        ...\n    },\n  }, ...\n  ]\n}\n```\n\n## Wallet implementation notes\n\nWallets can request the list of keyset IDs from the mint upon startup and load only tokens from its database that have a keyset ID supported by the mint it interacts with. This also helps wallets to determine whether the mint has added a new current keyset or whether it has changed the `active` flag of an existing one.\n\nA useful flow is:\n\n- If we don't have any keys from this mint yet, get all keys: `GET /v1/keys` and store them\n- Get all keysets with `GET /v1/keysets`\n- For all new keyset returned here which we don't have yet, get it using `GET /v1/keys/{keyset_id}` and store it\n- If any of the keysets has changed its `active` flag, update it in the db and use the keyset accordingly\n\n[00]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crqqpd6d9\n[01]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crznyve7g\n[02]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cry0t0uzl\n[03]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crxuwwl3j\n[04]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crg74fknc\n[05]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cr2dsg4q4\n[06]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crv3ltsuz\n[07]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crwz62n00\n[08]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crs4q9zck\n[09]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crjx9yptm\n[10]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cnqx5tye7\n[11]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cnz43282n\n[12]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cnyf7fzky\n[TokenV3]: https://github.com/cashuBTC/nuts/blob/main/00.md#023---v3-tokens\n","sig":"ec392accca59dfd1783789eb24d982822f61889994fc1b86e8bea3b33bdb19c94607d90691b54ab11ebae00c58a91d9ea7047ecce53afd2669157d62b0481cee"}