One challenge of implementing Monomer is creating a standard for how gas is metered and handled. By metered we mean both tracked and calculated. By handled we mean how fees are checked and ultimately deducted.

On the Monomer side, the problem we want to solve is two-fold:

  1. Exempting the first transaction of every block from gas fees (the first tx in each block are Layer-1 deposits).
  2. Translating the gas limit from the Engine API’s PayloadAttributes to the Cosmos SDK.

To tackle this problem, we need to take a step back and understand how gas is handled in the Cosmos SDK; first at a high-level, and then at a more granular implementation-level.

Overview

In the Cosmos SDK, gas is a simple alias for uint64, and is managed by the GasMeter interface. The SDK makes use of two different gas meters, the main gas meter and the block gas meter, both of which are generally held in the application ctx.

Gas Metering

The Cosmos SDK employs the following calculation for computing the fee that the end-user (the account that initiated the transaction) is typically responsible for paying:

fee = gas * gas-prices

The SDK tracks gas consumption automatically whenever reads or writes are performed on the store. But it can also be tracked manually by the module developer when executing expensive computations.

The Main Gas Meter

ctx.GasMeter() is the main gas meter of the application. It’s initialized in FinalizeBlock via setFinalizeBlockState, and then tracks gas consumption during execution sequences that lead to state-transitions. At the beginning of each tx execution, the main gas meter must be set to 0 in the AnteHandler, so that it can track gas consumption per-transaction.

Gas consumption can be done manually by the module developer in the BeginBlocker, EndBlocker, or Msg service, and is generally done with the following pattern:

ctx.GasMeter().ConsumeGas(amount, "description")

The Block Gas Meter

ctx.BlockGasMeter() is the gas meter used to track gas consumption across a block. It is initialized in internalFinalizeBlock, and gas is consumed from it in runTx. It is incorporated into the ctx for transaction execution, and modules within the SDK can consume block gas at any point during their execution by utilizing the ctx.BlockGasMeter().ConsumeGas() pattern.

Tying the Two Together

To summarize, the difference between the main gas meter and the block gas meter is their respective scope: The main gas meter operates on a per-transaction level, and is reset to 0 at the beginning of the AnteHandler chain; whereas the block gas meter operates on a per-block level, and is reset to 0 at the beginning of each block. Once a transaction has finished processing, ctx.GasMeter().GasConsumedToLimit() is added to the block gas meter such that ctx.BlockGasMeter() reflects the total gas consumed by all transactions in the block.

Relevant Issues:

Gas Handling

AnteHandler

The AnteHandler performs basic validity checks on a transaction, such that it could be thrown out of the mempool. They are called on both CheckTx and DeliverTx, and check things like:

  • The feepayer address exists and has enough funds to pay the gas fee
  • The transaction signature is valid and the signer has the correct authority

During CheckTx, the AnteHandler verifies that the gas prices provided with the transaction is greater than the local min-gas-prices (a parameter local to each full-node and used during CheckTx to discard transactions that do not provide a minimum amount of fees).

The AnteHandler verifies that the sender of the transaction has enough funds to cover for the fees. When the end-user generates a transactions, they must indicate 2 of the 3 following parameters (the third one being implicit): fees, gas, and gas-prices. This signals how much they are willing to pay for nodes to execute their transaction. The provided gas value is stored in a parameter called GasWanted for later use.

Implementation

At a high-level, the AnteHandler can be thought of as a chain of middleware, separated into smaller functions (decorators), each responsible for performing their own specific checks. Each decorator consumes a ctx sdk.Context and tx sdk.Tx as inputs, and returns (newCtx sdk.Context, err error).

The default AnteHandler (as defined in x/auth/ante) defines the following chain:

