Add fees to NUT-02 #126

Merged
callebtc merged 8 commits from input-fees into main 2024-06-27 17:30:32 +00:00
callebtc commented 2024-05-27 07:44:22 +00:00 (Migrated from github.com)

Proposing to add fees to the mandatory part of the spec as I expect most public mints will require fees at some point. If the fees are not set, we should assume it as being 0.

Moved around some existing text. The new part is:

Fees (parts per thousand)

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 and the sum is rounded up to the next larger integer.

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 fees would be 300 ppk for this transaction and the mint would charge 1 sat in fees (ceil(0.3) == 1). The fees for spending 1-10 inputs is 1 sat, 11-20 inputs is 2 sat and so on.

...

Wallet input and output construction

When constructing a transaction with ecash inputs (example: /v1/swap or /v1/melt), wallets MUST add fees to the inputs (or subtract from the outputs) if they spent ecash from a keyset with fees. The mint checks the following equation:

sum(inputs) - sum(fees) == sum(outputs)

The fees are calculated for each input individually (by summing the fee from the keyset they are from) and then rounded up to the next integer.

Tracking progress

Mints:

  • nutshell PR
  • CDK PR
  • gonuts PR
  • nutmix PR
  • chamberlain

Wallets

  • nutshell PR
  • cashu-ts
  • Minibits
  • eNuts
  • Cashu.me
  • ...
Proposing to add fees to the mandatory part of the spec as I expect most public mints will require fees at some point. If the fees are not set, we should assume it as being 0. Moved around some existing text. The new part is: ### Fees (parts per thousand) 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 and the sum is rounded up to the next larger integer. 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 fees would be 300 ppk for this transaction and the mint would charge `1 sat` in fees (`ceil(0.3) == 1`). The fees for spending 1-10 inputs is 1 sat, 11-20 inputs is 2 sat and so on. ... #### Wallet input and output construction When constructing a transaction with ecash inputs (example: `/v1/swap` or `/v1/melt`), wallets **MUST** add fees to the inputs (or subtract from the outputs) if they spent ecash from a keyset with fees. The mint checks the following equation: ```python sum(inputs) - sum(fees) == sum(outputs) ``` The `fees` are calculated for each input individually (by summing the fee from the keyset they are from) and then rounded up to the next integer. ## Tracking progress Mints: - [x] nutshell [PR](https://github.com/cashubtc/nutshell/pull/503) - [x] CDK [PR](https://github.com/cashubtc/cdk/pull/227) - [x] gonuts [PR](https://github.com/elnosh/gonuts/pull/36) - [x] nutmix [PR](https://github.com/lescuer97/nutmix/pull/74) - [ ] chamberlain Wallets - [x] nutshell [PR](https://github.com/cashubtc/nutshell/pull/503) - [ ] cashu-ts - [x] Minibits - [ ] eNuts - [ ] Cashu.me - [ ] ...
AngusP (Migrated from github.com) reviewed 2024-05-27 07:44:22 +00:00
ebrakke (Migrated from github.com) reviewed 2024-05-27 07:44:22 +00:00
thunderbiscuit (Migrated from github.com) reviewed 2024-05-27 07:44:22 +00:00
BilligsterUser (Migrated from github.com) reviewed 2024-05-27 07:44:22 +00:00
elnosh (Migrated from github.com) reviewed 2024-05-29 15:44:12 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
elnosh (Migrated from github.com) commented 2024-05-29 15:44:12 +00:00

any reason for including melts? I thought mints could use the fee_reserve field in melts to charge fees

any reason for including melts? I thought mints could use the `fee_reserve` field in melts to charge fees
thesimplekid (Migrated from github.com) reviewed 2024-05-29 21:42:53 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
thesimplekid (Migrated from github.com) commented 2024-05-29 21:42:53 +00:00

The fee_reserve is the estimation of the routing fees in the case of Bolt11, this is calculated at the time of the quote when the mint does not know the number of inputs nor the keysets of the Proofs the wallet will send when it actually goes to melt thus it does not know what the fee will be. So the wallet must calculate the fee to the mint when selecting proofs for the melt. And send enough Proofs to pay the amount + fee_reserve + mint fee.

The `fee_reserve` is the estimation of the routing fees in the case of Bolt11, this is calculated at the time of the quote when the mint does not know the number of inputs nor the `keysets` of the `Proofs` the wallet will send when it actually goes to `melt` thus it does not know what the fee will be. So the wallet must calculate the fee to the mint when selecting proofs for the melt. And send enough `Proofs` to pay the `amount` + `fee_reserve` + `mint fee`.
callebtc (Migrated from github.com) reviewed 2024-05-30 07:04:14 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
callebtc (Migrated from github.com) commented 2024-05-30 07:04:14 +00:00

