Skip to content

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