From 027b1901469465c96a9d394f0cd2e405a09c47c9 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 13 Dec 2022 12:16:24 +0100 Subject: [PATCH 01/17] Adds fee specs --- .../specs/src/economics/fee-system.md | 144 +++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 976bcb1264..dc8f9f1cc7 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -1,7 +1,143 @@ -## Fee system +# Fee system -In order to be accepted by the Namada ledger, transactions must pay fees. Transaction fees serve two purposes: first, the efficient allocation of block space given permissionless transaction submission and varying demand, and second, incentive-compatibility to encourage block producers to add transactions to the blocks which they create and publish. +In order to be accepted by the Namada ledger, transactions must pay fees. Transaction fees serve two purposes: first, the efficient allocation of block space and gas (which are scarce resources) given permissionless transaction submission and varying demand, and second, incentive-compatibility to encourage block producers to add transactions to the blocks which they create and publish. -Namada transaction fees can be paid in any fungible token which is a member of a whitelist controlled by Namada governance. Governance also sets minimum fee rates (which can be periodically updated so that they are usually sufficient) which transactions must pay in order to be accepted (but they can always pay more to encourage the proposer to prioritise them). When using the shielded pool, transactions can also unshield tokens in order to pay the required fees. +Namada transaction fees can be paid in any fungible token which is a member of a whitelist controlled by Namada governance. Governance also sets minimum fee rates (which can be periodically updated so that they are usually sufficient) which transactions must pay in order to be accepted (but they can always pay more to encourage the proposer to prioritize them). When using the shielded pool, transactions can also unshield tokens in order to pay the required fees. -The token whitelist consists of a list of $(T, GP_{min})$ pairs, where $T$ is a token identifier and $GP_{min}$ is the minimum price per unit gas which must be paid by a transaction paying fees using that asset. This whitelist can be updated with a standard governance proposal. All fees collected are paid directly to the block proposer (incentive-compatible, so that side payments are no more profitable). \ No newline at end of file +The token whitelist consists of a list of $(T, GP_{min})$ pairs, where $T$ is a token identifier and $GP_{min}$ is the minimum (base) price per unit gas which must be paid by a transaction paying fees using that asset. This whitelist can be updated with a standard governance proposal. All fees collected are paid directly to the block proposer (incentive-compatible, so that side payments are no more profitable). + +Fees are distributed among the delegators with the mechanism explained in the [POS](./proof-of-stake/reward-distribution.md) specs. + +Fees are only meant for `InnerTx` transactions: `WrapperTx`s are not subject to them. + +## Fee payment + +The `WrapperTx` struct holds all the data necessary for the payment of fees in the form of the types: `Fee`, `GasLimit` and the `PublicKey` used to derive the address of the fee payer which coincides with the signer of the wrapper transaction itself. + +Since fees have a purpose in allocating scarce block resources (space and gas limit) they have to be paid upfront, as soon as the transaction is deemed valid and accepted into a block (refer to [replay protection](../base-ledger/replay-protection.md) specs for more details on transactions' validity). Moreover, for the same reasons, the fee payer will pay for the entire `GasLimit` allocated and not the actual gas consumed for the transaction: this will incentivize fee payers to stick to a reasonable gas limit for their transactions allowing for the inclusion of more transactions into a block. Since the gas used by a transaction leaks a bit of information about the transaction itself: a submitter may want to obfuscate this value a bit by increasing the gas limit of the wrapper transaction but he will be charged for this (refer to section 2.1.3 of the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). + +Fees are not distributed among the validators who actively participate in the block validation process. This is because a tx submitter could be side-paying the block proposer for tx inclusion which would prevent the correct distribution of fees among validators. The fair distribution of fees is enforced by the block proposer rotation policy of Tendermint. + +By requesting an upfront payment, fees also serve as prevention against DOS attacks since the signer needs to pay for all the submitted transactions. More specifically, to serve as a denial-of-service and spam prevention mechanism, fee payment needs to enforce: + +1. **Succesful** payment at block inclusion time (implying the ability to check the good outcome at block creation time) +2. Minimal payment overhead in terms of computation/memory requirements (otherwise fee payment itself could be exploited as a DOS vector) + +Given that transactions are executed in the same order they appear in the block this will lead to a common behavior across all the block proposers: they'll tend to place all the wrapper transactions before the decrypted transactions coming from the previous block. By doing this, they will make sure to prevent inner transactions from draining the addresses of the funds needed to pay fees. The proposer will be able to check in advance that fee payers have enough unshielded funds and, if this is not the case, exclude the transaction from the block and leave it in the mempool for future inclusion. This behavior ultimately leads to more resource-optimized blocks. + +As a drawback, this behavior could cause some inner txs coming from the previous block to fail (in case they involve an unshielded transfer) because funds have been moved to the block proposer as a fee payment for a `WrapperTx` included in the same block. This is somehow undesirable since inner transactions' execution should have priority over the wrapper. There are two ways to overcome this issue: + +1. Users are responsible for correctly timing/funding their transactions with the help of the wallet +2. We force in protocol that a block should list the wrappers after the decrypted transactions + +If we follow the second option the block proposers will no more be able to optimize the block (this would require running the inner transactions to calculate the possibly new unshielded balance) and, inevitably, some wrapper transactions for which fees cannot be paid will end up in the block. These will be deemed invalid during validation so that the corresponding inner transaction will not be executed, preserving the correctness of the state machine, but it represents a slight underoptimization of the block and a potential vector for DOS attacks since the invalid wrapper has allocated space and gas in the block without being charged due to the lack of funds. Because of this, we stick to the first option by not imposing any specific order in procotol. + +Fees are collected via protocol, in the `finalize_block` function, for `WrapperTx`s which have been processed with success: this is to prevent a malicious block proposer from including transactions that are known in advance to be invalid just to collect more fees. Given the two-block execution model of Namada (wrapper and inner tx) and the need to collect fees for the allocated resources, nothing can be done in case the inner transaction fails: by that point, fees have already been collected and no refunds will be issued, meaning that the inner tx signer is responsible for submitting a semantically valid transaction for the state of the application (importance on the lifetime parameter of the tx here). If enough funds are available, these are deducted from the unshielded storage balances of the fee payers and directed to the balance of the block proposer: payers (wrapper tx signers) are responsible for keeping enough unshielded funds for their transactions. If instead, the balance is not enough to cover fees, then the corresponding inner transaction gets discarded and no funds are moved from the payer address to the block proposer. This is due to the following reasons: + +- It would penalize the tx submitter who might not be responsible for the lack of funds at that moment +- Moving insufficient funds would incentivize the block proposer to include transactions for which fees cannot be paid. Since the block proposer knows the balances of the involved addresses at block creation time and given the strategy of placing wrapper txs first, this would constitute malicious behavior by the proposer + +By discarding the transaction without paying fees instead we avoid these pitfalls. This logic implies that the strategy of placing wrapper transactions before any decrypted tx in the block will be reinforced. It might look like a contradiction to what was said before, a wrapper transaction included in a block will not pay fees for the resources it acquired, but this is not true: + +- An address might have no funds at all making it impossible to perform any payment +- The only actor with an interest in managing block resources is the block proposer who's aware of the balances of the involved addresses + +To support the in-protocol fee payment mechanism we need to update the `Header` struct to carry the `ProposerAddress`: + +```rust +/// The data from Tendermint header +/// relevant for Anoma storage +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] +pub struct Header { + /// Merkle root hash of block + pub hash: Hash, + /// Timestamp associated to block + pub time: DateTimeUtc, + /// Hash of the addresses of the next validator set + pub next_validators_hash: Hash, + /// Address of the block proposer + pub proposer_address: Address +} +``` + +From this address, it is then possible to derive the relative Namada address for the payment. + +The `Fee` field of `WrapperTx` is defined as follows: + +```rust +pub struct Fee { + /// amount of the fee + pub amount: Amount, + /// address of the token + pub token: Address, +} +``` + +The signer of the wrapper transaction defines the token in which fees must be paid among those available in the token whitelist. At the same time, he also sets the amount which must meet the minimum price per gas unit for that token $GP_{min}$ (also defined in the whitelist). The difference between the minimum and the actual value set by the submitter represents the incentive for the block proposer to prefer the inclusion of this transaction over other ones. + +The block proposer can check the validity of these two parameters while constructing the block. These validity checks are also replicated in `process_proposal` and `mempool_check`. In case a transaction with invalid parameters ended up in a block, it would be discarded without paying any fee (as already explained earlier in this document). The block proposer and the `process_proposal` function should also cache the available funds of every fee-paying address involved in the block: this is because a signer might submit more than one transaction per block and the check on funds should take into consideration the updated value of the unshielded balance. + +Since the whitelist can be changed via governance, transactions could fail these checks in the block where the whitelist change happens. For `mempool_check`, the checks could reject transactions that may become valid in the future or vice-versa: since we can assume a slow rate of change for these parameters and mempool and block space optimizations are a priority, it is up to the clients to track any changes of these parameters and act accordingly. + +## Gas accounting + +We provide a mapping between all the whitelisted transactions and VPs to their cost in gas units. Being the cost hardcoded, it is guaranteed that the same transaction will always require the same amount of gas: since the price per gas is controlled via governance, though, the price for the same transaction may vary in time. A transaction is also charged with the gas required by the validity predicates that it triggers. + +Gas accounting is about preventing a transaction from exceeding two gas limits: + +1. Its own `GasLimit` (declared in the wrapper transaction) +2. The gas limit of the entire block + +### Wrapper GasLimit + +The protocol injects a gas counter in each transaction and VP to be executed which allows monitoring of the exact amount of gas utilized. To do so, the gas meter simply checks the hash of the transaction or VP against the table in storage to determine which one it is and, from there, derives the amount of gas required. + +To perform the check we need the limit which was declared by the corresponding wrapper transaction: this limit should be saved in storage together with the queue of encrypted transactions for easy access. + +Since the hash can be retrieved as soon as the transaction gets decrypted, we can immediately check whether the `GasLimit` set in the corresponding wrapper is enough to cover this amount. This check, though, is weak because we also need to keep in account the gas required for the involved VPs which is hard to determine ahead of time: this is just an optimization to short-circuit the execution of transactions whose gas limit is not enough to cover for even the tx itself. + +When executing the VPs the procedure is the same and, again, since we know ahead of time the gas required by each VP we can immediately terminate the execution if it overshoots the limit. + +In any case, if the gas limit is exceeded, the transaction is considered invalid and all the modifications applied to the WAL get discarded. This doesn't affect the other transactions which can be executed normally (see the following section). + +### Block GasLimit + +This constraint is given by the following two: + +- The compliance of each inner transaction with the `WrapperTx` gas limit explained in the previous section +- The compliance of the cumulative wrapper transactions' `GasLimit` with the maximum gas allowed for a block + +Tendermint doesn't provide more than the `BlockSize.MaxGas` parameter, leaving the validation step to the application (see [tendermint spec](https://github.com/tendermint/tendermint/blob/29e5fbcc648510e4763bd0af0b461aed92c21f30/spec/core/data_structures.md#consensusparams) and [issue](https://github.com/tendermint/tendermint/issues/2310)): therefore, instead of using the Tendermint provided param, Namada introduces a `MaxBlockGas` protocol parameter. +This limit is checked during block validation, in `process_proposal`: if the block exceeds the maximum amount of gas allowed, the validators will still accept the block and discard only the excess transactions. + +Note that block gas limit validation should always occur against the `GasLimit` declared in the wrappers, not the real gas used by the inner transactions. If this was the case, in fact, a malicious proposer could craft a block exceeding the gas limit with the hope that some transaction may use less gas than declared: but if this doesn't happen, then the last transactions of the block will be rejected because they would exceed the block gas limit even though they were charged with fees in the previous block, effectively suffering economic damage. In this sense, since the wrapper tx gas limit imposes an economic constraint, it is the reference point for all the gas limit checks. + +Given that the block allocates a certain gas for each transaction and that transactions are prevented from going out of gas, it derives that the execution of each transaction is isolated from all the other ones in terms of gas, which explains the last statement of the previous section. + +## Checks + +This section summarizes the checks performed in protocol. + + + +|Method|Checks| +|---|---| +|`CheckTx` and `ProcessProposal`| | +|`ProcessProposal`|| +|`FinalizeBlock`| | + +## Alternatives considered + +A drawback of the proposed implementation is that fee payment can only occur from an unshielded balance. This restricts the sources from which a user can gather the funds necessary for the transaction and may also cause a locked-out problem in which a user finds himself with no more unshielded funds, making it impossible for him to operate on the chain. In this case, the user could always reach out to another user to sign wrapper txs for him (ideally to unshield some tokens). + +An alternative solution could be to allow the signer of a wrapper to perform a transfer transaction to pay fees. To do so we would need the `WrapperTx` struct to hold a signed `Transfer` struct in plaintext carrying all the information regarding the transfer. The transaction itself can be crafted in protocol by validators to reduce the burden of the messages on the network and to prevent users from including arbitrary transactions. Since the block proposer is not known ahead of time, we would also need to implement a new internal address with the relative VP to which users should pay the fees. The block proposer could then redeem the tokens from there. This mechanism could function in two ways: + +1. The internal VP prevents any movement of funds from the internal address and the withdraws happen via protocol +2. The internal VP has a way to retrieve the current block proposer and allows only him to withdraw. The block proposer then inserts a last transaction in the block (without going out of gas or size) to redeem all of the tokens in the internal address balance + +Unfortunately, at the benefit of a more generalized fee payment mechanism, this solution adds the following cons: + +- The need for an additional internal VP +- Overhead given by the transfer execution which could become a possible DOS vector in case a lot of transfers failed +- Transfer execution would technically require gas making the problem recursive +- Checking the funds in advance would be harder \ No newline at end of file From 4857e98962ffb9f82bef593109c74cf20e57d6c8 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 19 Dec 2022 17:06:20 +0100 Subject: [PATCH 02/17] Updates fee specs --- .../specs/src/economics/fee-system.md | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index dc8f9f1cc7..d76e792ca7 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -6,15 +6,15 @@ Namada transaction fees can be paid in any fungible token which is a member of a The token whitelist consists of a list of $(T, GP_{min})$ pairs, where $T$ is a token identifier and $GP_{min}$ is the minimum (base) price per unit gas which must be paid by a transaction paying fees using that asset. This whitelist can be updated with a standard governance proposal. All fees collected are paid directly to the block proposer (incentive-compatible, so that side payments are no more profitable). -Fees are distributed among the delegators with the mechanism explained in the [POS](./proof-of-stake/reward-distribution.md) specs. +Fees are distributed among the delegators with the mechanism explained in the [proof-of-stake reward distribution specs](./proof-of-stake/reward-distribution.md). -Fees are only meant for `InnerTx` transactions: `WrapperTx`s are not subject to them. +Fees are only meant for `InnerTx` transactions: `WrapperTx`s carry information about fees for the relative inner tx but are not subject to fees themselves. ## Fee payment The `WrapperTx` struct holds all the data necessary for the payment of fees in the form of the types: `Fee`, `GasLimit` and the `PublicKey` used to derive the address of the fee payer which coincides with the signer of the wrapper transaction itself. -Since fees have a purpose in allocating scarce block resources (space and gas limit) they have to be paid upfront, as soon as the transaction is deemed valid and accepted into a block (refer to [replay protection](../base-ledger/replay-protection.md) specs for more details on transactions' validity). Moreover, for the same reasons, the fee payer will pay for the entire `GasLimit` allocated and not the actual gas consumed for the transaction: this will incentivize fee payers to stick to a reasonable gas limit for their transactions allowing for the inclusion of more transactions into a block. Since the gas used by a transaction leaks a bit of information about the transaction itself: a submitter may want to obfuscate this value a bit by increasing the gas limit of the wrapper transaction but he will be charged for this (refer to section 2.1.3 of the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). +Since fees have a purpose in allocating scarce block resources (space and gas limit) they have to be paid upfront, as soon as the transaction is deemed valid and accepted into a block (refer to [replay protection](../base-ledger/replay-protection.md) specs for more details on transactions' validity). Moreover, for the same reasons, the fee payer will pay for the entire `GasLimit` allocated and not the actual gas consumed for the transaction: this will incentivize fee payers to stick to a reasonable gas limit for their transactions allowing for the inclusion of more transactions into a block. Since the gas used by a transaction leaks a bit of information about the transaction itself: a submitter may want to obfuscate this value a bit by increasing the gas limit of the wrapper transaction but they will be charged for this (refer to section 2.1.3 of the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). Fees are not distributed among the validators who actively participate in the block validation process. This is because a tx submitter could be side-paying the block proposer for tx inclusion which would prevent the correct distribution of fees among validators. The fair distribution of fees is enforced by the block proposer rotation policy of Tendermint. @@ -32,15 +32,7 @@ As a drawback, this behavior could cause some inner txs coming from the previous If we follow the second option the block proposers will no more be able to optimize the block (this would require running the inner transactions to calculate the possibly new unshielded balance) and, inevitably, some wrapper transactions for which fees cannot be paid will end up in the block. These will be deemed invalid during validation so that the corresponding inner transaction will not be executed, preserving the correctness of the state machine, but it represents a slight underoptimization of the block and a potential vector for DOS attacks since the invalid wrapper has allocated space and gas in the block without being charged due to the lack of funds. Because of this, we stick to the first option by not imposing any specific order in procotol. -Fees are collected via protocol, in the `finalize_block` function, for `WrapperTx`s which have been processed with success: this is to prevent a malicious block proposer from including transactions that are known in advance to be invalid just to collect more fees. Given the two-block execution model of Namada (wrapper and inner tx) and the need to collect fees for the allocated resources, nothing can be done in case the inner transaction fails: by that point, fees have already been collected and no refunds will be issued, meaning that the inner tx signer is responsible for submitting a semantically valid transaction for the state of the application (importance on the lifetime parameter of the tx here). If enough funds are available, these are deducted from the unshielded storage balances of the fee payers and directed to the balance of the block proposer: payers (wrapper tx signers) are responsible for keeping enough unshielded funds for their transactions. If instead, the balance is not enough to cover fees, then the corresponding inner transaction gets discarded and no funds are moved from the payer address to the block proposer. This is due to the following reasons: - -- It would penalize the tx submitter who might not be responsible for the lack of funds at that moment -- Moving insufficient funds would incentivize the block proposer to include transactions for which fees cannot be paid. Since the block proposer knows the balances of the involved addresses at block creation time and given the strategy of placing wrapper txs first, this would constitute malicious behavior by the proposer - -By discarding the transaction without paying fees instead we avoid these pitfalls. This logic implies that the strategy of placing wrapper transactions before any decrypted tx in the block will be reinforced. It might look like a contradiction to what was said before, a wrapper transaction included in a block will not pay fees for the resources it acquired, but this is not true: - -- An address might have no funds at all making it impossible to perform any payment -- The only actor with an interest in managing block resources is the block proposer who's aware of the balances of the involved addresses +Fees are collected via protocol, in the `finalize_block` function, for `WrapperTx`s which have been processed with success: this is to prevent a malicious block proposer from including transactions that are known in advance to be invalid just to collect more fees. Given the two-block execution model of Namada (wrapper and inner tx) and the need to collect fees for the allocated resources, nothing can be done in case the inner transaction fails: by that point, fees have already been collected and no refunds will be issued, meaning that the inner tx signer is responsible for submitting a semantically valid transaction for the state of the application (importance on the lifetime parameter of the tx here). If enough funds are available, these are deducted from the unshielded storage balances of the fee payers and directed to the balance of the block proposer: payers (wrapper tx signers) are responsible for keeping enough unshielded funds for their transactions. If instead, the balance is not enough to cover fees, then the proposed block is considered invalid and rejected, initiating a new Tendermint round: this logic implies that the strategy of placing wrapper transactions before any decrypted tx in the block will be reinforced. To support the in-protocol fee payment mechanism we need to update the `Header` struct to carry the `ProposerAddress`: @@ -75,13 +67,13 @@ pub struct Fee { The signer of the wrapper transaction defines the token in which fees must be paid among those available in the token whitelist. At the same time, he also sets the amount which must meet the minimum price per gas unit for that token $GP_{min}$ (also defined in the whitelist). The difference between the minimum and the actual value set by the submitter represents the incentive for the block proposer to prefer the inclusion of this transaction over other ones. -The block proposer can check the validity of these two parameters while constructing the block. These validity checks are also replicated in `process_proposal` and `mempool_check`. In case a transaction with invalid parameters ended up in a block, it would be discarded without paying any fee (as already explained earlier in this document). The block proposer and the `process_proposal` function should also cache the available funds of every fee-paying address involved in the block: this is because a signer might submit more than one transaction per block and the check on funds should take into consideration the updated value of the unshielded balance. +The block proposer can check the validity of these two parameters while constructing the block. These validity checks are also replicated in `process_proposal` and `mempool_check`. In case a transaction with invalid parameters ended up in a block, the entire block would be rejected (as already explained earlier in this document). The block proposer and the `process_proposal` function should also cache the available funds of every fee-paying address involved in the block: this is because a signer might submit more than one transaction per block and the check on funds should take into consideration the updated value of the unshielded balance. Since the whitelist can be changed via governance, transactions could fail these checks in the block where the whitelist change happens. For `mempool_check`, the checks could reject transactions that may become valid in the future or vice-versa: since we can assume a slow rate of change for these parameters and mempool and block space optimizations are a priority, it is up to the clients to track any changes of these parameters and act accordingly. ## Gas accounting -We provide a mapping between all the whitelisted transactions and VPs to their cost in gas units. Being the cost hardcoded, it is guaranteed that the same transaction will always require the same amount of gas: since the price per gas is controlled via governance, though, the price for the same transaction may vary in time. A transaction is also charged with the gas required by the validity predicates that it triggers. +We provide a mapping between all the whitelisted transactions and VPs to their cost in gas units: more specifically, the cost of a tx/VP is given by the run time cost of its wasm code. As the cost is constant, it is guaranteed that the same transaction will always require the same amount of gas: since the price per gas unit is controlled via governance, though, the price for the same transaction may vary in time. A transaction is also charged with the gas required by the validity predicates that it triggers. Gas accounting is about preventing a transaction from exceeding two gas limits: @@ -96,7 +88,7 @@ To perform the check we need the limit which was declared by the corresponding w Since the hash can be retrieved as soon as the transaction gets decrypted, we can immediately check whether the `GasLimit` set in the corresponding wrapper is enough to cover this amount. This check, though, is weak because we also need to keep in account the gas required for the involved VPs which is hard to determine ahead of time: this is just an optimization to short-circuit the execution of transactions whose gas limit is not enough to cover for even the tx itself. -When executing the VPs the procedure is the same and, again, since we know ahead of time the gas required by each VP we can immediately terminate the execution if it overshoots the limit. +When executing the VPs in parallel the procedure is the same and, again, since we know ahead of time the gas required by each VP we can immediately terminate the execution if it overshoots the limit. In any case, if the gas limit is exceeded, the transaction is considered invalid and all the modifications applied to the WAL get discarded. This doesn't affect the other transactions which can be executed normally (see the following section). @@ -108,7 +100,7 @@ This constraint is given by the following two: - The compliance of the cumulative wrapper transactions' `GasLimit` with the maximum gas allowed for a block Tendermint doesn't provide more than the `BlockSize.MaxGas` parameter, leaving the validation step to the application (see [tendermint spec](https://github.com/tendermint/tendermint/blob/29e5fbcc648510e4763bd0af0b461aed92c21f30/spec/core/data_structures.md#consensusparams) and [issue](https://github.com/tendermint/tendermint/issues/2310)): therefore, instead of using the Tendermint provided param, Namada introduces a `MaxBlockGas` protocol parameter. -This limit is checked during block validation, in `process_proposal`: if the block exceeds the maximum amount of gas allowed, the validators will still accept the block and discard only the excess transactions. +This limit is checked during block validation, in `process_proposal`: if the block exceeds the maximum amount of gas allowed, the validators will reject it. Note that block gas limit validation should always occur against the `GasLimit` declared in the wrappers, not the real gas used by the inner transactions. If this was the case, in fact, a malicious proposer could craft a block exceeding the gas limit with the hope that some transaction may use less gas than declared: but if this doesn't happen, then the last transactions of the block will be rejected because they would exceed the block gas limit even though they were charged with fees in the previous block, effectively suffering economic damage. In this sense, since the wrapper tx gas limit imposes an economic constraint, it is the reference point for all the gas limit checks. @@ -118,8 +110,6 @@ Given that the block allocates a certain gas for each transaction and that trans This section summarizes the checks performed in protocol. - - |Method|Checks| |---|---| |`CheckTx` and `ProcessProposal`| | From 3a488787aa46e494b687d6b3f246d1dfaef0ad4b Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 22 Dec 2022 16:28:23 +0100 Subject: [PATCH 03/17] Adds unshielding to fee specs --- .../specs/src/economics/fee-system.md | 349 ++++++++++++++---- 1 file changed, 286 insertions(+), 63 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index d76e792ca7..7e7beb897b 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -1,40 +1,151 @@ # Fee system -In order to be accepted by the Namada ledger, transactions must pay fees. Transaction fees serve two purposes: first, the efficient allocation of block space and gas (which are scarce resources) given permissionless transaction submission and varying demand, and second, incentive-compatibility to encourage block producers to add transactions to the blocks which they create and publish. - -Namada transaction fees can be paid in any fungible token which is a member of a whitelist controlled by Namada governance. Governance also sets minimum fee rates (which can be periodically updated so that they are usually sufficient) which transactions must pay in order to be accepted (but they can always pay more to encourage the proposer to prioritize them). When using the shielded pool, transactions can also unshield tokens in order to pay the required fees. - -The token whitelist consists of a list of $(T, GP_{min})$ pairs, where $T$ is a token identifier and $GP_{min}$ is the minimum (base) price per unit gas which must be paid by a transaction paying fees using that asset. This whitelist can be updated with a standard governance proposal. All fees collected are paid directly to the block proposer (incentive-compatible, so that side payments are no more profitable). - -Fees are distributed among the delegators with the mechanism explained in the [proof-of-stake reward distribution specs](./proof-of-stake/reward-distribution.md). - -Fees are only meant for `InnerTx` transactions: `WrapperTx`s carry information about fees for the relative inner tx but are not subject to fees themselves. +In order to be accepted by the Namada ledger, transactions must pay fees. +Transaction fees serve two purposes: first, the efficient allocation of block +space and gas (which are scarce resources) given permissionless transaction +submission and varying demand, and second, incentive-compatibility to encourage +block producers to add transactions to the blocks which they create and publish. + +Namada transaction fees can be paid in any fungible token which is a member of a +whitelist controlled by Namada governance. Governance also sets minimum fee +rates (which can be periodically updated so that they are usually sufficient) +which transactions must pay in order to be accepted (but they can always pay +more to encourage the proposer to prioritize them). When using the shielded +pool, transactions can also unshield tokens in order to pay the required fees. + +The token whitelist consists of a list of $(T, GP_{min})$ pairs, where $T$ is a +token identifier and $GP_{min}$ is the minimum (base) price per unit gas which +must be paid by a transaction paying fees using that asset. This whitelist can +be updated with a standard governance proposal. All fees collected are paid +directly to the block proposer (incentive-compatible, so that side payments are +no more profitable). + +Fees are distributed among the delegators with the mechanism explained in the +[proof-of-stake reward distribution specs](./proof-of-stake/reward-distribution.md). + +Fees are only meant for `InnerTx` transactions: `WrapperTx`s carry information +about fees for the relative inner tx but are not subject to fees themselves. ## Fee payment -The `WrapperTx` struct holds all the data necessary for the payment of fees in the form of the types: `Fee`, `GasLimit` and the `PublicKey` used to derive the address of the fee payer which coincides with the signer of the wrapper transaction itself. - -Since fees have a purpose in allocating scarce block resources (space and gas limit) they have to be paid upfront, as soon as the transaction is deemed valid and accepted into a block (refer to [replay protection](../base-ledger/replay-protection.md) specs for more details on transactions' validity). Moreover, for the same reasons, the fee payer will pay for the entire `GasLimit` allocated and not the actual gas consumed for the transaction: this will incentivize fee payers to stick to a reasonable gas limit for their transactions allowing for the inclusion of more transactions into a block. Since the gas used by a transaction leaks a bit of information about the transaction itself: a submitter may want to obfuscate this value a bit by increasing the gas limit of the wrapper transaction but they will be charged for this (refer to section 2.1.3 of the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). - -Fees are not distributed among the validators who actively participate in the block validation process. This is because a tx submitter could be side-paying the block proposer for tx inclusion which would prevent the correct distribution of fees among validators. The fair distribution of fees is enforced by the block proposer rotation policy of Tendermint. +The `WrapperTx` struct holds all the data necessary for the payment of fees in +the form of the types: `Fee`, `GasLimit` and the `PublicKey` used to derive the +address of the fee payer which coincides with the signer of the wrapper +transaction itself. + +Since fees have a purpose in allocating scarce block resources (space and gas +limit) they have to be paid upfront, as soon as the transaction is deemed valid +and accepted into a block (refer to +[replay protection](../base-ledger/replay-protection.md) specs for more details +on transactions' validity). Moreover, for the same reasons, the fee payer will +pay for the entire `GasLimit` allocated and not the actual gas consumed for the +transaction: this will incentivize fee payers to stick to a reasonable gas limit +for their transactions allowing for the inclusion of more transactions into a +block. Since the gas used by a transaction leaks a bit of information about the +transaction itself: a submitter may want to obfuscate this value a bit by +increasing the gas limit of the wrapper transaction but they will be charged for +this (refer to section 2.1.3 of the Ferveo +[documentation](https://eprint.iacr.org/2022/898.pdf)). + +Fees are not distributed among the validators who actively participate in the +block validation process. This is because a tx submitter could be side-paying +the block proposer for tx inclusion which would prevent the correct distribution +of fees among validators. The fair distribution of fees is enforced by the block +proposer rotation policy of Tendermint. + +By requesting an upfront payment, fees also serve as prevention against DOS +attacks since the signer needs to pay for all the submitted transactions. More +specifically, to serve as a denial-of-service and spam prevention mechanism, fee +payment needs to enforce: + +1. **Succesful** payment at block inclusion time (implying the ability to check + the good outcome at block creation time) +2. Minimal payment overhead in terms of computation/memory requirements + (otherwise fee payment itself could be exploited as a DOS vector) + +Given that transactions are executed in the same order they appear in the block +this will lead to a common behavior across all the block proposers: they'll tend +to place all the wrapper transactions before the decrypted transactions coming +from the previous block. By doing this, they will make sure to prevent inner +transactions from draining the addresses of the funds needed to pay fees. The +proposer will be able to check in advance that fee payers have enough unshielded +funds and, if this is not the case, exclude the transaction from the block and +leave it in the mempool for future inclusion. This behavior ultimately leads to +more resource-optimized blocks. + +As a drawback, this behavior could cause some inner txs coming from the previous +block to fail (in case they involve an unshielded transfer) because funds have +been moved to the block proposer as a fee payment for a `WrapperTx` included in +the same block. This is somehow undesirable since inner transactions' execution +should have priority over the wrapper. There are two ways to overcome this +issue: + +1. Users are responsible for correctly timing/funding their transactions with + the help of the wallet +2. We force in protocol that a block should list the wrappers after the + decrypted transactions + +If we follow the second option the block proposers will no more be able to +optimize the block (this would require running the inner transactions to +calculate the possibly new unshielded balance) and, inevitably, some wrapper +transactions for which fees cannot be paid will end up in the block. These will +be deemed invalid during validation so that the corresponding inner transaction +will not be executed, preserving the correctness of the state machine, but it +represents a slight underoptimization of the block and a potential vector for +DOS attacks since the invalid wrapper has allocated space and gas in the block +without being charged due to the lack of funds. Because of this, we stick to the +first option by not imposing any specific order in procotol. + +Fees are collected via protocol, in the `finalize_block` function, for +`WrapperTx`s which have been processed with success: this is to prevent a +malicious block proposer from including transactions that are known in advance +to be invalid just to collect more fees. Given the two-block execution model of +Namada (wrapper and inner tx) and the need to collect fees for the allocated +resources, nothing can be done in case the inner transaction fails: by that +point, fees have already been collected and no refunds will be issued, meaning +that the inner tx signer is responsible for submitting a semantically valid +transaction for the state of the application (importance on the lifetime +parameter of the tx here). + +If enough funds are available, these are deducted from the unshielded storage +balances of the fee payers and directed to the balance of the block proposer. To +prevent a possible locked-out problem in which a user doesn't have enough funds +to pay fees (preventing any sort of operation on the chain), Namada allows the +signer of the wrapper transaction to unshield some funds on the go to cover the +cost of the fee. To support this mechanism the `WrapperTx` struct must be +extended as follows: -By requesting an upfront payment, fees also serve as prevention against DOS attacks since the signer needs to pay for all the submitted transactions. More specifically, to serve as a denial-of-service and spam prevention mechanism, fee payment needs to enforce: - -1. **Succesful** payment at block inclusion time (implying the ability to check the good outcome at block creation time) -2. Minimal payment overhead in terms of computation/memory requirements (otherwise fee payment itself could be exploited as a DOS vector) - -Given that transactions are executed in the same order they appear in the block this will lead to a common behavior across all the block proposers: they'll tend to place all the wrapper transactions before the decrypted transactions coming from the previous block. By doing this, they will make sure to prevent inner transactions from draining the addresses of the funds needed to pay fees. The proposer will be able to check in advance that fee payers have enough unshielded funds and, if this is not the case, exclude the transaction from the block and leave it in the mempool for future inclusion. This behavior ultimately leads to more resource-optimized blocks. - -As a drawback, this behavior could cause some inner txs coming from the previous block to fail (in case they involve an unshielded transfer) because funds have been moved to the block proposer as a fee payment for a `WrapperTx` included in the same block. This is somehow undesirable since inner transactions' execution should have priority over the wrapper. There are two ways to overcome this issue: - -1. Users are responsible for correctly timing/funding their transactions with the help of the wallet -2. We force in protocol that a block should list the wrappers after the decrypted transactions +```rust +pub struct WrapperTx { + /// The fee to be paid for including the tx + pub fee: Fee, + /// Used to determine an implicit account of the fee payer + pub pk: common::PublicKey, + /// Max amount of gas that can be used when executing the inner tx + pub gas_limit: GasLimit, + /// The optional unshielding data for fee payment + pub unshield: Option, + /// the encrypted payload + pub inner_tx: EncryptedTx, + /// sha-2 hash of the inner transaction acting as a commitment + /// the contents of the encrypted payload + pub tx_hash: Hash, +} +``` -If we follow the second option the block proposers will no more be able to optimize the block (this would require running the inner transactions to calculate the possibly new unshielded balance) and, inevitably, some wrapper transactions for which fees cannot be paid will end up in the block. These will be deemed invalid during validation so that the corresponding inner transaction will not be executed, preserving the correctness of the state machine, but it represents a slight underoptimization of the block and a potential vector for DOS attacks since the invalid wrapper has allocated space and gas in the block without being charged due to the lack of funds. Because of this, we stick to the first option by not imposing any specific order in procotol. +The new `unshield` field carries an optional `SignedTxData` struct encoding for +an unshielding `Transfer`. The tx itself is crafted in protocol by the +validators to reduce the burden of the messages on the network and to prevent +users from including arbitrary transactions. This unshielding operation is +exempted from paying fees and doesn't charge gas. -Fees are collected via protocol, in the `finalize_block` function, for `WrapperTx`s which have been processed with success: this is to prevent a malicious block proposer from including transactions that are known in advance to be invalid just to collect more fees. Given the two-block execution model of Namada (wrapper and inner tx) and the need to collect fees for the allocated resources, nothing can be done in case the inner transaction fails: by that point, fees have already been collected and no refunds will be issued, meaning that the inner tx signer is responsible for submitting a semantically valid transaction for the state of the application (importance on the lifetime parameter of the tx here). If enough funds are available, these are deducted from the unshielded storage balances of the fee payers and directed to the balance of the block proposer: payers (wrapper tx signers) are responsible for keeping enough unshielded funds for their transactions. If instead, the balance is not enough to cover fees, then the proposed block is considered invalid and rejected, initiating a new Tendermint round: this logic implies that the strategy of placing wrapper transactions before any decrypted tx in the block will be reinforced. +If the balance is not enough to cover fees, then the proposed block is +considered invalid and rejected, initiating a new Tendermint round: this logic +implies that the strategy of placing wrapper transactions before any decrypted +tx in the block will be reinforced. -To support the in-protocol fee payment mechanism we need to update the `Header` struct to carry the `ProposerAddress`: +To support the in-protocol fee payment mechanism we need to update the `Header` +struct to carry the `ProposerAddress`: ```rust /// The data from Tendermint header @@ -52,7 +163,8 @@ pub struct Header { } ``` -From this address, it is then possible to derive the relative Namada address for the payment. +From this address, it is then possible to derive the relative Namada address for +the payment. The `Fee` field of `WrapperTx` is defined as follows: @@ -65,15 +177,73 @@ pub struct Fee { } ``` -The signer of the wrapper transaction defines the token in which fees must be paid among those available in the token whitelist. At the same time, he also sets the amount which must meet the minimum price per gas unit for that token $GP_{min}$ (also defined in the whitelist). The difference between the minimum and the actual value set by the submitter represents the incentive for the block proposer to prefer the inclusion of this transaction over other ones. - -The block proposer can check the validity of these two parameters while constructing the block. These validity checks are also replicated in `process_proposal` and `mempool_check`. In case a transaction with invalid parameters ended up in a block, the entire block would be rejected (as already explained earlier in this document). The block proposer and the `process_proposal` function should also cache the available funds of every fee-paying address involved in the block: this is because a signer might submit more than one transaction per block and the check on funds should take into consideration the updated value of the unshielded balance. - -Since the whitelist can be changed via governance, transactions could fail these checks in the block where the whitelist change happens. For `mempool_check`, the checks could reject transactions that may become valid in the future or vice-versa: since we can assume a slow rate of change for these parameters and mempool and block space optimizations are a priority, it is up to the clients to track any changes of these parameters and act accordingly. +The signer of the wrapper transaction defines the token in which fees must be +paid among those available in the token whitelist. At the same time, he also +sets the amount which must meet the minimum price per gas unit for that token +$GP_{min}$ (also defined in the whitelist). The difference between the minimum +and the actual value set by the submitter represents the incentive for the block +proposer to prefer the inclusion of this transaction over other ones. + +The block proposer can check the validity of these two parameters while +constructing the block. These validity checks are also replicated in +`process_proposal` and `mempool_check`. In case a transaction with invalid +parameters ended up in a block, the entire block would be rejected (as already +explained earlier in this document). The block proposer and the +`process_proposal` function should also cache the available funds of every +fee-paying address involved in the block: this is because a signer might submit +more than one transaction per block and the check on funds should take into +consideration the updated value of the unshielded balance. + +Since the whitelist can be changed via governance, transactions could fail these +checks in the block where the whitelist change happens. For `mempool_check`, the +checks could reject transactions that may become valid in the future or +vice-versa: since we can assume a slow rate of change for these parameters and +mempool and block space optimizations are a priority, it is up to the clients to +track any changes of these parameters and act accordingly. + +The proposer and the validators must also check the validity of the optional +unshielding transfer attached to the wrapper. More specifically the correctness +implies that: + +1. The unshielding provides just the right amount of funds to pay fees +2. The actual wasm execution runs successfully + +The first condition can be tested statically and requires that: + +1. The `SignedTxData` actually encodes a `Transfer` struct +2. The `shielded` field must be set to `Some` +3. The `source` and `target` addresses both match that of the wrapper signer +4. The `token` match the one specified in the `Fee` struct +5. The `amount`, added to the already available unshielded balance for that + token, is just enough to cover the fees, i.e. the value given by + $Fee.amount * GasLimit$ (to prevent leveraging this transfer for other + purposes) + +If checks 1 to 4 fail, the transaction can be safely discarded, while if the +check fails at point 5 the transaction could be kept in mempool for future usage +(if more funds should become available), until the expiration time of the +transaction is reached. + +Once these checks have been performed the block proposer should run the actual +transfer against the current state of the application to check whether the +transaction is valid or not: if this succeeds the transaction can be included in +the block, otherwise it should be discarded. + +These same checks are done by the validators in `process_proposal`: if any of +them fail, the entire block is rejected. As an optimization, the storage changes +applied by the unshieldings are stored in the write-ahead log so that there's no +need to run these transactions again in `finalize_block`: in case of a block +rejection the WAL is discarded without committing the changes to storage. ## Gas accounting -We provide a mapping between all the whitelisted transactions and VPs to their cost in gas units: more specifically, the cost of a tx/VP is given by the run time cost of its wasm code. As the cost is constant, it is guaranteed that the same transaction will always require the same amount of gas: since the price per gas unit is controlled via governance, though, the price for the same transaction may vary in time. A transaction is also charged with the gas required by the validity predicates that it triggers. +We provide a mapping between all the whitelisted transactions and VPs to their +cost in gas units: more specifically, the cost of a tx/VP is given by the run +time cost of its wasm code. As the cost is constant, it is guaranteed that the +same transaction will always require the same amount of gas: since the price per +gas unit is controlled via governance, though, the price for the same +transaction may vary in time. A transaction is also charged with the gas +required by the validity predicates that it triggers. Gas accounting is about preventing a transaction from exceeding two gas limits: @@ -82,52 +252,105 @@ Gas accounting is about preventing a transaction from exceeding two gas limits: ### Wrapper GasLimit -The protocol injects a gas counter in each transaction and VP to be executed which allows monitoring of the exact amount of gas utilized. To do so, the gas meter simply checks the hash of the transaction or VP against the table in storage to determine which one it is and, from there, derives the amount of gas required. +The protocol injects a gas counter in each transaction and VP to be executed +which allows monitoring of the exact amount of gas utilized. To do so, the gas +meter simply checks the hash of the transaction or VP against the table in +storage to determine which one it is and, from there, derives the amount of gas +required. -To perform the check we need the limit which was declared by the corresponding wrapper transaction: this limit should be saved in storage together with the queue of encrypted transactions for easy access. +To perform the check we need the limit which was declared by the corresponding +wrapper transaction: this limit should be saved in storage together with the +queue of encrypted transactions for easy access. -Since the hash can be retrieved as soon as the transaction gets decrypted, we can immediately check whether the `GasLimit` set in the corresponding wrapper is enough to cover this amount. This check, though, is weak because we also need to keep in account the gas required for the involved VPs which is hard to determine ahead of time: this is just an optimization to short-circuit the execution of transactions whose gas limit is not enough to cover for even the tx itself. +Since the hash can be retrieved as soon as the transaction gets decrypted, we +can immediately check whether the `GasLimit` set in the corresponding wrapper is +enough to cover this amount. This check, though, is weak because we also need to +keep in account the gas required for the involved VPs which is hard to determine +ahead of time: this is just an optimization to short-circuit the execution of +transactions whose gas limit is not enough to cover for even the tx itself. -When executing the VPs in parallel the procedure is the same and, again, since we know ahead of time the gas required by each VP we can immediately terminate the execution if it overshoots the limit. +When executing the VPs in parallel the procedure is the same and, again, since +we know ahead of time the gas required by each VP we can immediately terminate +the execution if it overshoots the limit. -In any case, if the gas limit is exceeded, the transaction is considered invalid and all the modifications applied to the WAL get discarded. This doesn't affect the other transactions which can be executed normally (see the following section). +In any case, if the gas limit is exceeded, the transaction is considered invalid +and all the modifications applied to the WAL get discarded. This doesn't affect +the other transactions which can be executed normally (see the following +section). ### Block GasLimit This constraint is given by the following two: -- The compliance of each inner transaction with the `WrapperTx` gas limit explained in the previous section -- The compliance of the cumulative wrapper transactions' `GasLimit` with the maximum gas allowed for a block - -Tendermint doesn't provide more than the `BlockSize.MaxGas` parameter, leaving the validation step to the application (see [tendermint spec](https://github.com/tendermint/tendermint/blob/29e5fbcc648510e4763bd0af0b461aed92c21f30/spec/core/data_structures.md#consensusparams) and [issue](https://github.com/tendermint/tendermint/issues/2310)): therefore, instead of using the Tendermint provided param, Namada introduces a `MaxBlockGas` protocol parameter. -This limit is checked during block validation, in `process_proposal`: if the block exceeds the maximum amount of gas allowed, the validators will reject it. - -Note that block gas limit validation should always occur against the `GasLimit` declared in the wrappers, not the real gas used by the inner transactions. If this was the case, in fact, a malicious proposer could craft a block exceeding the gas limit with the hope that some transaction may use less gas than declared: but if this doesn't happen, then the last transactions of the block will be rejected because they would exceed the block gas limit even though they were charged with fees in the previous block, effectively suffering economic damage. In this sense, since the wrapper tx gas limit imposes an economic constraint, it is the reference point for all the gas limit checks. - -Given that the block allocates a certain gas for each transaction and that transactions are prevented from going out of gas, it derives that the execution of each transaction is isolated from all the other ones in terms of gas, which explains the last statement of the previous section. +- The compliance of each inner transaction with the `WrapperTx` gas limit + explained in the previous section +- The compliance of the cumulative wrapper transactions' `GasLimit` with the + maximum gas allowed for a block + +Tendermint doesn't provide more than the `BlockSize.MaxGas` parameter, leaving +the validation step to the application (see +[tendermint spec](https://github.com/tendermint/tendermint/blob/29e5fbcc648510e4763bd0af0b461aed92c21f30/spec/core/data_structures.md#consensusparams) +and [issue](https://github.com/tendermint/tendermint/issues/2310)): therefore, +instead of using the Tendermint provided param, Namada introduces a +`MaxBlockGas` protocol parameter. This limit is checked during block validation, +in `process_proposal`: if the block exceeds the maximum amount of gas allowed, +the validators will reject it. + +Note that block gas limit validation should always occur against the `GasLimit` +declared in the wrappers, not the real gas used by the inner transactions. If +this was the case, in fact, a malicious proposer could craft a block exceeding +the gas limit with the hope that some transactions may use less gas than +declared: but if this doesn't happen, then the last transactions of the block +will be rejected because they would exceed the block gas limit even though they +were charged fees in the previous block, effectively suffering economic damage. +In this sense, since the wrapper tx gas limit imposes an economic constraint, it +is the reference point for all the gas limit checks. + +Given that the block allocates a certain gas for each transaction and that +transactions are prevented from going out of gas, it derives that the execution +of each transaction is isolated from all the other ones in terms of gas, which +explains the last statement of the previous section. ## Checks This section summarizes the checks performed in protocol. -|Method|Checks| -|---|---| -|`CheckTx` and `ProcessProposal`|
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
| -|`ProcessProposal`|
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| -|`FinalizeBlock`|
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| +| Method | Checks | If check fails | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • `SignedTxData` must deserialize to `Transfer`
    • `source` and `target` must match the wrapper signer
    • `token` must match the `Fee` one
