Skip to content

Commit

Permalink
Add fees to NUT-02 (#126)
Browse files Browse the repository at this point in the history
* add fees to NUT-02

* edit

* adjust title

* change title in README

* clarify fee can be null

* edit language

* address conduitions comment on floating point division

* Update 02.md

Co-authored-by: gandlafbtc <[email protected]>

---------

Co-authored-by: gandlafbtc <[email protected]>
  • Loading branch information
callebtc and gandlafbtc authored Jun 27, 2024
1 parent b661a4a commit 40771dc
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 58 deletions.
157 changes: 100 additions & 57 deletions 02.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,87 @@
NUT-02: Keysets and keyset ID
==========================
# 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 additionally indicates its keyset `id`, the currency `unit`, and whether the keyset is `active` or not.
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.

## Multiple keysets
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.

#### Active keysets
Mints can have multiple keysets at the same time but **MUST** have at least one `active` keyset (see [NUT-01][01]). `Proofs` from inactive keysets are still accepted but new outputs (`BlindedMessages` and `BlindSignatures`) **MUST** be redeemed from `active` keysets only.
## Keyset properties

#### Currency unit
Mints **MUST** generate a keysets for each `unit` they support.
### Keyset ID

#### Wallet input and output construction
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).

Wallets **SHOULD** store keysets the first time they encounter them along with the URL of the mint they are from. That way, wallets can choose `Proofs` of all keysets of a mint from their database when selecting inputs for an operation. Wallets **SHOULD** spend `Proofs` of inactive keysets first. When constructing outputs for an operation, wallets **MUST** choose only `active` keysets (see [NUT-00][00]).
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]).

## Example
### Active keysets

A wallet can ask the mint for a list of all keyset IDs via the `GET /v1/keysets` endpoint.
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.

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

### 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:

```python
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:

```python
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:

```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`:

Expand All @@ -43,77 +101,51 @@ Response `GetKeysetsResponse` of `Bob`:
{
"keysets": [
{
"id": <keyset_id_hex_str>,
"unit": <currency_unit_str>,
"active": <bool>
"id": <hex_str>,
"unit": <str>,
"active": <bool>,
"input_fee_ppk": <int|null>,
},
...
]
}
```
## Example response

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

```json
{
"keysets": [
{
"id": "009a1f293253e41e",
"unit": "sat",
"active": True
"active": True,
"input_fee_ppk": 100
},
{
"id": "0042ade98b2a370a",
"unit": "sat",
"active": False
"active": False,
"input_fee_ppk": 100
},
{
"id": "00c074b96c7e2b0e",
"unit": "usd",
"active": True
}
"active": True,
"input_fee_ppk": 100
}
]
}
```

#### 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 rotated to a new current keyset (i.e. added new active keysets and inactivated old ones) and whether the wallet should recycle all tokens from inactive keysets to currently `active` ones.


## 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.

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 `id` is also stored in `BlindedMessages` sent to the mint and `BlindSignatures` returned from the mint (see [NUT-00][00]).

### Keyset ID version

Keyset IDs have a version byte (two hexadecimal characters). The currently used version byte is `00`.

### Deriving the keyset ID

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:

```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]
```

## 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
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`:

Expand Down Expand Up @@ -142,12 +174,23 @@ Response of `Bob` (same as [NUT-01][01]):
"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

[00]: 00.md
[01]: 01.md
[02]: 02.md
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
|----------|-----------------------------------|
| [00][00] | Cryptography and Models |
| [01][01] | Mint public keys |
| [02][02] | Keysets and keyset IDs |
| [02][02] | Keysets and fees |
| [03][03] | Swapping tokens |
| [04][04] | Minting tokens |
| [05][05] | Melting tokens |
Expand Down

0 comments on commit 40771dc

Please sign in to comment.