Implement EIP
Patterns for implementing spec changes in src/ethereum/forks/. Run this skill before implementing an EIP or modifying fork code.
Fork Directory Layout
Each fork lives at src/ethereum/forks/<fork_name>/. Explore the latest fork directory for current structure. Key files:
__init__.py— FORK_CRITERIA, fork metadatafork.py— state transition functionsblocks.py— block structure and validationtransactions.py— transaction types and processingstate_tracker.py— fork-specific state trackingvm/instructions/__init__.py— Ops enum +op_implementationdictvm/gas.py— gas constants and calculationsvm/precompiled_contracts/__init__.py— precompile address constantsvm/precompiled_contracts/mapping.py—PRE_COMPILED_CONTRACTSregistry
Import Isolation (enforced by ethereum-spec-lint)
- Within same fork: relative imports (
from . import vm,from .state import ...) - Previous fork only: absolute imports (
from ethereum.cancun import ...) - Shared modules: always OK (
ethereum.crypto,ethereum.utils,ethereum.exceptions) - Future forks: NEVER allowed
- Ancient forks (2+ back): NEVER allowed
- Run
ethereum-spec-lintto verify before committing
Adding a New Opcode
- Add to
Opsenum invm/instructions/__init__.pywith hex value - Implement function in appropriate
vm/instructions/<category>.py— follows pattern: STACK → GAS (charge_gas) → OPERATION → PROGRAM COUNTER. Opcodes that touch state use the staged gas labels — see "Gas Handling" below. - Register in
op_implementationdict invm/instructions/__init__.py - Add gas constant in
vm/gas.pyif needed
Gas Handling
Recent forks meter two gas dimensions: execution gas and state gas (for durable state growth). Key rules:
- Gas constants and calculations go in
vm/gas.py; a frame's mutable gas state lives onEvm.gas_meter. - Extend the named helper vocabulary (
charge_*,credit_*,restore_*,withhold_*, ...) instead of doing gas arithmetic by hand at call sites; encode each helper's invariant as an assert. - State gas is charged by the frame whose opcode causes the creation, before the child's execution-gas share is withheld; the whole reservoir passes to the child.
- A failing frame settles its own meter before returning, so parents incorporate children unconditionally.
- Opcodes that touch state use labeled stages, with all charging before the operation:
GAS (STATE-INDEPENDENT)→STATE ACCESS (STATE-DEPENDENT GAS)→STATE GAS→CHILD GRANT→OPERATION. Simple opcodes keep the bareGASmarker.generic_call/generic_createcontain no pricing; they run the child lifecycle:PREFLIGHT→DESTINATION ACCESS→CHILD GRANT→DISPATCH→OUTCOME. - Avoid "frame" in gas identifiers (a future EIP claims the term); when a name diverges from the spec's variable name, cross-reference the spec name in the docstring.
- A gas change is behavior-preserving only if the relative order of every charge, check, and trace event is unchanged; verify with the gas-related fill tests under
tests/<fork>/.
Adding a New Precompile
- Define address constant in
vm/precompiled_contracts/__init__.pyusinghex_to_address("0x...") - Create implementation file
vm/precompiled_contracts/<name>.py - Register in
PRE_COMPILED_CONTRACTSdict invm/precompiled_contracts/mapping.py - Add gas constant in
vm/gas.py
Adding a New Transaction Type
- Define
@slotted_freezable @dataclassclass intransactions.py - Add to
Transactionunion type at bottom of file - Handle in
fork.pyvalidation/processing logic - Add exception type in
exceptions.pyif needed
Creating a New Fork
uv run ethereum-spec-new-fork --new-fork=<name> --template-fork=<template>
- Copies all files from template fork and applies codemods (renames, constant updates)
- After running: update
__init__.pydocstring, fork-specific constants, runuv run ruff format - Fork criteria types:
ByBlockNumber(N)(pre-merge),ByTimestamp(T)(post-merge),Unscheduled(order_index=N)(in development)
Branch Naming
- Feature branches:
eips/<fork_name>/eip-<number> - PR targets:
forks/<fork_name>