NUT-02: Keysets and fees¶
mandatory
A 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.
Each 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.
A 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.
Keyset properties¶
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 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).
Active keysets¶
Mints can have multiple keysets at the same time but MUST have at least one active
keyset (see NUT-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.
To rotate keysets, a mint can generate a new active keyset and inactive 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.
Wallets SHOULD prioritize swaps with Proofs
from inactive keysets (see NUT-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).
Fees¶
Keysets 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 keyest).
As 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.
Wallet transaction construction¶
When 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:
sum(inputs) - fees == sum(outputs)
Here, 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:
def fees(inputs: List[Proof]) -> int:
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
Here, 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.
Notice that since transactions can spend inputs from different keysets, the sum considers the fee for each Proof
indexed by the keyset ID individually.
Deriving the keyset ID¶
Keyset ID version¶
Keyset IDs have a version byte (two hexadecimal characters). The currently used version byte is 00
.
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 - HASH_SHA256 the concatenated public keys
4 - take the first 14 characters of the hex-encoded hash
5 - prefix it with a keyset ID version byte
An example implementation in Python:
def derive_keyset_id(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]
Example: Get mint keysets¶
A wallet can ask the mint for a list of all keysets via the GET /v1/keysets
endpoint.
Request of Alice
:
GET https://mint.host:3338/v1/keysets
With curl:
curl -X GET https://mint.host:3338/v1/keysets
Response GetKeysetsResponse
of Bob
:
{
"keysets": [
{
"id": <hex_str>,
"unit": <str>,
"active": <bool>,
"input_fee_ppk": <int|null>,
},
...
]
}
Here, 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
.
Example response¶
{
"keysets": [
{
"id": "009a1f293253e41e",
"unit": "sat",
"active": True,
"input_fee_ppk": 100
},
{
"id": "0042ade98b2a370a",
"unit": "sat",
"active": False,
"input_fee_ppk": 100
},
{
"id": "00c074b96c7e2b0e",
"unit": "usd",
"active": True,
"input_fee_ppk": 100
}
]
}
Requesting public keys for a specific keyset¶
To 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.
Example¶
Request of Alice
:
We request the keys for the keyset 009a1f293253e41e
.
GET https://mint.host:3338/v1/keys/009a1f293253e41e
With curl:
curl -X GET https://mint.host:3338/v1/keys/009a1f293253e41e
Response of Bob
(same as NUT-01):
{
"keysets": [{
"id": "009a1f293253e41e",
"unit": "sat",
"keys": {
"1": "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104",
"2": "03b0f36d6d47ce14df8a7be9137712c42bcdd960b19dd02f1d4a9703b1f31d7513",
"4": "0366be6e026e42852498efb82014ca91e89da2e7a5bd3761bdad699fa2aec9fe09",
"8": "0253de5237f189606f29d8a690ea719f74d65f617bb1cb6fbea34f2bc4f930016d",
...
},
}, ...
]
}
Wallet implementation notes¶
Wallets 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.
A useful flow is:
- If we don't have any keys from this mint yet, get all keys:
GET /v1/keys
and store them - Get all keysets with
GET /v1/keysets
- For all new keyset returned here which we don't have yet, get it using
GET /v1/keys/{keyset_id}
and store it - If any of the keysets has changed its
active
flag, update it in the db and use the keyset accordingly