Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-7706: Separate gas type for calldata

Create a separate basefee and gaslimit for calldata

Authors Vitalik Buterin (@vbuterin)
Created 2024-05-13
Discussion Link https://ethereum-magicians.org/t/eip-7706-create-a-separate-basefee-and-gaslimit-for-calldata/19998
Requires EIP-1559, EIP-4844

Abstract

Add a new type of gas for transaction calldata. Add a new transaction type that provides max_basefee and priority_fee as a vector, providing values for execution gas, blob gas and calldata gas. Modify the basefee adjustment to use the same mechanism for the three types of gas.

Motivation

A major argument against raising the Ethereum gas limit, making calldata cheaper, or increasing the EIP-4844 blob count before technologies like PeerDAS become available, is that the theoretical maximum size of an Ethereum block is already too large, and we cannot afford to increase it further. However, there is an inefficiency here: the current average size of a block (not including blobs) is ~100 kB, and the theoretical max is 30,000,000 / 16 = 1,875,000 bytes (one could make larger blocks using zero bytes, but in practice zero-byte-heavy blocks would be compressed to less than 1.87 million bytes due to snappy compression). Ideally, we would have a way to bound the maximum, without making calldata more scarce on average.

This EIP does exactly this, by adopting the same technique that was applied for blob data in EIP-4844: we introduce a separate fee market for calldata, with a separate basefee and a separate per-block gas limit. The theoretical max calldata size of a block would be greatly reduced, while basic economic analysis suggests that on average, calldata would become considerably cheaper.

The EIP also introduces a new transaction type which includes the three types of max-basefees and priority fees as a vector, allowing the same code paths to handle all three types of gas. We also make the basefee adjustment, which currently uses separate mechanisms for execution gas (introduced in EIP-1559) and blobs (introduced in EIP-4844), use the same approach for all three types of gas. This simplifies the basefee adjustment rules, and ensures that the stronger mathematical properties of the newer EIP-4844 basefee adjustment algorithm cover all three types of gas.

Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Parameters

  • FORK_BLKNUM = TBD
  • NEW_TX_TYPE = TBD
  • CALLDATA_GAS_PER_TOKEN = 4
  • TOKENS_PER_NONZERO_BYTE = 4
  • CALLDATA_GAS_LIMIT_RATIO = 4
  • LIMIT_TARGET_RATIOS = [2, 2, 4]
  • MIN_BASE_FEE_PER_GAS = 1 # Rename of EIP-4844 MIN_BASE_FEE_PER_BLOB_GAS
  • BASE_FEE_UPDATE_FRACTION = 8 # Roughly matches EIP-4844 parameters

New transaction type

As of FORK_BLOCK_NUMBER, a new EIP-2718 transaction is introduced with TransactionType = TX_TYPE(NEW_TX_TYPE).

The EIP-2718 TransactionPayload for this transaction is

[chain_id, nonce, gas_limit, to, value, data, access_list, blob_versioned_hashes, max_fees_per_gas, priority_fees_per_gas, y_parity, r, s]

We require max_fees_per_gas and priority_fees_per_gas to be length-3 vectors, each of which contain integers from 0 to 2**64-1.

The intrinsic cost of the new transaction is inherited from EIP-4844, except that the calldata gas cost (16 per nonzero byte, 4 per zero byte) is removed.

Block processing and transaction fees

We add the functions get_max_fees and get_priority_fees, to compute these length-3 vectors for previous transaction types:

def get_max_fees(tx: Transaction) -> [int, int, int]:
    if tx.type == NEW_TX_TYPE:
        return tx.max_fees_per_gas
    elif tx.type == BLOB_TX_TYPE:
        return [tx.max_fee_per_gas, tx.max_fee_per_blob_gas, tx.max_fee_per_gas]
    elif is_eip_1559(tx.type):
        return [tx.max_fee_per_gas, 0, tx.max_fee_per_gas]
    else:
        return [tx.gasprice, 0, tx.gasprice]
def get_priority_fees(tx: Transaction) -> [int, int, int]:
    if tx.type == NEW_TX_TYPE:
        return tx.priority_fees_per_gas
    elif tx.type == BLOB_TX_TYPE:
        return [tx.max_priority_fee_per_gas, 0, tx.max_priority_fee_per_gas]
    elif is_eip_1559(tx.type):
        return [tx.max_priority_fee_per_gas, 0, tx.max_priority_fee_per_gas]
    else:
        return [tx.gasprice, 0, tx.gasprice]

We also add some helpers:

def all_less_or_equal(v1: [int, int, int], v2: [int, int, int]) -> bool:
    return all(x <= y for x, y in zip(v1, v2))

def vector_add(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
    return [x+y for x, y in zip(v1, v2)]

def vector_subtract(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
    return [x-y for x, y in zip(v1, v2)]

def vector_subtract_clamp_at_zero(v1: [int, int, int], v2: [int, int, int]) -> [uint, uint, uint]:
    return [x-y if x >= y else 0 for x, y in zip(v1, v2)]

def vector_mul(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
    return [x*y for x, y in zip(v1, v2)]
# Same rules as current calldata pricing, but rephrased (similar language to EIP-7623)
def get_calldata_gas(calldata: bytes) -> int:
    tokens = calldata.count(0) + (len(calldata) - calldata.count(0)) * TOKENS_PER_NONZERO_BYTE
    return tokens * CALLDATA_GAS_PER_TOKEN
def get_gaslimits(tx: Transaction) -> [int, int, int]:
    if tx.type == NEW_TX_TYPE:
        return [tx.gaslimit, len(tx.blob_versioned_hashes) * GAS_PER_BLOB, get_calldata_gas(tx.data)]
    elif tx.type == BLOB_TX_TYPE:
        return [tx.gaslimit, len(tx.blob_versioned_hashes) * GAS_PER_BLOB, get_calldata_gas(tx.data)]
    elif is_eip_1559(tx.type):
        return [tx.gaslimit, 0, get_calldata_gas(tx.data)]
    else:
        return [tx.gaslimit, 0, get_calldata_gas(tx.data)]
def get_fees_per_gas(tx: Transaction, block_basefees: [int, int, int]) -> [int, int, int]:
    max_fees = get_max_fees(tx)
    priority_fees = get_priority_fees(tx)
    output = []
    # Fee sufficiency check, similar to EIP-1559 and 4844
    require(all_less_or_equal(block_basefees, max_fees))
    # Similar logic to EIP-1559 and 4844
    return [
        min(basefee + priority_fee, max_fee)
        for block_basefee, max_fee, priority_fee in zip(block_basefees, max_fees, priority_fees)
    ]
get_block_gaslimits(block: Block) -> [int, int, int]:
    return [block.gaslimit, MAX_BLOB_GAS_PER_BLOCK, block.gaslimit // CALLDATA_GAS_LIMIT_RATIO]

At the start of block processing:

  • We initialize a vector gas_used_so_far to [0, 0, 0]

At the start of processing a transaction:

  • Compute fees_per_gas = get_fees_per_gas(tx, get_block_basefees(block)) and tx_gaslimits = get_gaslimits(tx)
  • Check that all_less_or_equal(vector_add(gas_used_so_far, tx_gaslimits), block.gas_limits)
  • Deduct sum(vector_mul(fees_per_gas, tx_gaslimits)) wei from the tx.origin account

Note that get_block_basefees(block) is not yet defined, we will define it in the section below. The block.gas_limits field is also defined in the section below.

At the end of processing a transaction:

  • Compute tx_gas_consumed as a three item vector, where the first item is the amount of gas actually consumed by the transaction execution, and the second and third match the values in get_gaslimits(tx)
  • Refund sum(vector_mul(fees_per_gas, vector_sub(tx_gaslimits, tx_gas_consumed))) to the tx.origin account (in practice, only the first term will be nonzero for now)
  • Set gas_used_so_far = vector_add(gas_used_so_far, tx_gas_consumed)

At the end of processing a block:

  • Require block.gas_used = gas_used_so_far

The block.gas_used field will be defined in the section below.

Block structure:

We update BlockHeader field, to remove the blob_gas_used, gas_used, base_fee_per_gas, gas_limit and excess_blob_gas fields, and we add new fields, all of the [int, int, int] type: gas_limits, gas_used, excess_gas. The resulting RLP encoding becomes:

rlp([
    parent_hash,
    0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, # ommers hash
    coinbase,
    state_root,
    txs_root,
    receipts_root,
    logs_bloom,
    0, # difficulty
    number,
    timestamp,
    extradata,
    prev_randao,
    0x0000000000000000, # nonce
    withdrawals_root,
    gas_limits,
    gas_used,
    excess_gas
])

We define:

get_block_gas_targets(parent: Header) -> [int, int, int]:
    return [limit // target_ratio for limit, target_ratio in zip(parent.gas_limits, LIMIT_TARGET_RATIOS)]

We calculate the required excess_gas values as follows:

def calc_excess_gas(parent: Header) -> [int, int, int]:
    return vector_subtract_clamp_at_zero(vector_add(parent_excess, parent_used), get_block_gas_targets(parent))

We calculate the required gas_limits as follows:

  • gas_limits[0] must follow the existing adjustment formula
  • gas_limits[1] must equal MAX_BLOB_GAS_PER_BLOCK
  • gas_limits[2] must equal gas_limits[0] // CALLDATA_GAS_LIMIT_RATIO

Now, we define get_block_basefees:

def get_block_basefees(parent: Header) -> [int, int, int]:
    return [
        fake_exponential(
            MIN_BASE_FEE_PER_GAS,
            excess_gas,
            target * BASE_FEE_UPDATE_FRACTION
        )
        for (excess_gas, target) in zip(parent.excess_gas, get_block_gas_targets(parent))
    ]

Rationale

This allows the same logic that is used for handling gas to handle all three types of gas. As a result, it’s arguably a net simplification of protocol gas handling logic, despite the fact that the total number of gas types increases from 2 to 3

Target ratios

The target ratios for execution gas and blobs are set to 2; the target ratio for calldata is set to 4. This greatly decreases the number of scenarios in which calldata actually hits the limit, which mitigates economic impact of the EIP, because analysis of EIP-1559-style fee markets is much simpler in “under-the-limit” conditions than in “at-the-limit” conditions. Additionally, it reduces the risk that applications requiring large calldata will outright stop working.

The current parameters set the target calldata per block to 187,500 bytes, about 2x the current average. Using basic supply-and-demand reasoning, this implies that calldata is likely to become significantly cheaper as a result of this EIP.

Backwards Compatibility

Previous transaction types set the calldata basefee and priority fee to equal each other. The calldata gas costs were intentionally set to be identical to today, and the gas target similar to present-day usage, so that setting the two fees to be equal each other is a reasonable approximation to optimal behavior. In practice, the new transaction type would be superior, so we expect users to switch to it over time. However, the loss suffered by old-style transaction users would not be that high, because priority fees are generally small compared to basefees, and the amount that a user pays is proportional to the basefee.

Security Considerations

Optimal block building behavior becomes more complex as a result of this EIP, particularly under the boundary conditions when blocks are full of one or both types of gas. We argue that the effects of this are not too large, because in practice over 90% of blocks are under-full, and naive “greedy algorithms” can get a close-enough-to-optimal outcome. The centralization risks of proprietary block-building algorithms are thus likely to be much smaller than other existing risks with eg. MEV extraction.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Vitalik Buterin (@vbuterin), "EIP-7706: Separate gas type for calldata [DRAFT]," Ethereum Improvement Proposals, no. 7706, May 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7706.