| cip | 53 |
|---|---|
| title | Block gas limit for non-blob transactions |
| description | Bound the total gas that non-blob transactions can declare in a block. |
| author | Rootul Patel (@rootulp) |
| discussions-to | https://forum.celestia.org/t/cip-53-block-gas-limit-for-non-blob-transactions/2324 |
| status | Draft |
| type | Standards Track |
| category | Core |
| created | 2026-10-28 |
| requires | CIP-51 |
Abstract
This CIP adds a block validity rule in the v11 network upgrade: the gas limits declared by the non-blob transactions in a block MUST sum to at most MaxSDKGas = 100,000,000. Non-blob transactions are the transactions that are neither blob transactions nor PayForFibre transactions, the same set that the MaxSDKMessages limit counts. ProcessProposal rejects a block that exceeds the limit, PrepareProposal skips transactions that do not fit, and CheckTx rejects a transaction whose gas limit alone exceeds it. The consensus parameter ConsensusParams.Block.MaxGas stays at -1, because the Cosmos SDK block gas meter it enables has unsafe semantics.
Motivation
Celestia bounds a block by its size and by message counts, but not by how much execution its messages do:
ConsensusParams.Block.MaxGasis -1, so blocks have no gas limit.MaxBytes(32 MiB) andMaxTxSize(8 MiB) bound block and transaction size.MaxPFBMessages(200) andMaxSDKMessages(600) bound message counts. CIP-27 introduced these limits, with 600 PFB and 200 non-PFB messages, to keep block processing at about 0.25 seconds. celestia-app has since changed the values to those listed here.
Message counts bound the fixed cost of each message, such as decoding, ante handler checks, and signature verification. They do not bound what a message does once it executes. One message can iterate over a list it carries, write many state entries, or verify a large payload. That work is metered in gas, and a transaction pays for the gas it declares, but nothing caps the gas a block can contain. The worst-case block is therefore bounded only by how much gas-dense work fits in 32 MiB and by what the attacker will pay.
The celestia-app benchmarks show that gas tracks execution time. In those benchmarks, an 8 MB block of 31,645 MsgSend transactions used about 2.8 billion gas, and delivering its transactions took about 7.5 seconds, roughly 2.7 nanoseconds per gas unit. MaxSDKMessages stopped that block shape by capping the number of messages. A message type whose gas grows with its input avoids a count limit, because it packs the same work into fewer messages.
Per-block limits so far have been added for specific transaction shapes after the shape was found to be expensive, as MaxSDKMessages was. This approach works for issues that are known. It does not work for issues that are not yet found, such as a new module, a dependency upgrade, or an IBC or Hyperlane code path whose gas grows with an attacker-chosen input. A block gas limit bounds all of these at once. Whatever the message, metered work in a block cannot exceed MaxSDKGas. A vulnerability that once allowed seconds or minutes of execution per block is limited to a fraction of a second, which gives the network time to deploy a targeted fix.
Why not set ConsensusParams.Block.MaxGas
CometBFT and the Cosmos SDK already define a block gas limit, ConsensusParams.Block.MaxGas. Celestia cannot use it safely. When MaxGas is set, the SDK enforces it with a block gas meter in FinalizeBlock, and it charges a transaction’s consumed gas to that meter after the transaction executes. A transaction that exceeds the remaining block gas:
- has already done its execution work, so the limit does not bound work done in the block.
- has its message writes reverted, while its fee, sequence increment, and other ante handler writes persist.
- is still included in the block, so a block can contain transactions that fail only because of where they appear in the block.
CIP-11 describes a related problem with unspent gas refunds. Because of these semantics, the celestia-app v10 cosmos-sdk fork prevents updates to MaxGas (CIP-52). This CIP does not change that. It enforces the limit as a block validity rule on declared gas, which validators check before a block is accepted. FinalizeBlock behavior does not change.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Definitions
- A blob transaction is a transaction encoded as a
BlobTx. Its inner transaction contains exactly oneMsgPayForBlobs. - A PayForFibre transaction is a transaction whose only message is a
MsgPayForFibre, as defined in CIP-51. - A non-blob transaction is any other transaction in a block. This is the same set of transactions that the
MaxSDKMessageslimit counts. - The gas limit of a transaction is the
gas_limitfield of itsAuthInfo.Fee.
Block validity
- Starting from app version 11, the sum of the gas limits of all non-blob transactions in a block MUST be less than or equal to
MaxSDKGas. ProcessProposalMUST reject a proposed block that violates this rule. It MUST reject a block containing a non-blob transaction that does not declare a fee, since the ante handler rejects such a transaction anyway.- The sum MUST be computed without integer overflow. For example, an implementation can check
gasLimit <= MaxSDKGas - sumbefore adding a transaction’s gas limit tosum. A transaction’s gas limit is attacker-controlled and can be as large as 2^64 - 1. - Blob transactions and PayForFibre transactions MUST NOT count toward
MaxSDKGas.MaxPFBMessages,MaxPayForFibreMessages, and the square size continue to bound them. ConsensusParams.Block.MaxGasMUST remain -1.
Block construction
PrepareProposalSHOULD skip a non-blob transaction whose gas limit would make the block exceedMaxSDKGas, and SHOULD continue to consider later transactions. A transaction with a smaller gas limit may still fit.- A skipped transaction is not removed from the mempool and can be included in a later block.
Transaction admission
CheckTxSHOULD reject a non-blob, non-PayForFibre transaction whose gas limit alone exceedsMaxSDKGas, since no valid block can include it. This applies to both new transactions and rechecks.- Such a transaction is rejected with a new celestia-app error,
ErrTxExceedsMaxSDKGas.
Parameters
| Parameter | Proposed value | Description | Changeable via Governance |
|---|---|---|---|
appconsts.MaxSDKGas | 100,000,000 | Maximum sum of the gas limits of non-blob transactions in a block. | False |
The following parameter does not change:
| Parameter | Current value | Proposed value | Description | Changeable via Governance |
|---|---|---|---|---|
consensus.block.MaxGas | -1 | -1 | Maximum gas allowed per block (-1 is infinite). | False |
Rationale
Declared gas instead of consumed gas
The limit applies to the gas a transaction declares, not the gas it consumes:
- Declared gas is known before execution. Validators can check the rule in
ProcessProposalwith a decode and a sum, without executing the block. - Declared gas is an upper bound on consumed gas. A transaction that tries to consume more runs out of gas and stops.
- Celestia charges fees on declared gas and does not refund unspent gas (CIP-11 was withdrawn). Reserving block gas costs as much as using it, so an attacker cannot cheaply reserve space with a large gas limit and refund the unused part.
The cost is that over-provisioned gas limits use capacity they do not spend. Clients that estimate gas and add a margin are not affected much. Users who over-provision already pay for the unspent gas.
Excluding blob and PayForFibre transactions
A blob transaction’s gas mostly pays for bytes, not execution. MsgPayForBlobs charges GasPerBlobByte (8) per blob byte, and every transaction pays TxSizeCostPerByte (10) per byte. A full 32 MiB block of blobs declares hundreds of millions of gas but executes quickly, because blob data is not processed by the state machine. Counting blob transactions would require a limit above the gas of a full blob block, which would allow just as much non-blob execution. That would remove most of the protection. Blob transactions contain exactly one MsgPayForBlobs, whose execution is fixed and small, and MaxPFBMessages bounds their count. PayForFibre transactions are bounded by MaxPayForFibreMessages and their signature checks are priced separately (CIP-51).
Excluding these transactions also means that blob throughput, Celestia’s primary use, does not change.
Choice of value
MaxSDKGas = 100,000,000 is chosen to:
- Bound execution time. At the measured 2.7 to 5 nanoseconds per gas unit for typical messages, 100 million gas is about 0.27 to 0.5 seconds of execution. This is the same order as the 0.25 second processing target in CIP-27, and well under the about 2.6 second block time.
- Leave headroom for current usage. The busiest sampled mainnet block declares about 3.3 million non-blob gas, about 3% of this limit (see Mainnet usage). Typical transfers, staking, governance, and IBC transactions declare 100,000 to a few million gas, so many fit in one block.
- Stay compatible with
MaxSDKMessages. A block of 600 transactions that each declare 150,000 gas, a generous limit for a token transfer, uses 90 million gas and fits. - Allow large single transactions. A single transaction can still declare up to 100 million gas. A non-blob transaction of the maximum size, 8 MiB, pays about 84 million gas in
TxSizeCostPerBytealone, so it still fits.
Mainnet usage
Declared gas was sampled from 3,500 Celestia mainnet blocks between heights 3,175,000 and 15,379,220 (queried on 2026-10-28). The sample includes about 1,200 consecutive recent blocks, about 2,000 blocks spread evenly over the preceding month, and about 300 blocks spread across earlier history.
| Metric | p50 | p99 | p99.9 | Max |
|---|---|---|---|---|
| Non-blob gas declared per block | 0 | 495,666 | 1,198,671 | 3,273,835 |
| Non-blob gas used per block | 0 | 330,385 | 952,052 | 2,560,221 |
| Non-blob transactions per block | 0 | 2 | 3 | 5 |
| Largest non-blob transaction gas limit | 0 | 464,511 | 750,711 | 2,885,723 |
| Gas declared per block, all transactions | 606,868 | 13,938,157 | 50,259,702 | 73,057,814 |
The largest non-blob transaction in the sample is an IBC relay transaction (MsgUpdateClient and MsgRecvPacket) that declares about 2.9 million gas. A separate scan of block headers between 2026-09-29 and 2026-10-14 found that the block with the most transactions had 31, and it declared 3.6 million gas in total. The busiest sampled block declares about 3% of MaxSDKGas. Nearly all gas on mainnet comes from blob transactions, which this CIP does not limit.
Alternatives considered
- Set
ConsensusParams.Block.MaxGas. Rejected because of the SDK block gas meter semantics described in Motivation. - Only add per-message limits. Limits for specific message types remain useful, and celestia-app already has several. They only cover issues that are already known. A gas limit is a general backstop that complements them.
- Multidimensional gas, or a separate execution gas meter. Pricing execution separately from bytes would allow one limit across all transactions. This is a larger change to fees and clients, and it can be done later. The limit in this CIP covers the transactions that have the most execution today.
- Make the limit a governance parameter. A hardcoded constant matches the other per-block limits (
MaxPFBMessages,MaxSDKMessages), avoids a new parameter and its validation, and prevents governance from disabling the backstop. A later network upgrade can change it.
Backwards Compatibility
This CIP changes block validity, so it is consensus-breaking and activates with the v11 network upgrade. Validators running v11 reject proposals that exceed MaxSDKGas. Blocks at app version 10 and earlier are processed by the application for that version, which does not have this rule, so syncing earlier heights is not affected.
Effects on users and clients:
- A non-blob transaction with a gas limit above 100,000,000 is rejected by
CheckTxand can never be included in a block. No sampled mainnet transaction declares that much gas. - When the limit is reached,
PrepareProposaldefers remaining non-blob transactions to later blocks, the same wayMaxSDKMessagesdefers them today. - Blob and PayForFibre transactions are not affected.
Test Cases
- A block of non-blob transactions whose gas limits sum to exactly
MaxSDKGasis accepted byProcessProposal. - A block whose non-blob gas limits sum to
MaxSDKGas + 1, either in one transaction or across several, is rejected byProcessProposal. - A block whose non-blob transactions sum to
MaxSDKGasand that also contains blob transactions is accepted. - Given three non-blob transactions that each declare
MaxSDKGas / 2,PrepareProposalincludes the first two and skips the third. - Given a non-blob transaction that declares
MaxSDKGas + 1followed by one that declaresMaxSDKGas / 2,PrepareProposalskips the first and includes the second. - An implementation that applies the recommended
CheckTxadmission check rejects a non-blob transaction that declaresMaxSDKGas + 1withErrTxExceedsMaxSDKGas, for both new transactions and rechecks, and does not return that error for one that declaresMaxSDKGas. - The overflow-safe check rejects a transaction that declares 2^64 - 1 gas when the running sum is non-zero.
Reference Implementation
https://github.com/celestiaorg/celestia-app/pull/8399
The implementation adds the MaxSDKGas constant to pkg/appconsts. It adds the overflow-safe check next to the existing MaxSDKMessages checks in the PrepareProposal filter and ProcessProposal, and adds a CheckTx check next to the other per-transaction admission limits.
Security Considerations
Defense in depth
This CIP turns the cost of metered non-blob execution in a block from unbounded into bounded by MaxSDKGas times the most expensive time per gas unit of any reachable code path. The protection is only as good as gas pricing. Work that is underpriced compared to typical messages can still take longer than the target, by the ratio of its time per gas unit to the typical value. The limit does not replace accurate gas pricing or per-message limits. It caps the damage when those are wrong. Benchmarks for new message types SHOULD measure time per gas unit and compare it to the target that MaxSDKGas assumes.
Proposer behavior
ProcessProposal checks the limit independently, so a byzantine proposer cannot get an over-limit block accepted by honest validators. Computing the rule requires only decoding transactions and reading the fee, which ProcessProposal already does for every transaction. A non-blob transaction that has no fee is rejected, so a proposer cannot avoid the limit with a transaction type that declares no gas.
Integer overflow
Gas limits are attacker-chosen 64-bit values. An implementation that adds them naively could overflow and accept a block far over the limit. The specification requires an overflow-safe comparison, and the test cases cover it.
Block space contention
MaxSDKGas is a scarce per-block resource. Transactions that declare large gas limits at a high gas price can fill it and push other non-blob transactions to later blocks. This is the same tradeoff as for MaxSDKMessages, but priced by gas instead of by message count. Filling the limit costs MaxSDKGas times the gas price for each block, for example 0.4 TIA per block at the default node minimum gas price of 0.004 utia/gas, and the cost rises with fee market competition. Blob transactions do not compete for this resource, so this contention cannot censor blob submissions.
Unchanged FinalizeBlock semantics
Because ConsensusParams.Block.MaxGas stays at -1, the SDK block gas meter stays disabled and FinalizeBlock never fails a transaction for exceeding block gas. Every transaction in an accepted block runs to completion or fails on its own gas meter, as it does today.
Copyright
Copyright and related rights waived via CC0.