Skip to content

Commit

Permalink
edits from review
Browse files Browse the repository at this point in the history
  • Loading branch information
brentstone committed Sep 12, 2024
1 parent 7a38798 commit b4e3e74
Showing 1 changed file with 44 additions and 35 deletions.
79 changes: 44 additions & 35 deletions packages/specs/pages/base-ledger/fee-system.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -132,36 +133,42 @@ 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.

### 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.
Expand All @@ -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}$$

<Callout type="info">
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.
</Callout>

Gas accounting is about preventing a transaction from exceeding two gas limits:
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -212,6 +221,6 @@ This section summarizes the checks performed in protocol.
| Method | Checks | If check fails |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `CheckTx` and `ProcessProposal` | <ul><li> - Each tx `GasLimit` doesn't surpass `MaxBlockGas` protocol parameter</li><li> - Fees are paid with a whitelisted token and meet the minimum amount required of fee per unit of gas</li><li> - If MASP fee payment, the transfer must run successfully<li> - Paying address has enough funds to cover fee</li></li></ul> | Reject the block |
| `ProcessProposal` | <ul><li> - Cumulated `GasLimit` isn't greater than the `MaxBlockGas` parameter </li></ul> | Reject the block |
| `ProcessProposal` | <ul><li> - Cumulative `GasLimit` isn't greater than the `MaxBlockGas` parameter </li></ul> | Reject the block |
| `FinalizeBlock` | <ul><li> - For every tx, gas used isn't greater than the `GasLimit` allocated in the corresponding header </li></ul> | Reject the transaction and discard its modification to the state |

0 comments on commit b4e3e74

Please sign in to comment.