any reason for including melts?

the mint does not know the number of inputs

You just explained it :) I thought about this a lot but I think fee_reserve does not fulfill the requirements here. The mint does not know how many inputs will be spent for a melt, so it can't use that as an estimate for the fee_reserve. In the worst case, someone could pay a 1000 sat invoice with 1000x1sat tokens. With that, the whole fee system could be circumvented by users simply using internal invoices instead of sending around ecash (which would cost fees).

> any reason for including melts? > the mint does not know the number of inputs You just explained it :) I thought about this a lot but I think `fee_reserve` does not fulfill the requirements here. The mint does not know how many inputs will be spent for a melt, so it can't use that as an estimate for the fee_reserve. In the worst case, someone could pay a 1000 sat invoice with 1000x1sat tokens. With that, the whole fee system could be circumvented by users simply using internal invoices instead of sending around ecash (which would cost fees).
Semisol commented 2024-05-30 07:48:19 +00:00 (Migrated from github.com)

unintended side effect: this has a negative effect on privacy measures and offline spending

unintended side effect: this has a negative effect on privacy measures and offline spending
elnosh (Migrated from github.com) reviewed 2024-05-30 14:16:11 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
elnosh (Migrated from github.com) commented 2024-05-30 14:16:11 +00:00

Ok, I think I see part of it. The mint does not know the number of inputs so it can't calculate the fee per input. But it seems to me that the mint can still charge fees in melts although not in the same way as with input_fee_ppk. I see most mint implementations have some setting like LIGHTNING_FEE_PERCENT which is the percent they are currently using for the fee_reserve (or if not, let me know), so mints could add to that their desired fees for the melt. So that field will be LIGHTNING_FEE_PERCENT + MELT_FEE_PERCENT. It's by percent and not by input but still a way to charge fee.

the whole fee system could be circumvented by users simply using internal invoices instead

Maybe I'm missing something but I think mints today have ways to charge fees in both mints and melts requests to avoid this. If a mint receives a request for a mint quote for 1000 sats and it wants to charge a fee, it can return an invoice for 1002 sats (2 sats in fees). Similar in melt, if a wallet user wants a 1000 sat invoice paid then the mint can ask 1002 sat worth of proofs (a fee accounting for both lightning and melt).

Ok, I think I see part of it. The mint does not know the number of inputs so it can't calculate the fee per input. But it seems to me that the mint can still charge fees in melts although not in the same way as with `input_fee_ppk`. I see most mint implementations have some setting like `LIGHTNING_FEE_PERCENT` which is the percent they are currently using for the `fee_reserve` (or if not, let me know), so mints could add to that their desired fees for the melt. So that field will be `LIGHTNING_FEE_PERCENT` + `MELT_FEE_PERCENT`. It's by percent and not by input but still a way to charge fee. > the whole fee system could be circumvented by users simply using internal invoices instead Maybe I'm missing something but I think mints today have ways to charge fees in both mints and melts requests to avoid this. If a mint receives a request for a mint quote for 1000 sats and it wants to charge a fee, it can return an invoice for 1002 sats (2 sats in fees). Similar in melt, if a wallet user wants a 1000 sat invoice paid then the mint can ask 1002 sat worth of proofs (a fee accounting for both lightning and melt).
callebtc (Migrated from github.com) reviewed 2024-05-30 14:21:24 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
callebtc (Migrated from github.com) commented 2024-05-30 14:21:24 +00:00

Maybe I'm missing something but I think mints today have ways to charge fees in both mints and melts requests to avoid this. If a mint receives a request for a mint quote for 1000 sats and it wants to charge a fee, it can return an invoice for 1002 sats (2 sats in fees). Similar in melt, if a wallet user wants a 1000 sat invoice paid then the mint can ask 1002 sat worth of proofs (a fee accounting for both lightning and melt).

That's true but consider the example above with someone paying a 1000 sat invoice with 1000 1 sat ecash tokens. The mint would have to add the expected maximum input fee possible (might be on the order of the lightning fee reserve or even higher) to the fee_reserve and deduct it later depending on how many inputs were spent.

This way it seems cleaner: the mint checks is the provided inputs are amount + fee + fee_reserve and can deduct the overpaid fee_reserve like they do now