| Reject the block | +| `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | +| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | ## Alternatives considered -A drawback of the proposed implementation is that fee payment can only occur from an unshielded balance. This restricts the sources from which a user can gather the funds necessary for the transaction and may also cause a locked-out problem in which a user finds himself with no more unshielded funds, making it impossible for him to operate on the chain. In this case, the user could always reach out to another user to sign wrapper txs for him (ideally to unshield some tokens). +### Inter-chain fee payment + +One may want to pay fees for a `WrapperTx` on Namada with some funds kept on a +different chain that can communicate with Namada, so either Ethereum or an +IBC-compatible chain. + +This solution, though, has the following drawbacks: + +- Require an internal address (with the corresponding VP) as a target of the + payment (cannot pay to the block proposer directly) +- Since the payment must be initiated from another chain it must happen at least + one block ahead of the wrapper transaction for which it's paying the fee. This + means that the fee payment effectively happens in advance and we would need a + mechanism to map a payment to a specific wrapper transaction +- The payer would be an address outside of Namada which could be a problem in + terms of accountability -An alternative solution could be to allow the signer of a wrapper to perform a transfer transaction to pay fees. To do so we would need the `WrapperTx` struct to hold a signed `Transfer` struct in plaintext carrying all the information regarding the transfer. The transaction itself can be crafted in protocol by validators to reduce the burden of the messages on the network and to prevent users from including arbitrary transactions. Since the block proposer is not known ahead of time, we would also need to implement a new internal address with the relative VP to which users should pay the fees. The block proposer could then redeem the tokens from there. This mechanism could function in two ways: +Moreover, this technique is already feasible: it is sufficient to move funds +from the external chain to an address on the Namada chain which would require +the same amount of operations and the same costs. -1. The internal VP prevents any movement of funds from the internal address and the withdraws happen via protocol -2. The internal VP has a way to retrieve the current block proposer and allows only him to withdraw. The block proposer then inserts a last transaction in the block (without going out of gas or size) to redeem all of the tokens in the internal address balance +So, at the cost of increased complexity of the Namada logic, this type of +payment doesn't actually introduce any new feature. -Unfortunately, at the benefit of a more generalized fee payment mechanism, this solution adds the following cons: +### Shielded fee payment -- The need for an additional internal VP -- Overhead given by the transfer execution which could become a possible DOS vector in case a lot of transfers failed -- Transfer execution would technically require gas making the problem recursive -- Checking the funds in advance would be harder \ No newline at end of file +Shielded fee payment should not be supported since that would make it impossible +for validator nodes to check the correctness of the payment: they could only +check that the transaction run without errors but would not be able to determine +the exact amount paid (which must match the `GasLimit`) and involve a +whitelisted token. From 6d243e8735fd08e2a414b204e0269fe060e56ae1 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 27 Dec 2022 14:55:43 +0100 Subject: [PATCH 04/17] Refactors sections of fee specs --- .../specs/src/economics/fee-system.md | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 7e7beb897b..812eee974a 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -108,38 +108,8 @@ transaction for the state of the application (importance on the lifetime parameter of the tx here). If enough funds are available, these are deducted from the unshielded storage -balances of the fee payers and directed to the balance of the block proposer. To -prevent a possible locked-out problem in which a user doesn't have enough funds -to pay fees (preventing any sort of operation on the chain), Namada allows the -signer of the wrapper transaction to unshield some funds on the go to cover the -cost of the fee. To support this mechanism the `WrapperTx` struct must be -extended as follows: - -```rust -pub struct WrapperTx { - /// The fee to be paid for including the tx - pub fee: Fee, - /// Used to determine an implicit account of the fee payer - pub pk: common::PublicKey, - /// Max amount of gas that can be used when executing the inner tx - pub gas_limit: GasLimit, - /// The optional unshielding data for fee payment - pub unshield: Option, - /// the encrypted payload - pub inner_tx: EncryptedTx, - /// sha-2 hash of the inner transaction acting as a commitment - /// the contents of the encrypted payload - pub tx_hash: Hash, -} -``` - -The new `unshield` field carries an optional `SignedTxData` struct encoding for -an unshielding `Transfer`. The tx itself is crafted in protocol by the -validators to reduce the burden of the messages on the network and to prevent -users from including arbitrary transactions. This unshielding operation is -exempted from paying fees and doesn't charge gas. - -If the balance is not enough to cover fees, then the proposed block is +balances of the fee payers and directed to the balance of the block proposer. If +instead the balance is not enough to cover fees, then the proposed block is considered invalid and rejected, initiating a new Tendermint round: this logic implies that the strategy of placing wrapper transactions before any decrypted tx in the block will be reinforced. @@ -201,6 +171,38 @@ vice-versa: since we can assume a slow rate of change for these parameters and mempool and block space optimizations are a priority, it is up to the clients to track any changes of these parameters and act accordingly. +### Unshielding + +To prevent a possible locked-out problem in which a user doesn't have enough +funds to pay fees (preventing any sort of operation on the chain), Namada allows +the signer of the wrapper transaction to unshield some funds on the go to cover +the cost of the fee. To support this mechanism the `WrapperTx` struct must be +extended as follows: + +```rust +pub struct WrapperTx { + /// The fee to be paid for including the tx + pub fee: Fee, + /// Used to determine an implicit account of the fee payer + pub pk: common::PublicKey, + /// Max amount of gas that can be used when executing the inner tx + pub gas_limit: GasLimit, + /// The optional unshielding data for fee payment + pub unshield: Option, + /// the encrypted payload + pub inner_tx: EncryptedTx, + /// sha-2 hash of the inner transaction acting as a commitment + /// the contents of the encrypted payload + pub tx_hash: Hash, +} +``` + +The new `unshield` field carries an optional `SignedTxData` struct encoding for +an unshielding `Transfer`. The tx itself is crafted in protocol by the +validators to reduce the burden of the messages on the network and to prevent +users from including arbitrary transactions. This unshielding operation is +exempted from paying fees and doesn't charge gas. + The proposer and the validators must also check the validity of the optional unshielding transfer attached to the wrapper. More specifically the correctness implies that: From 562183e46d4710b6cf3c54e4eac033b57e8d23b1 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 28 Dec 2022 13:27:58 +0100 Subject: [PATCH 05/17] Enforces tx type order in fee specs --- .../specs/src/economics/fee-system.md | 192 +++++++++--------- 1 file changed, 100 insertions(+), 92 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 812eee974a..e7ada62630 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -36,16 +36,15 @@ transaction itself. Since fees have a purpose in allocating scarce block resources (space and gas limit) they have to be paid upfront, as soon as the transaction is deemed valid and accepted into a block (refer to -[replay protection](../base-ledger/replay-protection.md) specs for more details +[replay protection specs](../base-ledger/replay-protection.md) for more details on transactions' validity). Moreover, for the same reasons, the fee payer will pay for the entire `GasLimit` allocated and not the actual gas consumed for the transaction: this will incentivize fee payers to stick to a reasonable gas limit for their transactions allowing for the inclusion of more transactions into a block. Since the gas used by a transaction leaks a bit of information about the -transaction itself: a submitter may want to obfuscate this value a bit by -increasing the gas limit of the wrapper transaction but they will be charged for -this (refer to section 2.1.3 of the Ferveo -[documentation](https://eprint.iacr.org/2022/898.pdf)). +transaction itself, a submitter may want to obfuscate this value a bit by +increasing the gas limit of the wrapper transaction (refer to section 2.1.3 of +the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). Fees are not distributed among the validators who actively participate in the block validation process. This is because a tx submitter could be side-paying @@ -55,23 +54,23 @@ proposer rotation policy of Tendermint. By requesting an upfront payment, fees also serve as prevention against DOS attacks since the signer needs to pay for all the submitted transactions. More -specifically, to serve as a denial-of-service and spam prevention mechanism, fee -payment needs to enforce: +specifically, to serve as a denial-of-service and spam prevention mechanism, the +fee system needs to enforce: 1. **Succesful** payment at block inclusion time (implying the ability to check the good outcome at block creation time) 2. Minimal payment overhead in terms of computation/memory requirements (otherwise fee payment itself could be exploited as a DOS vector) -Given that transactions are executed in the same order they appear in the block -this will lead to a common behavior across all the block proposers: they'll tend -to place all the wrapper transactions before the decrypted transactions coming -from the previous block. By doing this, they will make sure to prevent inner -transactions from draining the addresses of the funds needed to pay fees. The -proposer will be able to check in advance that fee payers have enough unshielded -funds and, if this is not the case, exclude the transaction from the block and -leave it in the mempool for future inclusion. This behavior ultimately leads to -more resource-optimized blocks. +Given that transactions are executed in the same order they appear in the block, +block proposers will tend to a common behavior: they'll place all the wrapper +transactions before the decrypted transactions coming from the previous block. +By doing this, they will make sure to prevent inner transactions from draining +the addresses of the funds needed to pay fees. The proposers will be able to +check in advance that fee payers have enough unshielded funds and, if this is +not the case, exclude the transaction from the block and leave it in the mempool +for future inclusion. This behavior ultimately leads to more resource-optimized +blocks. As a drawback, this behavior could cause some inner txs coming from the previous block to fail (in case they involve an unshielded transfer) because funds have @@ -82,7 +81,7 @@ issue: 1. Users are responsible for correctly timing/funding their transactions with the help of the wallet -2. We force in protocol that a block should list the wrappers after the +2. Namada forces in protocol that a block should list the wrappers after the decrypted transactions If we follow the second option the block proposers will no more be able to @@ -94,25 +93,37 @@ will not be executed, preserving the correctness of the state machine, but it represents a slight underoptimization of the block and a potential vector for DOS attacks since the invalid wrapper has allocated space and gas in the block without being charged due to the lack of funds. Because of this, we stick to the -first option by not imposing any specific order in procotol. - -Fees are collected via protocol, in the `finalize_block` function, for -`WrapperTx`s which have been processed with success: this is to prevent a -malicious block proposer from including transactions that are known in advance -to be invalid just to collect more fees. Given the two-block execution model of -Namada (wrapper and inner tx) and the need to collect fees for the allocated -resources, nothing can be done in case the inner transaction fails: by that -point, fees have already been collected and no refunds will be issued, meaning -that the inner tx signer is responsible for submitting a semantically valid -transaction for the state of the application (importance on the lifetime -parameter of the tx here). +first option. + +Fees are collected via protocol for `WrapperTx`s which have been processed with +success: this is to prevent a malicious block proposer from including +transactions that are known in advance to be invalid just to collect more fees. +Given the two-block execution model of Namada (wrapper and inner tx) and the +need to collect fees for the allocated resources, nothing can be done in case +the inner transaction fails: by that point, fees have already been collected and +no refunds will be issued, meaning that the inner tx signer is responsible for +submitting a semantically valid transaction for the state of the application +(importance on the lifetime parameter of the tx here). + +Since a signer might submit more than one transaction per block, the +`process_proposal` function needs to cache the updated unshielded balance to +correctly manage fees. As an optimization, instead of using an additional cache, +`process_proposal` uses the already available write-ahead log. Therefore, fee +payment is effectively carried out at block validation time and the new balances +are updated in the WAL itself. To guarantee that the results coming from this +process are correct, Namada imposes that **all the wrapper transactions in a +block are listed before the inner transactions**. This is already the expected +behavior of the block proposers (as stated before) but we need to enforce it in +protocol: if this wasn't the case, an inner transaction placed in between +wrappers could modify a balance involved in fee payment, leading to a +miscalculation of the balance itself which would cause a late rejection of the +block in `finalize_block`. If enough funds are available, these are deducted from the unshielded storage balances of the fee payers and directed to the balance of the block proposer. If -instead the balance is not enough to cover fees, then the proposed block is -considered invalid and rejected, initiating a new Tendermint round: this logic -implies that the strategy of placing wrapper transactions before any decrypted -tx in the block will be reinforced. +instead, the balance is not enough to cover fees, then the proposed block is +considered invalid and rejected, the WAL is discarded and a new Tendermint round +is initiated. To support the in-protocol fee payment mechanism we need to update the `Header` struct to carry the `ProposerAddress`: @@ -122,14 +133,14 @@ struct to carry the `ProposerAddress`: /// relevant for Anoma storage #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub struct Header { - /// Merkle root hash of block - pub hash: Hash, - /// Timestamp associated to block - pub time: DateTimeUtc, - /// Hash of the addresses of the next validator set - pub next_validators_hash: Hash, - /// Address of the block proposer - pub proposer_address: Address + /// Merkle root hash of block + pub hash: Hash, + /// Timestamp associated to block + pub time: DateTimeUtc, + /// Hash of the addresses of the next validator set + pub next_validators_hash: Hash, + /// Address of the block proposer + pub proposer_address: Address } ``` @@ -140,16 +151,16 @@ The `Fee` field of `WrapperTx` is defined as follows: ```rust pub struct Fee { - /// amount of the fee - pub amount: Amount, - /// address of the token - pub token: Address, + /// amount of the fee + pub amount: Amount, + /// address of the token + pub token: Address, } ``` The signer of the wrapper transaction defines the token in which fees must be paid among those available in the token whitelist. At the same time, he also -sets the amount which must meet the minimum price per gas unit for that token +sets the amount which must meet the minimum price per gas unit for that token, $GP_{min}$ (also defined in the whitelist). The difference between the minimum and the actual value set by the submitter represents the incentive for the block proposer to prefer the inclusion of this transaction over other ones. @@ -158,10 +169,8 @@ The block proposer can check the validity of these two parameters while constructing the block. These validity checks are also replicated in `process_proposal` and `mempool_check`. In case a transaction with invalid parameters ended up in a block, the entire block would be rejected (as already -explained earlier in this document). The block proposer and the -`process_proposal` function should also cache the available funds of every -fee-paying address involved in the block: this is because a signer might submit -more than one transaction per block and the check on funds should take into +explained earlier in this document). As mentioned before, a signer might submit +more than one transaction per block and the proposer should take into consideration the updated value of the unshielded balance. Since the whitelist can be changed via governance, transactions could fail these @@ -169,31 +178,30 @@ checks in the block where the whitelist change happens. For `mempool_check`, the checks could reject transactions that may become valid in the future or vice-versa: since we can assume a slow rate of change for these parameters and mempool and block space optimizations are a priority, it is up to the clients to -track any changes of these parameters and act accordingly. +track any changes in these parameters and act accordingly. ### Unshielding To prevent a possible locked-out problem in which a user doesn't have enough funds to pay fees (preventing any sort of operation on the chain), Namada allows the signer of the wrapper transaction to unshield some funds on the go to cover -the cost of the fee. To support this mechanism the `WrapperTx` struct must be -extended as follows: +the cost of the fee. The `WrapperTx` struct must be extended as follows: ```rust pub struct WrapperTx { - /// The fee to be paid for including the tx - pub fee: Fee, - /// Used to determine an implicit account of the fee payer - pub pk: common::PublicKey, - /// Max amount of gas that can be used when executing the inner tx - pub gas_limit: GasLimit, - /// The optional unshielding data for fee payment - pub unshield: Option, - /// the encrypted payload - pub inner_tx: EncryptedTx, - /// sha-2 hash of the inner transaction acting as a commitment - /// the contents of the encrypted payload - pub tx_hash: Hash, + /// The fee to be paid for including the tx + pub fee: Fee, + /// Used to determine an implicit account of the fee payer + pub pk: common::PublicKey, + /// Max amount of gas that can be used when executing the inner tx + pub gas_limit: GasLimit, + /// The optional unshielding data for fee payment + pub unshield: Option, + /// the encrypted payload + pub inner_tx: EncryptedTx, + /// sha-2 hash of the inner transaction acting as a commitment + /// the contents of the encrypted payload + pub tx_hash: Hash, } ``` @@ -222,20 +230,20 @@ The first condition can be tested statically and requires that: purposes) If checks 1 to 4 fail, the transaction can be safely discarded, while if the -check fails at point 5 the transaction could be kept in mempool for future usage -(if more funds should become available), until the expiration time of the -transaction is reached. +check fails at point 5 the transaction could be kept in mempool for future +usage, until the expiration time of the transaction is reached. -Once these checks have been performed the block proposer should run the actual -transfer against the current state of the application to check whether the -transaction is valid or not: if this succeeds the transaction can be included in -the block, otherwise it should be discarded. +Once these controls have been performed, the block proposer should run the +actual transfer against the current state of the application to check whether +the transaction is valid or not: if this succeeds the transaction can be +included in the block, otherwise it should be discarded. These same checks are done by the validators in `process_proposal`: if any of -them fail, the entire block is rejected. As an optimization, the storage changes -applied by the unshieldings are stored in the write-ahead log so that there's no -need to run these transactions again in `finalize_block`: in case of a block -rejection the WAL is discarded without committing the changes to storage. +them fail, the entire block is rejected. Given that fees are paid at this time, +the storage changes applied by the unshieldings must be stored in the +write-ahead log too and the balance key must be searched in the WAL before the +storage: in case of a block rejection the WAL is discarded without committing +the changes to storage. ## Gas accounting @@ -261,15 +269,15 @@ storage to determine which one it is and, from there, derives the amount of gas required. To perform the check we need the limit which was declared by the corresponding -wrapper transaction: this limit should be saved in storage together with the -queue of encrypted transactions for easy access. +wrapper transaction: this can be recovered from the queue of `WrapperTx`s in +storage. Since the hash can be retrieved as soon as the transaction gets decrypted, we can immediately check whether the `GasLimit` set in the corresponding wrapper is enough to cover this amount. This check, though, is weak because we also need to keep in account the gas required for the involved VPs which is hard to determine ahead of time: this is just an optimization to short-circuit the execution of -transactions whose gas limit is not enough to cover for even the tx itself. +transactions whose gas limit is not enough to cover even the tx itself. When executing the VPs in parallel the procedure is the same and, again, since we know ahead of time the gas required by each VP we can immediately terminate @@ -292,8 +300,8 @@ This constraint is given by the following two: Tendermint doesn't provide more than the `BlockSize.MaxGas` parameter, leaving the validation step to the application (see [tendermint spec](https://github.com/tendermint/tendermint/blob/29e5fbcc648510e4763bd0af0b461aed92c21f30/spec/core/data_structures.md#consensusparams) -and [issue](https://github.com/tendermint/tendermint/issues/2310)): therefore, -instead of using the Tendermint provided param, Namada introduces a +and [this issue](https://github.com/tendermint/tendermint/issues/2310)): +therefore, instead of using the Tendermint provided param, Namada introduces a `MaxBlockGas` protocol parameter. This limit is checked during block validation, in `process_proposal`: if the block exceeds the maximum amount of gas allowed, the validators will reject it. @@ -302,11 +310,11 @@ Note that block gas limit validation should always occur against the `GasLimit` declared in the wrappers, not the real gas used by the inner transactions. If this was the case, in fact, a malicious proposer could craft a block exceeding the gas limit with the hope that some transactions may use less gas than -declared: but if this doesn't happen, then the last transactions of the block -will be rejected because they would exceed the block gas limit even though they -were charged fees in the previous block, effectively suffering economic damage. -In this sense, since the wrapper tx gas limit imposes an economic constraint, it -is the reference point for all the gas limit checks. +declared: if this doesn't happen, the last transactions of the block will be +rejected because they would exceed the block gas limit even though they were +charged fees in the previous block, effectively suffering economic damage. In +this sense, since the wrapper tx gas limit imposes an economic constraint, it is +the reference point for all the gas limit checks. Given that the block allocates a certain gas for each transaction and that transactions are prevented from going out of gas, it derives that the execution @@ -320,7 +328,7 @@ This section summarizes the checks performed in protocol. | Method | Checks | If check fails | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • `SignedTxData` must deserialize to `Transfer`
    • `source` and `target` must match the wrapper signer
    • `token` must match the `Fee` one
| Reject the block | -| `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | +| `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Wrapper transactions are listed before decrypted transactions
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | | `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | ## Alternatives considered @@ -343,8 +351,8 @@ This solution, though, has the following drawbacks: terms of accountability Moreover, this technique is already feasible: it is sufficient to move funds -from the external chain to an address on the Namada chain which would require -the same amount of operations and the same costs. +from the external chain to an address on the Namada chain which requires the +same amount of operations and the same costs. So, at the cost of increased complexity of the Namada logic, this type of payment doesn't actually introduce any new feature. @@ -354,5 +362,5 @@ payment doesn't actually introduce any new feature. Shielded fee payment should not be supported since that would make it impossible for validator nodes to check the correctness of the payment: they could only check that the transaction run without errors but would not be able to determine -the exact amount paid (which must match the `GasLimit`) and involve a -whitelisted token. +the exact amount paid (which must match the `GasLimit`) and the token involved +(must be a whitelisted one). From 938eacbeaca2886d1807890855e897300fb4b1a3 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 29 Dec 2022 11:02:23 +0100 Subject: [PATCH 06/17] Fixes unshielding in fee specs --- .../specs/src/economics/fee-system.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index e7ada62630..ce33dd04b8 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -195,8 +195,8 @@ pub struct WrapperTx { pub pk: common::PublicKey, /// Max amount of gas that can be used when executing the inner tx pub gas_limit: GasLimit, - /// The optional unshielding data for fee payment - pub unshield: Option, + /// The optional unshielding tx for fee payment + pub unshield: Option, /// the encrypted payload pub inner_tx: EncryptedTx, /// sha-2 hash of the inner transaction acting as a commitment @@ -205,10 +205,8 @@ pub struct WrapperTx { } ``` -The new `unshield` field carries an optional `SignedTxData` struct encoding for -an unshielding `Transfer`. The tx itself is crafted in protocol by the -validators to reduce the burden of the messages on the network and to prevent -users from including arbitrary transactions. This unshielding operation is +The new `unshield` field carries an optional tx encoding for +an unshielding `Transfer`. The unshielding operation is exempted from paying fees and doesn't charge gas. The proposer and the validators must also check the validity of the optional @@ -220,17 +218,21 @@ implies that: The first condition can be tested statically and requires that: -1. The `SignedTxData` actually encodes a `Transfer` struct +1. The tx encodes a `Transfer` 2. The `shielded` field must be set to `Some` -3. The `source` and `target` addresses both match that of the wrapper signer -4. The `token` match the one specified in the `Fee` struct -5. The `amount`, added to the already available unshielded balance for that +3. The `source` address must be the masp +4. The `target` address matches that of the wrapper signer +5. The `token` match the one specified in the `Fee` struct +6. The `amount`, added to the already available unshielded balance for that token, is just enough to cover the fees, i.e. the value given by $Fee.amount * GasLimit$ (to prevent leveraging this transfer for other purposes) -If checks 1 to 4 fail, the transaction can be safely discarded, while if the -check fails at point 5 the transaction could be kept in mempool for future +The spending key associated with this operation could be relative to +any address as long as the signature of the transfer itself is valid. + +If checks 1 to 5 fail, the transaction can be safely discarded, while if the +check fails at point 6 the transaction could be kept in mempool for future usage, until the expiration time of the transaction is reached. Once these controls have been performed, the block proposer should run the @@ -327,7 +329,7 @@ This section summarizes the checks performed in protocol. | Method | Checks | If check fails | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • `SignedTxData` must deserialize to `Transfer`
    • `source` and `target` must match the wrapper signer
    • `token` must match the `Fee` one
| Reject the block | +| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • tx data must deserialize to `Transfer`
    • `source` must be the masp
    • `target` must match the wrapper signer
    • `token` must match the `Fee` one
| Reject the block | | `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Wrapper transactions are listed before decrypted transactions
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | | `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | From 1a112df43e0b12abd8489c5911a42095345b4397 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 3 Jan 2023 14:54:19 +0100 Subject: [PATCH 07/17] Adds governance proposals to fee specs --- .../specs/src/economics/fee-system.md | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index ce33dd04b8..393dcc03ba 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -182,10 +182,11 @@ track any changes in these parameters and act accordingly. ### Unshielding -To prevent a possible locked-out problem in which a user doesn't have enough -funds to pay fees (preventing any sort of operation on the chain), Namada allows -the signer of the wrapper transaction to unshield some funds on the go to cover -the cost of the fee. The `WrapperTx` struct must be extended as follows: +To provide improved privay, Namada allows the signer of the wrapper transaction +to unshield some funds on the go to cover the cost of the fee. This also +addresses a possible locked-out problem in which a user doesn't have enough +funds to pay fees (preventing any sort of operation on the chaind). The +`WrapperTx` struct must be extended as follows: ```rust pub struct WrapperTx { @@ -205,9 +206,9 @@ pub struct WrapperTx { } ``` -The new `unshield` field carries an optional tx encoding for -an unshielding `Transfer`. The unshielding operation is -exempted from paying fees and doesn't charge gas. +The new `unshield` field carries an optional tx encoding for an unshielding +`Transfer`. The unshielding operation is exempt from paying fees and doesn't +charge gas. The proposer and the validators must also check the validity of the optional unshielding transfer attached to the wrapper. More specifically the correctness @@ -220,16 +221,16 @@ The first condition can be tested statically and requires that: 1. The tx encodes a `Transfer` 2. The `shielded` field must be set to `Some` -3. The `source` address must be the masp -4. The `target` address matches that of the wrapper signer -5. The `token` match the one specified in the `Fee` struct -6. The `amount`, added to the already available unshielded balance for that +3. The `source` address must be the masp4. The `target` address matches that of + the wrapper signer +4. The `token` match the one specified in the `Fee` struct +5. The `amount`, added to the already available unshielded balance for that token, is just enough to cover the fees, i.e. the value given by $Fee.amount * GasLimit$ (to prevent leveraging this transfer for other purposes) -The spending key associated with this operation could be relative to -any address as long as the signature of the transfer itself is valid. +The spending key associated with this operation could be relative to any address +as long as the signature of the transfer itself is valid. If checks 1 to 5 fail, the transaction can be safely discarded, while if the check fails at point 6 the transaction could be kept in mempool for future @@ -247,6 +248,14 @@ write-ahead log too and the balance key must be searched in the WAL before the storage: in case of a block rejection the WAL is discarded without committing the changes to storage. +### Governance proposals + +Governance [proposals](../base-ledger/governance.md) may carry some wasm code to +be executed in case the proposal passed. This code is embedded into a +`DecryptedTx` directly by the validators at block processing time and is not +inserted into the block itself. These transactions are exempt from fees and +don't charge gas. + ## Gas accounting We provide a mapping between all the whitelisted transactions and VPs to their @@ -327,11 +336,11 @@ explains the last statement of the previous section. This section summarizes the checks performed in protocol. -| Method | Checks | If check fails | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| Method | Checks | If check fails | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • tx data must deserialize to `Transfer`
    • `source` must be the masp
    • `target` must match the wrapper signer
    • `token` must match the `Fee` one
| Reject the block | -| `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Wrapper transactions are listed before decrypted transactions
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | -| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | +| `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Wrapper transactions are listed before decrypted transactions
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | +| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | ## Alternatives considered From bebcbd32e6c6ad42dbc4bdec0f78c7c63afea183 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 9 Jan 2023 16:42:07 +0100 Subject: [PATCH 08/17] Adds protocol transactions to fee specs --- documentation/specs/src/economics/fee-system.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 393dcc03ba..5acbf1c57c 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -256,6 +256,12 @@ be executed in case the proposal passed. This code is embedded into a inserted into the block itself. These transactions are exempt from fees and don't charge gas. +### Protocol transactions + +Protocol transactions can only be correctly crafted by validators and serve a +role in allowing the chain to function properly. Given these, they are not +subject to fees and do not charge gas. + ## Gas accounting We provide a mapping between all the whitelisted transactions and VPs to their From 8aebc3b76e06b755ccff75dd785a5888e49177ab Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Sat, 21 Jan 2023 00:01:41 +0100 Subject: [PATCH 09/17] Fixes wal in fee specs --- .../specs/src/economics/fee-system.md | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 5acbf1c57c..ee2dd2f8e2 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -107,17 +107,14 @@ submitting a semantically valid transaction for the state of the application Since a signer might submit more than one transaction per block, the `process_proposal` function needs to cache the updated unshielded balance to -correctly manage fees. As an optimization, instead of using an additional cache, -`process_proposal` uses the already available write-ahead log. Therefore, fee -payment is effectively carried out at block validation time and the new balances -are updated in the WAL itself. To guarantee that the results coming from this -process are correct, Namada imposes that **all the wrapper transactions in a -block are listed before the inner transactions**. This is already the expected -behavior of the block proposers (as stated before) but we need to enforce it in -protocol: if this wasn't the case, an inner transaction placed in between -wrappers could modify a balance involved in fee payment, leading to a -miscalculation of the balance itself which would cause a late rejection of the -block in `finalize_block`. +correctly manage fees. To guarantee that the results coming from this process +are correct, Namada imposes that **all the wrapper transactions in a block are +listed before the inner transactions**. This is already the expected behavior of +the block proposers (as stated before) but we need to enforce it in protocol: if +this wasn't the case, an inner transaction placed in between wrappers could +modify a balance involved in fee payment, leading to a miscalculation of the +balance itself which would cause a late rejection of the block in +`finalize_block`. If enough funds are available, these are deducted from the unshielded storage balances of the fee payers and directed to the balance of the block proposer. If @@ -242,11 +239,9 @@ the transaction is valid or not: if this succeeds the transaction can be included in the block, otherwise it should be discarded. These same checks are done by the validators in `process_proposal`: if any of -them fail, the entire block is rejected. Given that fees are paid at this time, -the storage changes applied by the unshieldings must be stored in the -write-ahead log too and the balance key must be searched in the WAL before the -storage: in case of a block rejection the WAL is discarded without committing -the changes to storage. +them fail, the entire block is rejected. The balance key must be searched in the +local cache before the storage to ensure a correct computation in case of +transactions involving the same addresses. ### Governance proposals From 3c16ff2590b268542abb042609b5b69e6b2d4387 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 7 Feb 2023 16:23:34 +0100 Subject: [PATCH 10/17] Misc updates to fee specs --- .../specs/src/economics/fee-system.md | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index ee2dd2f8e2..f0e8f42bb2 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -20,9 +20,6 @@ be updated with a standard governance proposal. All fees collected are paid directly to the block proposer (incentive-compatible, so that side payments are no more profitable). -Fees are distributed among the delegators with the mechanism explained in the -[proof-of-stake reward distribution specs](./proof-of-stake/reward-distribution.md). - Fees are only meant for `InnerTx` transactions: `WrapperTx`s carry information about fees for the relative inner tx but are not subject to fees themselves. @@ -49,15 +46,15 @@ the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). Fees are not distributed among the validators who actively participate in the block validation process. This is because a tx submitter could be side-paying the block proposer for tx inclusion which would prevent the correct distribution -of fees among validators. The fair distribution of fees is enforced by the block -proposer rotation policy of Tendermint. +of fees among validators. The fair distribution of fees is enforced by the +stake-proportional block proposer rotation policy of Tendermint. By requesting an upfront payment, fees also serve as prevention against DOS attacks since the signer needs to pay for all the submitted transactions. More specifically, to serve as a denial-of-service and spam prevention mechanism, the fee system needs to enforce: -1. **Succesful** payment at block inclusion time (implying the ability to check +1. **Successful** payment at block inclusion time (implying the ability to check the good outcome at block creation time) 2. Minimal payment overhead in terms of computation/memory requirements (otherwise fee payment itself could be exploited as a DOS vector) @@ -218,7 +215,7 @@ The first condition can be tested statically and requires that: 1. The tx encodes a `Transfer` 2. The `shielded` field must be set to `Some` -3. The `source` address must be the masp4. The `target` address matches that of +3. The `source` address must be the masp. The `target` address matches that of the wrapper signer 4. The `token` match the one specified in the `Fee` struct 5. The `amount`, added to the already available unshielded balance for that @@ -229,14 +226,11 @@ The first condition can be tested statically and requires that: The spending key associated with this operation could be relative to any address as long as the signature of the transfer itself is valid. -If checks 1 to 5 fail, the transaction can be safely discarded, while if the -check fails at point 6 the transaction could be kept in mempool for future -usage, until the expiration time of the transaction is reached. - -Once these controls have been performed, the block proposer should run the -actual transfer against the current state of the application to check whether -the transaction is valid or not: if this succeeds the transaction can be -included in the block, otherwise it should be discarded. +If any of the checks fail, the transaction must be discarded. Once these +controls have been performed, the block proposer should run the actual transfer +against the current state of the application to check whether the transaction is +valid or not: if this succeeds the transaction can be included in the block, +otherwise it should be discarded. These same checks are done by the validators in `process_proposal`: if any of them fail, the entire block is rejected. The balance key must be searched in the From 8a6096dfdb4d38dd55b3bc35d2e1907be354c08b Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 8 Feb 2023 17:52:20 +0100 Subject: [PATCH 11/17] Updates check table in fee specs --- documentation/specs/src/economics/fee-system.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index f0e8f42bb2..5751a3b427 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -331,11 +331,11 @@ explains the last statement of the previous section. This section summarizes the checks performed in protocol. -| Method | Checks | If check fails | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • tx data must deserialize to `Transfer`
    • `source` must be the masp
    • `target` must match the wrapper signer
    • `token` must match the `Fee` one
| Reject the block | -| `ProcessProposal` |
  • If unshielding:
    • `amount` is the minimum required
    • the transfer must run successfully
  • Wrapper transactions are listed before decrypted transactions
  • Paying address has enough funds to cover fee
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | -| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | +| Method | Checks | If check fails | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • tx data must deserialize to `Transfer`
    • `source` must be the masp
    • `target` must match the wrapper signer
    • `token` must match the `Fee` one
    • `amount` is the minimum required
    • the transfer must run successfully
  • Paying address has enough funds to cover fee
| Reject the block | +| `ProcessProposal` |
  • Wrapper transactions are listed before decrypted transactions
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | +| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | ## Alternatives considered From 1dc8cbe41f494a6859df1dc4dff468072f50379d Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 16 Feb 2023 18:56:24 +0100 Subject: [PATCH 12/17] Improves gas accounting in specs --- .../specs/src/economics/fee-system.md | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 5751a3b427..1424a0fa98 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -20,9 +20,6 @@ be updated with a standard governance proposal. All fees collected are paid directly to the block proposer (incentive-compatible, so that side payments are no more profitable). -Fees are only meant for `InnerTx` transactions: `WrapperTx`s carry information -about fees for the relative inner tx but are not subject to fees themselves. - ## Fee payment The `WrapperTx` struct holds all the data necessary for the payment of fees in @@ -253,13 +250,27 @@ subject to fees and do not charge gas. ## Gas accounting -We provide a mapping between all the whitelisted transactions and VPs to their -cost in gas units: more specifically, the cost of a tx/VP is given by the run -time cost of its wasm code. As the cost is constant, it is guaranteed that the -same transaction will always require the same amount of gas: since the price per -gas unit is controlled via governance, though, the price for the same -transaction may vary in time. A transaction is also charged with the gas -required by the validity predicates that it triggers. +Gas must take into account the two scarce resources of a block: gas and space. + +Regarding the space limit, Namada charges, for every `WrapperTx`, a fixed amount +of gas per byte. + +For the gas limit, we provide a mapping between all the whitelisted transactions +and VPs to their cost in gas units: more specifically, the cost of a tx/VP is +given by the run time cost of its wasm code. As the cost is constant, it is +guaranteed that the same transaction will always require the same amount of gas: +since the price per gas unit is controlled via governance, though, the price for +the same transaction may vary in time. A transaction is also charged with the +gas required by the validity predicates that it triggers. + +In addition to these, each inner transaction spends gas for compilation costs +(of both the tx and the associated, non-native, VPs) which are charged even if +the compiled transactions was already available in cache, and ancillaries +operations (like loading non-native VP codes from storage). + +To summarize, the gas for a given wrapper transaction can be computed as: + +$$WrapperGas = TxSize + FixedRuntimeGas + TxCodeSize + MiscOpsGas$$ Gas accounting is about preventing a transaction from exceeding two gas limits: From 154736b6571c13e3cab05542ff7087114d1f5dbd Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 3 Mar 2023 12:07:06 +0100 Subject: [PATCH 13/17] Updates tendermint link in fee specs --- .../specs/src/economics/fee-system.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 1424a0fa98..b48adb0c0e 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -314,14 +314,16 @@ This constraint is given by the following two: - The compliance of the cumulative wrapper transactions' `GasLimit` with the maximum gas allowed for a block -Tendermint doesn't provide more than the `BlockSize.MaxGas` parameter, leaving -the validation step to the application (see -[tendermint spec](https://github.com/tendermint/tendermint/blob/29e5fbcc648510e4763bd0af0b461aed92c21f30/spec/core/data_structures.md#consensusparams) -and [this issue](https://github.com/tendermint/tendermint/issues/2310)): -therefore, instead of using the Tendermint provided param, Namada introduces a -`MaxBlockGas` protocol parameter. This limit is checked during block validation, -in `process_proposal`: if the block exceeds the maximum amount of gas allowed, -the validators will reject it. +Tendermint provides a `BlockSize.MaxGas` parameter, and applies some optional +validation in mempool if this parameter is initialized. It doesn't instead +perform any check in consensus, leaving this task to the application itself (see +[tendermint app spec](https://github.com/heliaxdev/tendermint/blob/78c705573710d5b41d0213080260f9eeb6b04ca7/spec/abci/apps.md#gas), +[tendermint spec](https://github.com/heliaxdev/tendermint/blob/78c705573710d5b41d0213080260f9eeb6b04ca7/spec/core/data_structures.md#blockparams) +and [this issue](https://github.com/tendermint/tendermint/issues/2310)). +Therefore, instead of using the Tendermint provided param (and its mempool +validation), Namada introduces a `MaxBlockGas` protocol parameter. This limit is +checked during mempool and block validation, in `process_proposal`: if the block +exceeds the maximum amount of gas allowed, the validators will reject it. Note that block gas limit validation should always occur against the `GasLimit` declared in the wrappers, not the real gas used by the inner transactions. If From ff34df1759a197f395df45061d4457d981f8cf3e Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 17 Apr 2023 18:09:23 +0200 Subject: [PATCH 14/17] Improves unshielding tx verification in fee specs --- documentation/specs/src/economics/fee-system.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index b48adb0c0e..6b814c9a39 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -221,7 +221,13 @@ The first condition can be tested statically and requires that: purposes) The spending key associated with this operation could be relative to any address -as long as the signature of the transfer itself is valid. +as long as the signature of the transfer itself is valid. Verifying that the +origin of the transaction is the same as the wrapper's source would be +impossible anyway for two reasons: + +- the transaction is signed by the `masp` internal address, making it impossible + to check against the public key field of the wrapper +- transparent addresses and spending keys are unrelated If any of the checks fail, the transaction must be discarded. Once these controls have been performed, the block proposer should run the actual transfer From f37826767dc3995f77b37e4e195dc95577fa6774 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 28 Apr 2023 19:17:39 +0200 Subject: [PATCH 15/17] Adjusts block proposer address in fee specs --- .../specs/src/economics/fee-system.md | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 6b814c9a39..4c4d73c244 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -116,27 +116,10 @@ instead, the balance is not enough to cover fees, then the proposed block is considered invalid and rejected, the WAL is discarded and a new Tendermint round is initiated. -To support the in-protocol fee payment mechanism we need to update the `Header` -struct to carry the `ProposerAddress`: - -```rust -/// The data from Tendermint header -/// relevant for Anoma storage -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] -pub struct Header { - /// Merkle root hash of block - pub hash: Hash, - /// Timestamp associated to block - pub time: DateTimeUtc, - /// Hash of the addresses of the next validator set - pub next_validators_hash: Hash, - /// Address of the block proposer - pub proposer_address: Address -} -``` - -From this address, it is then possible to derive the relative Namada address for -the payment. +From the consensus block proposer's address (included in the Tendermint +request), it is possible to derive the relative Namada address for the payment: +should, for any reason, the proposer's address be missing in the incoming +request, fees for that block will be burned. The `Fee` field of `WrapperTx` is defined as follows: @@ -176,7 +159,7 @@ track any changes in these parameters and act accordingly. To provide improved privay, Namada allows the signer of the wrapper transaction to unshield some funds on the go to cover the cost of the fee. This also addresses a possible locked-out problem in which a user doesn't have enough -funds to pay fees (preventing any sort of operation on the chaind). The +funds to pay fees (preventing any sort of operation on the chain). The `WrapperTx` struct must be extended as follows: ```rust From 702dc24b26e8f4d7866908702d8a71edd0bac1e3 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 15 May 2023 11:52:59 +0200 Subject: [PATCH 16/17] DoS checks in fee specs for fee unshielding --- .../specs/src/economics/fee-system.md | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/documentation/specs/src/economics/fee-system.md b/documentation/specs/src/economics/fee-system.md index 4c4d73c244..fc553d960e 100644 --- a/documentation/specs/src/economics/fee-system.md +++ b/documentation/specs/src/economics/fee-system.md @@ -170,8 +170,8 @@ pub struct WrapperTx { pub pk: common::PublicKey, /// Max amount of gas that can be used when executing the inner tx pub gas_limit: GasLimit, - /// The optional unshielding tx for fee payment - pub unshield: Option, + /// The optional unshielding transaction for fee payment + pub unshield: Option, /// the encrypted payload pub inner_tx: EncryptedTx, /// sha-2 hash of the inner transaction acting as a commitment @@ -180,43 +180,47 @@ pub struct WrapperTx { } ``` -The new `unshield` field carries an optional tx encoding for an unshielding -`Transfer`. The unshielding operation is exempt from paying fees and doesn't -charge gas. +The new `unshield` field carries an optional masp transaction struct. The +unshielding operation is exempt from paying fees and doesn't charge gas. To +execute it, validators will construct a valid `Transfer` transaction embedding +the provided unshielding `Transaction`. The proposer and the validators must also check the validity of the optional -unshielding transfer attached to the wrapper. More specifically the correctness +unshielding operation attached to the wrapper. More specifically the correctness implies that: 1. The unshielding provides just the right amount of funds to pay fees 2. The actual wasm execution runs successfully -The first condition can be tested statically and requires that: +The first condition can be enforced during the `Transfer` construction and +requires that: -1. The tx encodes a `Transfer` -2. The `shielded` field must be set to `Some` -3. The `source` address must be the masp. The `target` address matches that of - the wrapper signer -4. The `token` match the one specified in the `Fee` struct -5. The `amount`, added to the already available unshielded balance for that +1. The `shielded` field must be set to `Some` +2. The `source` address must be the masp. The `target` address is that of the + wrapper signer +3. The `token` is the one specified in the `Fee` struct +4. The `amount`, added to the already available unshielded balance for that token, is just enough to cover the fees, i.e. the value given by $Fee.amount * GasLimit$ (to prevent leveraging this transfer for other purposes) +5. The amount of spent and created notes must be within a well defined limit to + prevent DoS The spending key associated with this operation could be relative to any address as long as the signature of the transfer itself is valid. Verifying that the origin of the transaction is the same as the wrapper's source would be impossible anyway for two reasons: -- the transaction is signed by the `masp` internal address, making it impossible - to check against the public key field of the wrapper +- the transaction is crafted in protocol and cannot be signed with the wrapper's + signer private key - transparent addresses and spending keys are unrelated If any of the checks fail, the transaction must be discarded. Once these controls have been performed, the block proposer should run the actual transfer against the current state of the application to check whether the transaction is valid or not: if this succeeds the transaction can be included in the block, -otherwise it should be discarded. +otherwise it should be discarded. During this execution a gas limit is set to +prevent DoS. These same checks are done by the validators in `process_proposal`: if any of them fail, the entire block is rejected. The balance key must be searched in the @@ -245,21 +249,19 @@ Regarding the space limit, Namada charges, for every `WrapperTx`, a fixed amount of gas per byte. For the gas limit, we provide a mapping between all the whitelisted transactions -and VPs to their cost in gas units: more specifically, the cost of a tx/VP is -given by the run time cost of its wasm code. As the cost is constant, it is -guaranteed that the same transaction will always require the same amount of gas: -since the price per gas unit is controlled via governance, though, the price for -the same transaction may vary in time. A transaction is also charged with the -gas required by the validity predicates that it triggers. - -In addition to these, each inner transaction spends gas for compilation costs -(of both the tx and the associated, non-native, VPs) which are charged even if -the compiled transactions was already available in cache, and ancillaries -operations (like loading non-native VP codes from storage). +and non-native VPs to their cost in gas units: more specifically, the cost of a +tx/VP is given by the run time cost of its wasm code. A transaction is also +charged with the gas required by the validity predicates that it triggers. + +In addition to these, each inner transaction spends gas for loading the wasm +module from storage, compilation costs (of both the tx and the associated, +non-native, VPs) which are charged even if the compiled transactions was already +available in cache, ancillaries operations (like loading non-native VP modules +from storage) and the calls to the exposed host functions. To summarize, the gas for a given wrapper transaction can be computed as: -$$WrapperGas = TxSize + FixedRuntimeGas + TxCodeSize + MiscOpsGas$$ +$$\begin{aligned} Gas & = WrapperSize \\ & + TxFixedRuntimeGas \\ & + NonNativeVpsFixedRuntimeCost \\ & + 2 * WasmModuleSize \\ & + MiscOpsGas \\ & + HostFnsCallsGas\end{aligned}$$ Gas accounting is about preventing a transaction from exceeding two gas limits: @@ -333,11 +335,11 @@ explains the last statement of the previous section. This section summarizes the checks performed in protocol. -| Method | Checks | If check fails | -| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding:
    • tx data must deserialize to `Transfer`
    • `source` must be the masp
    • `target` must match the wrapper signer
    • `token` must match the `Fee` one
    • `amount` is the minimum required
    • the transfer must run successfully
  • Paying address has enough funds to cover fee
| Reject the block | -| `ProcessProposal` |
  • Wrapper transactions are listed before decrypted transactions
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | -| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | +| Method | Checks | If check fails | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| `CheckTx` and `ProcessProposal` |
  • Each wrapper tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter
  • Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas
  • If unshielding, the transfer must run successfully
  • Paying address has enough funds to cover fee
| Reject the block | +| `ProcessProposal` |
  • Wrapper transactions are listed before decrypted transactions
  • Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | +| `FinalizeBlock` |
  • For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding wrapper
| Reject the transaction | ## Alternatives considered From 340f12a698c6fd40e7ea22f6541fec870323a9dc Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 13 Dec 2022 12:25:50 +0100 Subject: [PATCH 17/17] changelog: add #889 --- .changelog/unreleased/docs/889-gas-and-fee-specs.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/unreleased/docs/889-gas-and-fee-specs.md diff --git a/.changelog/unreleased/docs/889-gas-and-fee-specs.md b/.changelog/unreleased/docs/889-gas-and-fee-specs.md new file mode 100644 index 0000000000..e6a4021cf8 --- /dev/null +++ b/.changelog/unreleased/docs/889-gas-and-fee-specs.md @@ -0,0 +1 @@ +- Adds specs for gas and fee ([#889](https://github.com/anoma/namada/pull/889)) \ No newline at end of file