From ada4fdd364cb5764f8a46f40715a7158f1440731 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Sun, 8 Sep 2024 17:27:22 +0200 Subject: [PATCH 1/4] Updates fee specs --- .../specs/pages/base-ledger/fee-system.mdx | 260 ++++-------------- 1 file changed, 50 insertions(+), 210 deletions(-) diff --git a/packages/specs/pages/base-ledger/fee-system.mdx b/packages/specs/pages/base-ledger/fee-system.mdx index 56c84cf2..8df36afa 100644 --- a/packages/specs/pages/base-ledger/fee-system.mdx +++ b/packages/specs/pages/base-ledger/fee-system.mdx @@ -35,10 +35,7 @@ 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 (refer to section 2.1.3 of -the Ferveo [documentation](https://eprint.iacr.org/2022/898.pdf)). +block. 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 @@ -56,31 +53,27 @@ fee system needs to enforce: 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, -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 protocol executes the fee payment part of a transaction before any of the inner transactions that compose the batch. +By doing this, we 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 -been moved to the block proposer as a fee payment for a `WrapperTx` included in +As a drawback, this behavior could cause some inner txs to fail because funds have +been moved to the block proposer as a fee payment for another transaction included earlier 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. Namada forces in protocol that a block should list the wrappers after the - decrypted transactions +2. Namada forces in protocol that a block should execute transactions right after the fee payment operation 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 +calculate the possibly new 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 @@ -91,44 +84,28 @@ 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). +transactions that are known in advance to be invalid just to collect more fees. Note that in this case we imply the correctness of the transaction's `Header`, i.e. we make sure that this is correct with respect to the constraints that we impose on it, we don't validate anything about the actual inner transactions which could end up failing. 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. 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 +`process_proposal` function needs to cache the updated balances to +correctly manage fees. + +For every transaction in the block, if enough funds are available, these are deducted from the 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, the WAL is discarded and a new Cometbft round -is initiated. +instead, the balance is not enough to cover fees, then the entire proposed block is +considered invalid and rejected and a new Cometbft round is initiated. From the consensus block proposer's address (included in the Cometbft -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. +request), it is 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, + /// Amount of fees paid per gas unit. + pub amount_per_gas_unit: DenominatedAmount, + /// Address of the fee token. + pub token: Address, } ``` @@ -137,15 +114,8 @@ 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). 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. +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`. 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 @@ -154,84 +124,23 @@ 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 in these parameters and act accordingly. -### Unshielding +### MASP fee payment -To provide improved data protection, Namada allows the signer of the wrapper transaction -to unshield some funds on the go to cover the cost of the fee. This also +To provide improved data protection, Namada allows 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 chain). The -`WrapperTx` struct must be extended as follows: +funds to pay fees (preventing any sort of operation on the chain). -```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 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 - /// the contents of the encrypted payload - pub tx_hash: Hash, -} -``` +When the transparent fee payment performed directly from the implicit address of the signer fails, the protocol tries to execute the first transaction of the batch: if this is a **valid MASP transaction** if then retries to perform the same exact transparent fee payment operation. If it is successful, the transaction is accepted, otherwise it gets rejected (possibly the entire block is rejected if we are validating a new block). So, essentially, MASP fee payment consists on allowing the transaction to unshield some tokens to the balance of the fee payer to cover the missing amount. -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 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 enforced during the `Transfer` construction and -requires 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 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. 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 -local cache before the storage to ensure a correct computation in case of -transactions involving the same addresses. +Since this operation comes before the actual payment, it can be exploited as a DOS vector: to prevent this, the protocol enforces a maximum gas limit that can be used for this operation. When the time comes, the protocol picks the gas limit for this operation as the minimum between the gas available to the transaction and the protocol parameter: if the transaction runs out of gas it gets discarded. If instead it gets applied correctly, the gas meter of the transaction accounts for the gas used and the execution proceeds. + +The spending key(s) associated with this operation could be relative to any address +as long as the signature of the transfer itself is valid. ### Governance proposals Governance [proposals](../modules/governance.mdx) 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 +be executed in case the proposal passed. This code is embedded into a new transaction crafted 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. @@ -245,131 +154,62 @@ subject to fees and do not charge gas. 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 +Regarding the space limit, Namada charges, for every transaction, a fixed amount of gas per byte. -For the gas limit, we provide a mapping between all the whitelisted transactions -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. +The cost of a WASM tx/vp is given by the run time cost of it. 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. +available in cache, the calls to the exposed host functions and the sections of native vps that are more computationally complex. + +To summarize, the gas for a given transaction can be computed as: -To summarize, the gas for a given wrapper transaction can be computed as: +$$\begin{aligned} Gas & = TxSize \\ & + TxHeaderValidationFixedCost \\ & + TxRuntimeCost \\ & + NonNativeVpsRuntimeCost \\ & + NativeVpsExpensiveSections \\ & + \\ & + HostFnCallsGas\end{aligned}$$ -$$\begin{aligned} Gas & = WrapperSize \\ & + TxFixedRuntimeGas \\ & + NonNativeVpsFixedRuntimeCost \\ & + 2 * WasmModuleSize \\ & + MiscOpsGas \\ & + HostFnsCallsGas\end{aligned}$$ + +The runtime gas meter instruments wasm modules before their execution to charge gas based on the actual opcodes that get executed. The actual code that end up running though, is an optimized version of the one declared in the WASM module: this leads to an excessive gas being charged for code that runs in the wasm context. The protocol compensates this by adjusting the non-wasm gas costs accordingly by a predefined factor. + Gas accounting is about preventing a transaction from exceeding two gas limits: -1. Its own `GasLimit` (declared in the wrapper transaction) +1. Its own `GasLimit` (declared in the transaction's header) 2. The gas limit of the entire block -### Wrapper GasLimit +### Tx 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 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 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. - -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). +which allows monitoring of the exact amount of gas utilized. + +As soon as the gas limit defined in the transaction's header is exceeded, the transaction is immediately terminated and all the modifications applied to the WAL get discarded. ### Block GasLimit This constraint is given by the following two: -- The compliance of each inner transaction with the `WrapperTx` gas limit +- The compliance of each inner transaction with the tx gas limit explained in the previous section -- The compliance of the cumulative wrapper transactions' `GasLimit` with the +- The compliance of the cumulative transactions' `GasLimit`s with the maximum gas allowed for a block Cometbft 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 [cometbft app spec](https://github.com/informalsystems/cometbft/blob/main/spec/abci/abci%2B%2B_app_requirements.md#gas), -[cometbft spec](https://github.com/informalsystems/cometbft/blob/main/spec/core/data_structures.md#blockparams). +[cometbft spec](https://github.com/informalsystems/cometbft/blob/main/spec/core/data_structures.md#blockparams)). Therefore, instead of using the Cometbft 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 -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: 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 -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 | If check fails | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `CheckTx` and `ProcessProposal` | | Reject the block | -| `ProcessProposal` | | Reject the block | -| `FinalizeBlock` | | Reject the transaction | - -## Alternatives considered - -### 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 - -Moreover, this technique is already feasible: it is sufficient to move funds -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. - -### Shielded fee payment +| `CheckTx` and `ProcessProposal` | | Reject the block | +| `ProcessProposal` | | Reject the block | +| `FinalizeBlock` | | Reject the transaction and discard its modification to the state | -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 the token involved -(must be a whitelisted one). From 956d3bedfa0edb819c33985c647ae69df6f19756 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Sun, 8 Sep 2024 17:36:59 +0200 Subject: [PATCH 2/4] Adds missing `Callout` import --- packages/specs/pages/base-ledger/fee-system.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/specs/pages/base-ledger/fee-system.mdx b/packages/specs/pages/base-ledger/fee-system.mdx index 8df36afa..6e1cea5b 100644 --- a/packages/specs/pages/base-ledger/fee-system.mdx +++ b/packages/specs/pages/base-ledger/fee-system.mdx @@ -1,3 +1,5 @@ +import { Callout } from 'nextra-theme-docs' + # Fee system In order to be accepted by the Namada ledger, transactions must pay fees. From 7a38798dbecc0db57b2cc9edd94f385964afb6e4 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Sun, 8 Sep 2024 17:38:23 +0200 Subject: [PATCH 3/4] Fixes gas formula --- packages/specs/pages/base-ledger/fee-system.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/specs/pages/base-ledger/fee-system.mdx b/packages/specs/pages/base-ledger/fee-system.mdx index 6e1cea5b..fe8e2c5f 100644 --- a/packages/specs/pages/base-ledger/fee-system.mdx +++ b/packages/specs/pages/base-ledger/fee-system.mdx @@ -168,7 +168,7 @@ available in cache, the calls to the exposed host functions and the sections of To summarize, the gas for a given transaction can be computed as: -$$\begin{aligned} Gas & = TxSize \\ & + TxHeaderValidationFixedCost \\ & + TxRuntimeCost \\ & + NonNativeVpsRuntimeCost \\ & + NativeVpsExpensiveSections \\ & + \\ & + HostFnCallsGas\end{aligned}$$ +$$\begin{aligned} Gas & = TxSize \\ & + TxHeaderValidationFixedCost \\ & + TxRuntimeCost \\ & + NonNativeVpsRuntimeCost \\ & + NativeVpsExpensiveSections \\ & + HostFnCallsGas\end{aligned}$$ The runtime gas meter instruments wasm modules before their execution to charge gas based on the actual opcodes that get executed. The actual code that end up running though, is an optimized version of the one declared in the WASM module: this leads to an excessive gas being charged for code that runs in the wasm context. The protocol compensates this by adjusting the non-wasm gas costs accordingly by a predefined factor. From b4e3e740844c55c2e8cfe03f4c562d9faf16f590 Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 12 Sep 2024 12:10:58 +0200 Subject: [PATCH 4/4] edits from review --- .../specs/pages/base-ledger/fee-system.mdx | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/specs/pages/base-ledger/fee-system.mdx b/packages/specs/pages/base-ledger/fee-system.mdx index fe8e2c5f..8a928adc 100644 --- a/packages/specs/pages/base-ledger/fee-system.mdx +++ b/packages/specs/pages/base-ledger/fee-system.mdx @@ -6,17 +6,17 @@ 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. +block producers to add transactions to the blocks that they create and publish. -Namada transaction fees can be paid in any fungible token which is a member of a +Namada transaction fees can be paid in any fungible token that 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 +that transactions must pay in order to be accepted. However, transactions 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 +token identifier and $GP_{min}$ is the minimum (base) price per unit gas that 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 @@ -25,8 +25,8 @@ no more profitable). ## 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 +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 @@ -36,24 +36,24 @@ and accepted into a block (refer to 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 +for their transactions, allowing for the inclusion of more transactions into a block. 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 +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 -stake-proportional block proposer rotation policy of Cometbft. +stake-proportional block proposer rotation policy of CometBFT. -By requesting an upfront payment, fees also serve as prevention against DOS +By requesting an upfront payment, fees also serve as prevention against denial-of-service (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 +specifically, to serve as a DoS and spam prevention mechanism, the fee system needs to enforce: 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) + (otherwise fee payment itself could be exploited as a DoS vector) The protocol executes the fee payment part of a transaction before any of the inner transactions that compose the batch. By doing this, we make sure to prevent inner transactions from draining @@ -75,18 +75,19 @@ issue: 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 balance) and, inevitably, some wrapper +calculate the possible new 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 +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. -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. Note that in this case we imply the correctness of the transaction's `Header`, i.e. we make sure that this is correct with respect to the constraints that we impose on it, we don't validate anything about the actual inner transactions which could end up failing. +Fees are collected via protocol for `WrapperTx`s that 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. +Note that in this case we imply the correctness of the transaction's `Header`, i.e. we make sure that this is correct with respect to the constraints that we impose on it, and we don't validate anything about the actual inner transactions that could end up failing. Since a signer might submit more than one transaction per block, the `process_proposal` function needs to cache the updated balances to @@ -95,9 +96,9 @@ correctly manage fees. For every transaction in the block, if enough funds are available, these are deducted from the 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 entire proposed block is -considered invalid and rejected and a new Cometbft round is initiated. +considered invalid and rejected, and a new CometBFT round is initiated. -From the consensus block proposer's address (included in the Cometbft +From the consensus block proposer's address (included in the CometBFT request), it is possible to derive the relative Namada address for the payment. The `Fee` field of `WrapperTx` is defined as follows: @@ -132,16 +133,22 @@ To provide improved data protection, Namada allows to unshield some funds on the 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 chain). -When the transparent fee payment performed directly from the implicit address of the signer fails, the protocol tries to execute the first transaction of the batch: if this is a **valid MASP transaction** if then retries to perform the same exact transparent fee payment operation. If it is successful, the transaction is accepted, otherwise it gets rejected (possibly the entire block is rejected if we are validating a new block). So, essentially, MASP fee payment consists on allowing the transaction to unshield some tokens to the balance of the fee payer to cover the missing amount. +When the transparent fee payment performed directly from the implicit address of the signer fails, the protocol tries to execute the first transaction of the batch: +if this is a **valid MASP transaction**, it then reattempts to perform the same exact transparent fee payment operation. +If it is successful, the transaction is accepted, otherwise it gets rejected (possibly the entire block is rejected if we are validating a new block). +So, essentially, MASP fee payment involves allowing the transaction to unshield some tokens to the balance of the fee payer to cover the missing amount. -Since this operation comes before the actual payment, it can be exploited as a DOS vector: to prevent this, the protocol enforces a maximum gas limit that can be used for this operation. When the time comes, the protocol picks the gas limit for this operation as the minimum between the gas available to the transaction and the protocol parameter: if the transaction runs out of gas it gets discarded. If instead it gets applied correctly, the gas meter of the transaction accounts for the gas used and the execution proceeds. +Since this operation comes before the actual payment, it can be exploited as a DoS vector. +To prevent this, the protocol enforces a maximum gas limit that can be used for this operation. +When the time comes, the protocol picks the gas limit for this operation as the minimum between the gas available to the transaction and the protocol parameter; +if the transaction runs out of gas, it gets discarded. If instead it gets applied correctly, the gas meter of the transaction accounts for the gas used and the execution proceeds. The spending key(s) associated with this operation could be relative to any address as long as the signature of the transfer itself is valid. ### Governance proposals -Governance [proposals](../modules/governance.mdx) may carry some wasm code to +Governance [proposals](../modules/governance.mdx) may carry some WASM code to be executed in case the proposal passed. This code is embedded into a new transaction crafted 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. @@ -149,19 +156,19 @@ 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 +role in allowing the chain to function properly. Thus, they are not subject to fees and do not charge gas. ## Gas accounting Gas must take into account the two scarce resources of a block: gas and space. -Regarding the space limit, Namada charges, for every transaction, a fixed amount -of gas per byte. +Regarding the space limit, Namada charges a fixed amount +of gas per byte for every transaction. The cost of a WASM tx/vp is given by the run time cost of it. -In addition to these, each inner transaction spends gas for loading the wasm +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, the calls to the exposed host functions and the sections of native vps that are more computationally complex. @@ -171,7 +178,9 @@ To summarize, the gas for a given transaction can be computed as: $$\begin{aligned} Gas & = TxSize \\ & + TxHeaderValidationFixedCost \\ & + TxRuntimeCost \\ & + NonNativeVpsRuntimeCost \\ & + NativeVpsExpensiveSections \\ & + HostFnCallsGas\end{aligned}$$ -The runtime gas meter instruments wasm modules before their execution to charge gas based on the actual opcodes that get executed. The actual code that end up running though, is an optimized version of the one declared in the WASM module: this leads to an excessive gas being charged for code that runs in the wasm context. The protocol compensates this by adjusting the non-wasm gas costs accordingly by a predefined factor. +The runtime gas meter instruments WASM modules before their execution to charge gas based on the actual opcodes that get executed. +The actual code that ends up running though, is an optimized version of the one declared in the WASM module. +This leads to an excessive gas being charged for code that runs in the WASM context. The protocol compensates this by adjusting the non-WASM gas costs accordingly by a predefined factor. Gas accounting is about preventing a transaction from exceeding two gas limits: @@ -181,7 +190,7 @@ Gas accounting is about preventing a transaction from exceeding two gas limits: ### Tx GasLimit -The protocol injects a gas counter in each transaction and VP to be executed +The protocol injects a gas counter in each transaction and VP to be executed, which allows monitoring of the exact amount of gas utilized. As soon as the gas limit defined in the transaction's header is exceeded, the transaction is immediately terminated and all the modifications applied to the WAL get discarded. @@ -195,14 +204,14 @@ This constraint is given by the following two: - The compliance of the cumulative transactions' `GasLimit`s with the maximum gas allowed for a block -Cometbft provides a `BlockSize.MaxGas` parameter, and applies some optional +CometBFT 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 -[cometbft app spec](https://github.com/informalsystems/cometbft/blob/main/spec/abci/abci%2B%2B_app_requirements.md#gas), -[cometbft spec](https://github.com/informalsystems/cometbft/blob/main/spec/core/data_structures.md#blockparams)). -Therefore, instead of using the Cometbft provided param (and its mempool +[CometBFT app spec](https://github.com/informalsystems/cometbft/blob/main/spec/abci/abci%2B%2B_app_requirements.md#gas), +[CometBFT spec](https://github.com/informalsystems/cometbft/blob/main/spec/core/data_structures.md#blockparams)). +Therefore, instead of using the CometBFT 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 +checked during mempool and block validation in `process_proposal`: if the block exceeds the maximum amount of gas allowed, the validators will reject it. ## Checks @@ -212,6 +221,6 @@ This section summarizes the checks performed in protocol. | Method | Checks | If check fails | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `CheckTx` and `ProcessProposal` |
  • - Each 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 MASP fee payment, the transfer must run successfully
  • - Paying address has enough funds to cover fee
| Reject the block | -| `ProcessProposal` |
  • - Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter
| Reject the block | +| `ProcessProposal` |
  • - Cumulative `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 header
| Reject the transaction and discard its modification to the state |