> Maybe I'm missing something but I think mints today have ways to charge fees in both mints and melts requests to avoid this. If a mint receives a request for a mint quote for 1000 sats and it wants to charge a fee, it can return an invoice for 1002 sats (2 sats in fees). Similar in melt, if a wallet user wants a 1000 sat invoice paid then the mint can ask 1002 sat worth of proofs (a fee accounting for both lightning and melt). That's true but consider the example above with someone paying a 1000 sat invoice with 1000 1 sat ecash tokens. The mint would have to add the expected maximum input fee possible (might be on the order of the lightning fee reserve or even higher) to the fee_reserve and deduct it later depending on how many inputs were spent. This way it seems cleaner: the mint checks is the provided inputs are amount + fee + fee_reserve and can deduct the overpaid fee_reserve like they do now
callebtc (Migrated from github.com) reviewed 2024-05-30 14:34:58 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
callebtc (Migrated from github.com) commented 2024-05-30 14:34:58 +00:00

Using only the fee_reserve would also mean that everyone has to reserve the maximum possible fee, so most users who won't use 1000x1 sat inputs would have to reserve as much as those who do.

Regarding the check in the mint, what I do in nutshell is for a /mint is:

# verify that the amount of the input proofs is equal to the amount of the quote
total_provided = sum_proofs(proofs)
total_needed = (
    melt_quote.amount
    + melt_quote.fee_reserve
    + self.get_fees_for_proofs(proofs)
)
if total_provided < total_needed:
    raise TransactionError(
        f"not enough inputs provided for melt. Provided: {total_provided}, needed: {total_needed}"
    )
Using only the `fee_reserve` would also mean that everyone has to reserve the maximum possible fee, so most users who won't use 1000x1 sat inputs would have to reserve as much as those who do. Regarding the check in the mint, what I do in nutshell is for a `/mint` is: ```python # verify that the amount of the input proofs is equal to the amount of the quote total_provided = sum_proofs(proofs) total_needed = ( melt_quote.amount + melt_quote.fee_reserve + self.get_fees_for_proofs(proofs) ) if total_provided < total_needed: raise TransactionError( f"not enough inputs provided for melt. Provided: {total_provided}, needed: {total_needed}" ) ```
elnosh (Migrated from github.com) reviewed 2024-06-03 14:37:04 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
elnosh (Migrated from github.com) commented 2024-06-03 14:37:03 +00:00

ok, prob a misunderstanding as I was proposing that this change does not affect melt operations and only do the input fee calculation for swaps.

This way it seems cleaner: the mint checks is the provided inputs are amount + fee + fee_reserve and can deduct the overpaid fee_reserve like they do now

I was thinking about this and agree that this could be cleaner to know the type of fee. But you could also end up with weird flows like paying fees in one way and the possibility of asking for back fees in another way

ok, prob a misunderstanding as I was proposing that this change does not affect melt operations and only do the input fee calculation for swaps. > This way it seems cleaner: the mint checks is the provided inputs are amount + fee + fee_reserve and can deduct the overpaid fee_reserve like they do now I was thinking about this and agree that this could be cleaner to know the type of fee. But you could also end up with weird flows like paying fees in one way and the possibility of asking for back fees in another way
conduition (Migrated from github.com) reviewed 2024-06-04 02:15:31 +00:00
@ -1,5 +1,4 @@
NUT-02: Keysets and keyset ID
==========================
# NUT-02: Keysets and fees
conduition (Migrated from github.com) commented 2024-06-04 02:15:30 +00:00

This implies wallets must use floating point operations to compute fees, which is a non-deterministic procedure. Wallets would need to be robust against off-by-one errors. I ran into this problem while working on fees for #128.

If it were me, i'd define the fees with integer arithmetic only so that every implementation can agree.

def fees(inputs: List[Proof]) -> int:
  sum_fees = 0
  for proof in inputs:
    sum_fees += proof.amount * keysets[proof.id].input_fee_ppk
  return sum_fees // 1000
This implies wallets must use floating point operations to compute fees, which is a non-deterministic procedure. Wallets would need to be robust against off-by-one errors. I ran into this problem while working on fees for #128. If it were me, i'd define the fees with integer arithmetic only so that every implementation can agree. ```suggestion def fees(inputs: List[Proof]) -> int: sum_fees = 0 for proof in inputs: sum_fees += proof.amount * keysets[proof.id].input_fee_ppk return sum_fees // 1000 ```
callebtc (Migrated from github.com) reviewed 2024-06-04 10:21:47 +00:00
@ -1,5 +1,4 @@
NUT-02: Keysets and keyset ID
==========================
# NUT-02: Keysets and fees
callebtc (Migrated from github.com) commented 2024-06-04 10:21:47 +00:00

I had this concern too and thought that using an intermediary float for the last step would not include any ambiguity but thinking about it now, it might.

Using integer division // however will round down and not up, so I'm not sure whether your suggestion would produce the same result as using ceil(). Do you agree?

I had this concern too and thought that using an intermediary float for the last step would not include any ambiguity but thinking about it now, it might. Using integer division `//` however will round down and not up, so I'm not sure whether your suggestion would produce the same result as using `ceil()`. Do you agree?
conduition (Migrated from github.com) reviewed 2024-06-04 14:46:27 +00:00
@ -1,5 +1,4 @@
NUT-02: Keysets and keyset ID
==========================
# NUT-02: Keysets and fees
conduition (Migrated from github.com) commented 2024-06-04 14:46:27 +00:00

Oh sorry, i missed that. if you want to round up with integer division, try this:

def fees(inputs: List[Proof]) -> int:
  sum_fees = 0
  for proof in inputs:
    sum_fees += proof.amount * keysets[proof.id].input_fee_ppk
  return (sum_fees + 999) // 1000
Oh sorry, i missed that. if you want to round up with integer division, try this: ```suggestion def fees(inputs: List[Proof]) -> int: sum_fees = 0 for proof in inputs: sum_fees += proof.amount * keysets[proof.id].input_fee_ppk return (sum_fees + 999) // 1000 ```
gandlafbtc (Migrated from github.com) reviewed 2024-06-06 00:09:57 +00:00
gandlafbtc (Migrated from github.com) commented 2024-06-06 00:09:57 +00:00
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.
```suggestion 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. ```
callebtc (Migrated from github.com) reviewed 2024-06-16 11:24:55 +00:00
@ -1,5 +1,4 @@
NUT-02: Keysets and keyset ID
==========================
# NUT-02: Keysets and fees
callebtc (Migrated from github.com) commented 2024-06-16 11:24:55 +00:00

Noticed another issue here: the fees do not depend on proof.amount but only on the number of input proofs.

Noticed another issue here: the fees do not depend on `proof.amount` but only on the number of input proofs.
callebtc (Migrated from github.com) reviewed 2024-06-16 13:36:33 +00:00
@ -1,5 +1,4 @@
NUT-02: Keysets and keyset ID
==========================
# NUT-02: Keysets and fees
callebtc (Migrated from github.com) commented 2024-06-16 13:36:32 +00:00

Addressed in b7d64fd.

Addressed in [b7d64fd](https://github.com/cashubtc/nuts/pull/126/commits/b7d64fdcae5b9e4225181154c8bd07a979314a2f).
callebtc (Migrated from github.com) reviewed 2024-06-16 15:09:59 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
callebtc (Migrated from github.com) commented 2024-06-16 15:09:59 +00:00

But you could also end up with weird flows like paying fees in one way and the possibility of asking for back fees in another way

Could you make an example?

> But you could also end up with weird flows like paying fees in one way and the possibility of asking for back fees in another way Could you make an example?
conduition (Migrated from github.com) reviewed 2024-06-18 15:12:28 +00:00
@ -93,1 +58,4 @@
#### Keyset ID version
Keyset IDs have a version byte (two hexadecimal characters). The currently used version byte is `00`.
conduition (Migrated from github.com) commented 2024-06-18 15:12:28 +00:00

Still missing proof.amount here:

    sum_fees += proof.amount * keysets[proof.id].input_fee_ppk
Still missing `proof.amount` here: ```suggestion sum_fees += proof.amount * keysets[proof.id].input_fee_ppk ```
elnosh (Migrated from github.com) reviewed 2024-06-18 15:31:33 +00:00
@ -38,0 +47,4 @@
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000
elnosh (Migrated from github.com) commented 2024-06-18 15:31:33 +00:00

Could you make an example?

I was hung up with having 2 different fee fields (input fees and lightning fees) so it was that scenario but seems that should be the way forward in order to be able to charge by input.

> Could you make an example? I was hung up with having 2 different fee fields (input fees and lightning fees) so it was that scenario but seems that should be the way forward in order to be able to charge by input.
callebtc (Migrated from github.com) reviewed 2024-06-26 21:16:03 +00:00
@ -93,1 +58,4 @@
#### Keyset ID version
Keyset IDs have a version byte (two hexadecimal characters). The currently used version byte is `00`.
callebtc (Migrated from github.com) commented 2024-06-26 21:16:03 +00:00

Still missing proof.amount here:

The fees are not amount-dependent.

> Still missing `proof.amount` here: The fees are not amount-dependent.
thesimplekid (Migrated from github.com) approved these changes 2024-06-27 16:34:21 +00:00
Egge21M (Migrated from github.com) approved these changes 2024-06-27 16:59:51 +00:00
elnosh (Migrated from github.com) approved these changes 2024-06-27 17:02:48 +00:00
lescuer97 commented 2024-07-27 08:41:22 +00:00 (Migrated from github.com)
merged in nutmix. https://github.com/lescuer97/nutmix/pull/74
Sign in to join this conversation.
No description provided.