graph LR

    subgraph Ante Handler Chain
        direction TB
        A[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/setup.go#l30-target_blanksetupcontextdecoratora|L30' target='_blank'>SetUpContextDecorator</a>"]] -->
        B[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/ext.go#l46-target_blankrejectextensionoptionsdecoratora|L46' target='_blank'>RejectExtensionOptionsDecorator</a>"]] -->
        C[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/basic.go#l27-target_blankvalidatebasicdecoratora|L27' target='_blank'>ValidateBasicDecorator</a>"]] -->
        D[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/basic.go#l203-target_blanktxtimeoutheightdecoratora|L203' target='_blank'>TxTimeoutHeightDecorator</a>"]] -->
        E[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/memo.go#l55-target_blankvalidatememodecoratora|L55' target='_blank'>ValidateMemoDecorator</a>"]] -->
        F[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/basic.go#l94-target_blankconsumegasfortxsizedecoratora|L94' target='_blank'>ConsumeGasForTxSizeDecorator</a>"]] -->
        G[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/fee.go#l42-target_blankdeductfeedecoratora|L42' target='_blank'>DeductFeeDecorator</a>"]] -->
        H[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/sigverify.go#l61-target_blanksetpubkeydecoratora|L61' target='_blank'>SetPubkeyDecorator</a>"]] -->
        I[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/sigverify.go#l399-target_blankvalidatesigcountdecoratora|L399' target='_blank'>ValidateSigCountDecorator</a>"]] -->
        J[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/sigverify.go#l253-target_blanksigverificationdecoratora|L253' target='_blank'>SigVerificationDecorator</a>"]] -->
        K[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/sigverify.go#l164-target_blanksiggasconsumedecoratora|L164' target='_blank'>SigGasConsumeDecorator</a>"]] -->
        L[["<a href='https://github.com/cosmos/cosmos-sdk/blob/v0.50.6/x/auth/ante/sigverify.go#l361-target_blankincrementsequencedecoratora|L361' target='_blank'>IncrementSequenceDecorator</a>"]]
    end

The following decorators are relevant to us when discussing gas: SetupContextDecorator, ValidateBasicDecorator, ConsumeGasForTxSizeDecorator, and DeductFeeDecorator.

SetupContextDecorator

The first thing the SetupContextDecorator does is require that tx implements the GasTx interface. It then sets the GasMeter in the ctx. This decorator also checks if there is a maximum block gas limit, and rejects any tx that exceeds it. It can do this without executing the transaction because the GasTx interface contains a GetGas() method, which just returns tx.AuthInfo.Fee.GasLimit. If the transaction’s gas limit exceeds the block’s gas limit, the decorator returns sdkerrors.ErrInvalidGasLimit.

Finally, it wraps the next decorator with a defer clause to recover from any downstream OutOfGas panics in the AnteHandler chain, returning an error with information on gas provided and gas used.

ValidateBasicDecorator

The ValidateBasicDecorator performs basic — stateless — validity checks on a transaction. The checks it performs relevant to our discussion pertain to the fees in tx.AuthInfo.

  1. Checks that fee != nil and that fee is non-negative.
  2. Ensures fee.GasLimit MaxGasWanted.
  3. Ensures the fee.Payer address is set and that the address exists.

ConsumeGasForTxSizeDecorator

The ConsumeGasForTxSizeDecorator consumes application gas (from the main gas meter) proportional to the size of tx before calling the next decorator. The exact call looks like this:

params := cgts.ak.GetParams(ctx)
 
gasService := cgts.ak.GetEnvironment().GasService
if err := gasService.GasMeter(ctx).Consume(
    params.TxSizeCostPerByte*storetypes.Gas(len(tx.Bytes())),
    "txSize"
); err != nil {
    return err
}

Question

Is transaction size the only meter that the main gas meter incorporates? If a transaction has a relatively small size, but runs for a while (or even hits an infinite loop?), shouldn’t that deduct much more gas than a larger transaction with a short runtime?

Transaction size is not the only factor that determines how much gas is consumed from the main gas meter. The application also tracks reads / writes to the store, which automatically consume a fixed amount of gas (see how WriteCostPerByte is used, for example); likewise verifying signatures consumes a fixed amount of gas as well.

The transaction’s gas limit effectively prevents infinite loops, since execution will halt in the case that it exceeds this limit. Gas consumption is more closely tied to computational complexity and storage (space complexity), rather than execution time (time complexity).

DeductFeeDecorator

The DeductFeeDecorator deducts the fee from the first signer of the tx. If the x/feegrant module is enabled and a fee granter is set, it will deduct fees from the fee granter instead.

The implementaion begins by requiring that tx implements the FeeTx interface. If simulate = false, and ctx.BlockHeight() > 0, the decorator will enforce the gas value to be positive. The decorator then calls into a helper function, checkDeductFee, which uses the AccountKeeper to deduct the fee from the account specified in the payer field of the corresponding tx.AuthInfo. Finally, DeductFees is called, which uses the following pattern for deducting the fee:

err := bankKeeper.SendCoinsFromAccountToModule(
    ctx,
    acc.GetAddress(),
    types.FeeCollectorName,
    fees
)

Wayfinding

Issue: #107 - Gas Logic for System-initiated Deposit Txs

Description: As the first transaction in every block, Monomer gets L1 block attributes from Optimism’s L1 Attributes Deposited Tx. This is a system-initiated transaction that is considered to be part of state-transition processing, and therefore, these transactions are not charged any ETH for gas.

We need to design a way to exempt them from consuming block gas and paying fees on the L2.

On the Comsos SDK side, the AnteHandler is in charge of performing validity checks on transactions — during both CheckTx and DeliverTx — and is also responsible for deducting fees from the sender account.

In order to exempt the L1AttributesDepositedTx from being charged fees, we should either

  1. Design a custom decorator for detecting these transactions and subverting the rest of the ante chain, or
  2. Construct two different ante chains that represent unique paths; one for native L2 txs and user-initiated L1 deposits (essentially, this would be the default x/auth/ante chain), and another for system-initiated L1 deposits.

The meaningful difference here is that (1) might carry security affects (wrt to skipping all other validity checks — still working on formalizing these concerns; ref: #106), whereas (2) supports the possibility of creating custom decorators specifically tuned for system-initiated txs. Since the system-initiated path would essentially be operating at an elevated privilege level, it could reject any user-initiated transactions (similar to how Evmos rejects specific messages in their RejectMessagesDecorator).

Both of these solutions can effectively exempt these transactions from paying a fee in the AnteHandler chain, however, there is still the concern that block gas can be consumed outside of the AnteHandler chain as well. One place I’ve spotted this occurring is in baseapp/baseapp.go.(runTx). I’m not yet sure how to reconcile this, but I am exploring our options.