Skip to content

Unify gas costs source for faster repricing cycles #1599

@LouisTsai-Csie

Description

@LouisTsai-Csie

Description

EIP-7904, proposed for the upcoming fork, introduces a set of eips that will update gas costs with actual computational overhead for each opcode.

However, the current setup relies on two separate data sources for gas models, requiring engineers to update both systems manually each time gas values change.

Current Structure

EELS

Defined in src/ethereum/forks/{fork_name}/vm/gas.py -> Example

For each opcode, charge specific gas amount based on the opcode's gas category:

def add(evm: Evm) -> None:
    x = pop(evm.stack)
    y = pop(evm.stack)

    charge_gas(evm, GAS_VERY_LOW) # ADD is under GAS_VERY_LOW category
    result = x.wrapping_add(y)

    push(evm.stack, result)
    evm.pc += Uint(1)

EEST

Defined in src/ethereum_spec_tests/ethereum_test_forks/forks/forks.py -> Example

In benchmark tests, this is used by hardcoded the value, similar to the EELS implementation here:

loop_cost = (
    gas_costs.G_KECCAK_256  # KECCAK static cost
    + math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD  # KECCAK dynamic
    # cost for CREATE2
    + gas_costs.G_VERY_LOW * 3  # ~MSTOREs+ADDs
    + gas_costs.G_COLD_ACCOUNT_ACCESS  # CALL to self-destructing contract
    + gas_costs.G_SELF_DESTRUCT
    + 63  # ~Gluing opcodes
)

Difference

The difference between the variables definition, their naming or related analysis could be found here.

Potential Issue

We currently classify gas costs into categories such as GAS_VERY_LOW and GAS_BASE. However, EIP-7904 does not merely reduce the gas cost within existing categories, it introduces an entirely new gas cost structure, including categories like BASE_OPCODE_COST and FAST_OPCODE_COST. Each opcode is reorganized under these new categories to better reflect its actual computational cost.

Proposed Solution

Create a base cost configuration file in yaml format, which includes fundamental gas definition and the gas cost model for each opcode:

static_costs:
  GAS_JUMPDEST: 1
  GAS_BASE: 2
  GAS_VERY_LOW: 3
  GAS_LOW: 5
  ...

opcode_costs:
  ADD: GAS_VERY_LOW
  MUL: GAS_LOW
  SUB: GAS_VERY_LOW
  DIV: GAS_LOW
  SDIV: GAS_LOW
  MOD: GAS_LOW
  SMOD: GAS_LOW
  ADDMOD: GAS_MID
  MULMOD: GAS_MID
  EXP: GAS_EXPONENTIATION  # base cost
  SIGNEXTEND: GAS_LOW
  ...
  # System Operations
  CREATE: GAS_CREATE  # base cost, plus init code and memory costs
  CALL: GAS_WARM_ACCESS  # Variable based on call type and account access
  CALLCODE: GAS_WARM_ACCESS  # Variable based on call type and account access
  RETURN: GAS_ZERO  # No gas cost
  CREATE2: GAS_CREATE  # base cost, plus init code and memory costs
  STATICCALL: GAS_WARM_ACCESS  # Variable based on call type and account access
  REVERT: GAS_ZERO  # No gas cost
  SELFDESTRUCT: GAS_SELF_DESTRUCT  # base cost, plus account creation if needed

Note that here it is only the base cost, excluding the cost for memory expansion, warm/code state access and other additional cost.

And we replace the current eels implementation like this:

# Import the gas cost defined in base_cost.yaml
def add(evm: Evm) -> None:
  x = pop(evm.stack)
  y = pop(evm.stack)

  charge_gas(evm, base_cost.ADD) # Updated version
  result = x.wrapping_add(y)

  push(evm.stack, result)
  evm.pc += Uint(1)

Another example:

def mload(evm: Evm) -> None:
    start_position = pop(evm.stack)

    extend_memory = calculate_gas_extend_memory(
        evm.memory, [(start_position, U256(32))]
    )
    charge_gas(evm, base_cost.MLOAD + extend_memory.cost) # only cover base cost here

    evm.memory += b"\x00" * extend_memory.expand_by
    value = U256.from_be_bytes(
        memory_read_bytes(evm.memory, start_position, U256(32))
    )
    push(evm.stack, value)

    evm.pc += Uint(1)

With this definition, we could enable a fork-dependent gas calculator that helps implement this feature, so we could get rid of hardcoded values in benchmark tests.

opcode = Bytecode(Op.ADD(Op.MLOAD(0), 1))
fork.gas_cost_cal(opcode)

Benefit

  1. During gas repricing iterations, other teams can use this as a reference for the latest gas cost model.
  2. Maintain a single unified gas definition across EELS and EEST, so any gas update only requires changes in one place.
  3. While the gas table itself may not be strictly necessary, it could help enable the gas calculator feature described here.

Note

  1. We can support this feature only starting from the Prague fork, as the current benchmark suite begins there.
  2. For the first iteration, we can focus on compute-intensive cases that do not involve complex gas cost models.
  3. This solution can also be limited to the gas-repricing branch, which is primarily used for iterative development.

This is just a draft that documented some initial idea and open to any discussion and feedback!

Metadata

Metadata

Labels

A-spec-specsArea: Specification—The Ethereum specification itself (eg. `src/ethereum/*`)C-featCategory: an improvement or new featureS-needs-discussionStatus: needs discussion

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions