Using and Extending Fork Methods¶
This document describes the Fork class in the Ethereum execution spec tests framework, which provides a standardized way to define properties of Ethereum forks. Understanding how to use and extend these fork methods is essential for writing flexible tests that can automatically adapt to different forks.
Overview¶
The BaseFork class is an abstract base class that defines the interface for all Ethereum forks. Each implemented
fork (like Frontier, Homestead, etc.) extends this class and implements its abstract methods to provide fork-specific
behavior.
The fork system allows:
- Defining fork-specific behaviors and parameters
- Comparing forks chronologically (
Paris < Shanghai) - Supporting automatic fork transitions
- Writing tests that automatically adapt to different forks
Using Fork Methods in Tests¶
Fork methods are powerful tools that allow your tests to adapt to different Ethereum forks automatically. Here are common patterns for using them:
1. Check Fork Support for Features¶
def test_some_feature(fork):
if fork.supports_blobs():
# Test blob-related functionality
...
else:
# Test alternative or skip
pytest.skip("Fork does not support blobs")
2. Get Fork-Specific Parameters¶
def test_transaction_gas(fork, state_test):
gas_cost = fork.gas_costs().GAS_TX_BASE
# Create a transaction with the correct gas parameters for this fork
tx = Transaction(
gas_limit=gas_cost + 10000,
# ...
)
state_test(
env=Environment(),
pre=pre,
tx=tx,
# ...
)
3. Determine Valid Transaction Types¶
def test_transaction_types(fork, state_test):
for tx_type in fork.tx_types():
# Test each transaction type supported by this fork
# ...
pass
4. Determine Valid Opcodes¶
def test_opcodes(fork, state_test):
# Create bytecode using only opcodes valid for this fork
valid_opcodes = fork.valid_opcodes()
# Use these opcodes to create test bytecode
# ...
5. Test Fork Transitions¶
def test_fork_transition(transition_fork, blockchain_test):
# The transition_fork is a special fork type that changes behavior
# based on block number or timestamp
fork_before = transition_fork.fork_at(block_number=4, timestamp=0)
fork_after = transition_fork.fork_at(block_number=5, timestamp=0)
# Test behavior before and after transition
# ...
Important Fork Methods¶
Header Information¶
These methods determine what fields are required in block headers for a given fork:
fork.header_base_fee_required() # Added in London
fork.header_prev_randao_required() # Added in Paris
fork.header_withdrawals_required() # Added in Shanghai
fork.header_excess_blob_gas_required() # Added in Cancun
fork.header_blob_gas_used_required() # Added in Cancun
fork.header_beacon_root_required() # Added in Cancun
fork.header_requests_required() # Added in Prague
Gas Parameters¶
Methods for determining gas costs and calculations:
fork.gas_costs() # Returns a GasCosts dataclass
fork.memory_expansion_gas_calculator() # Returns a callable
fork.transaction_intrinsic_cost_calculator() # Returns a callable
Transaction Types¶
Methods for determining valid transaction types:
fork.tx_types() # Returns list of supported transaction types
fork.contract_creating_tx_types() # Returns list of tx types that can create contracts
fork.precompiles() # Returns list of precompile addresses
fork.system_contracts() # Returns list of system contract addresses
EVM Features¶
Methods for determining EVM features and valid opcodes:
fork.valid_opcodes() # Returns list of valid opcodes for this fork
fork.call_opcodes() # Returns list of call opcodes
fork.create_opcodes() # Returns list of create opcodes
Blob-related Methods (Cancun+)¶
Methods for blob transaction support:
fork.supports_blobs() # Returns whether blobs are supported
fork.blob_gas_price_calculator() # Returns a callable
fork.excess_blob_gas_calculator() # Returns a callable
fork.min_base_fee_per_blob_gas() # Returns minimum base fee per blob gas
fork.blob_gas_per_blob() # Returns blob gas per blob
fork.target_blobs_per_block() # Returns target blobs per block
fork.max_blobs_per_block() # Returns max blobs per block
Meta Information¶
Methods for fork identification and comparison:
fork.name() # Returns the name of the fork
fork.transition_tool_name() # Returns name for transition tools
fork.is_deployed() # Returns whether the fork is deployed to mainnet
Fork Transitions¶
The framework supports creating transition forks that change behavior at specific block numbers or timestamps:
@transition_fork(to_fork=Shanghai, from_fork=Paris, at_timestamp=15_000)
class ParisToShanghaiAtTime15k(TransitionBaseClass):
"""Paris to Shanghai transition at Timestamp 15k."""
pass
The TransitionFork Type¶
Tests that use the valid_at_transition_to marker must type their fork parameter as TransitionFork instead of the
regular Fork type:
from execution_testing.forks import TransitionFork
@pytest.mark.valid_at_transition_to("London")
def test_transition_behavior(
blockchain_test: BlockchainTestFiller,
fork: TransitionFork,
pre: Alloc,
):
pass
The TransitionFork type provides the following methods:
fork.transitions_from()— returns the fork before the transitionfork.transitions_to()— returns the fork after the transitionfork.fork_at(block_number=N, timestamp=T)— returns the activeForkat the given block number and timestamp
Transition Fork Comparisons¶
Transition forks support comparison operators. A transition fork compares based on its transitions_to() fork. For a
transition A -> B:
| Expression | Result | Reason |
|---|---|---|
A->B >= A |
True |
B is newer than or equal to A |
A->B <= A |
False |
B is newer than A, not older or equal |
A->B >= B |
True |
B is equal to B |
A->B <= B |
True |
B is equal to B |
This logic mirrors how the test framework determines whether a transition fork is included when evaluating
valid_from / valid_until markers:
| Marker | Comparison | Transition Included? |
|---|---|---|
From A |
fork >= A |
Yes |
Until A |
fork <= A |
No |
From B |
fork >= B |
Yes |
Until B |
fork <= B |
Yes |
Comparisons between two transition forks also use their respective transitions_to() forks, which enables correct
sorting of transition forks.
Testing Behavior Across Transitions¶
With transition forks, you can test how behavior changes across fork boundaries:
# Behavior changes at block 5
fork = BerlinToLondonAt5
assert not fork.fork_at(block_number=4).header_base_fee_required() # Berlin doesn't require base fee
assert fork.fork_at(block_number=5).header_base_fee_required() # London requires base fee
Adding New Fork Methods¶
When adding new fork methods, follow these guidelines:
- Abstract Method Definition: Add the new abstract method to
BaseForkinbase_fork.py - Method Documentation: Add docstrings explaining the purpose and behavior
- Implementation in Subsequent Forks: Implement the method in every subsequent fork class only if the fork updates the value from previous forks.
Example of adding a new method:
@classmethod
@abstractmethod
def supports_new_feature(cls) -> bool:
"""Return whether the given fork supports the new feature."""
pass
Implementation in a fork class:
@classmethod
def supports_new_feature(cls) -> bool:
"""Return whether the given fork supports the new feature."""
return False # Frontier doesn't support this feature
Implementation in a newer fork class:
@classmethod
def supports_new_feature(cls) -> bool:
"""Return whether the given fork supports the new feature."""
return True # This fork does support the feature
When to Add a New Fork Method¶
Add a new fork method when:
- A New EIP Introduces a Feature: Add methods describing the new feature's behavior
- Tests Need to Behave Differently: When tests need to adapt to different fork behaviors
- Common Fork Information is Needed: When multiple tests need the same fork-specific information
- Intrinsic Fork Properties Change: When gas costs, opcodes, or other intrinsic properties change
Do not add a new fork method when:
- The information is only needed for one specific test
- The information is not directly related to fork behavior
- The information can be calculated using existing methods
Best Practices¶
- Use Existing Methods: Check if there's already a method that provides the information you need
- Name Methods Clearly: Method names should clearly describe what they return
- Document Behavior: Include clear docstrings explaining the method's purpose and return value
- Avoid Hard-coding: Use fork methods in tests instead of hard-coding fork-specific behavior
- Test Transitions: Ensure your method works correctly with transition forks
Example: Complete Test Using Fork Methods¶
Here's an example of a test that fully utilizes fork methods to adapt its behavior:
def test_transaction_with_fork_adaptability(fork, state_test):
# Prepare pre-state
pre = Alloc()
sender = pre.fund_eoa()
# Define transaction based on fork capabilities
tx_params = {
"gas_limit": 1_000_000,
"sender": sender,
}
# Add appropriate transaction type based on fork
tx_types = fork.tx_types()
if 3 in tx_types and fork.supports_blobs():
# EIP-4844 blob transaction (type 3)
tx_params["blob_versioned_hashes"] = [Hash.generate_zero_hashes(1)[0]]
elif 2 in tx_types:
# EIP-1559 transaction (type 2)
tx_params["max_fee_per_gas"] = 10
tx_params["max_priority_fee_per_gas"] = 1
elif 1 in tx_types:
# EIP-2930 transaction (type 1)
tx_params["access_list"] = []
# Create and run the test
tx = Transaction(**tx_params)
state_test(
env=Environment(),
pre=pre,
tx=tx,
post={
sender: Account(nonce=1),
},
)
Conclusion¶
The Fork class is a powerful abstraction that allows tests to adapt to different Ethereum forks. By using fork methods consistently, you can write tests that automatically handle fork-specific behavior, making your tests more maintainable and future-proof.
When adding new fork methods, keep them focused, well-documented, and implement them across all forks. This will ensure that all tests can benefit from the information and that transitions between forks are handled correctly.