|
| 1 | +# Using and Extending Fork Methods |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +The fork system allows: |
| 10 | + |
| 11 | +1. Defining fork-specific behaviors and parameters |
| 12 | +2. Comparing forks chronologically (`Paris < Shanghai`) |
| 13 | +3. Supporting automatic fork transitions |
| 14 | +4. Writing tests that automatically adapt to different forks |
| 15 | + |
| 16 | +## Using Fork Methods in Tests |
| 17 | + |
| 18 | +Fork methods are powerful tools that allow your tests to adapt to different Ethereum forks automatically. Here are common patterns for using them: |
| 19 | + |
| 20 | +### 1. Check Fork Support for Features |
| 21 | + |
| 22 | +```python |
| 23 | +def test_some_feature(fork): |
| 24 | + if fork.supports_blobs(block_number=0, timestamp=0): |
| 25 | + # Test blob-related functionality |
| 26 | + ... |
| 27 | + else: |
| 28 | + # Test alternative or skip |
| 29 | + pytest.skip("Fork does not support blobs") |
| 30 | +``` |
| 31 | + |
| 32 | +### 2. Get Fork-Specific Parameters |
| 33 | + |
| 34 | +```python |
| 35 | +def test_transaction_gas(fork, state_test): |
| 36 | + gas_cost = fork.gas_costs(block_number=0, timestamp=0).G_TRANSACTION |
| 37 | + |
| 38 | + # Create a transaction with the correct gas parameters for this fork |
| 39 | + tx = Transaction( |
| 40 | + gas_limit=gas_cost + 10000, |
| 41 | + # ... |
| 42 | + ) |
| 43 | + |
| 44 | + state_test( |
| 45 | + env=Environment(), |
| 46 | + pre=pre, |
| 47 | + tx=tx, |
| 48 | + # ... |
| 49 | + ) |
| 50 | +``` |
| 51 | + |
| 52 | +### 3. Determine Valid Transaction Types |
| 53 | + |
| 54 | +```python |
| 55 | +def test_transaction_types(fork, state_test): |
| 56 | + for tx_type in fork.tx_types(block_number=0, timestamp=0): |
| 57 | + # Test each transaction type supported by this fork |
| 58 | + # ... |
| 59 | +``` |
| 60 | + |
| 61 | +### 4. Determine Valid Opcodes |
| 62 | + |
| 63 | +```python |
| 64 | +def test_opcodes(fork, state_test): |
| 65 | + # Create bytecode using only opcodes valid for this fork |
| 66 | + valid_opcodes = fork.valid_opcodes() |
| 67 | + |
| 68 | + # Use these opcodes to create test bytecode |
| 69 | + # ... |
| 70 | +``` |
| 71 | + |
| 72 | +### 5. Test Fork Transitions |
| 73 | + |
| 74 | +```python |
| 75 | +def test_fork_transition(transition_fork, blockchain_test): |
| 76 | + # The transition_fork is a special fork type that changes behavior |
| 77 | + # based on block number or timestamp |
| 78 | + fork_before = transition_fork.fork_at(block_number=4, timestamp=0) |
| 79 | + fork_after = transition_fork.fork_at(block_number=5, timestamp=0) |
| 80 | + |
| 81 | + # Test behavior before and after transition |
| 82 | + # ... |
| 83 | +``` |
| 84 | + |
| 85 | +## Important Fork Methods |
| 86 | + |
| 87 | +### Header Information |
| 88 | + |
| 89 | +These methods determine what fields are required in block headers for a given fork: |
| 90 | + |
| 91 | +```python |
| 92 | +fork.header_base_fee_required(block_number=0, timestamp=0) # Added in London |
| 93 | +fork.header_prev_randao_required(block_number=0, timestamp=0) # Added in Paris |
| 94 | +fork.header_withdrawals_required(block_number=0, timestamp=0) # Added in Shanghai |
| 95 | +fork.header_excess_blob_gas_required(block_number=0, timestamp=0) # Added in Cancun |
| 96 | +fork.header_blob_gas_used_required(block_number=0, timestamp=0) # Added in Cancun |
| 97 | +fork.header_beacon_root_required(block_number=0, timestamp=0) # Added in Cancun |
| 98 | +fork.header_requests_required(block_number=0, timestamp=0) # Added in Prague |
| 99 | +``` |
| 100 | + |
| 101 | +### Gas Parameters |
| 102 | + |
| 103 | +Methods for determining gas costs and calculations: |
| 104 | + |
| 105 | +```python |
| 106 | +fork.gas_costs(block_number=0, timestamp=0) # Returns a GasCosts dataclass |
| 107 | +fork.memory_expansion_gas_calculator(block_number=0, timestamp=0) # Returns a callable |
| 108 | +fork.transaction_intrinsic_cost_calculator(block_number=0, timestamp=0) # Returns a callable |
| 109 | +``` |
| 110 | + |
| 111 | +### Transaction Types |
| 112 | + |
| 113 | +Methods for determining valid transaction types: |
| 114 | + |
| 115 | +```python |
| 116 | +fork.tx_types(block_number=0, timestamp=0) # Returns list of supported transaction types |
| 117 | +fork.contract_creating_tx_types(block_number=0, timestamp=0) # Returns list of tx types that can create contracts |
| 118 | +fork.precompiles(block_number=0, timestamp=0) # Returns list of precompile addresses |
| 119 | +fork.system_contracts(block_number=0, timestamp=0) # Returns list of system contract addresses |
| 120 | +``` |
| 121 | + |
| 122 | +### EVM Features |
| 123 | + |
| 124 | +Methods for determining EVM features and valid opcodes: |
| 125 | + |
| 126 | +```python |
| 127 | +fork.evm_code_types(block_number=0, timestamp=0) # Returns list of supported code types (e.g., Legacy, EOF) |
| 128 | +fork.valid_opcodes() # Returns list of valid opcodes for this fork |
| 129 | +fork.call_opcodes(block_number=0, timestamp=0) # Returns list of call opcodes with their code types |
| 130 | +fork.create_opcodes(block_number=0, timestamp=0) # Returns list of create opcodes with their code types |
| 131 | +``` |
| 132 | + |
| 133 | +### Blob-related Methods (Cancun+) |
| 134 | + |
| 135 | +Methods for blob transaction support: |
| 136 | + |
| 137 | +```python |
| 138 | +fork.supports_blobs(block_number=0, timestamp=0) # Returns whether blobs are supported |
| 139 | +fork.blob_gas_price_calculator(block_number=0, timestamp=0) # Returns a callable |
| 140 | +fork.excess_blob_gas_calculator(block_number=0, timestamp=0) # Returns a callable |
| 141 | +fork.min_base_fee_per_blob_gas(block_number=0, timestamp=0) # Returns minimum base fee per blob gas |
| 142 | +fork.blob_gas_per_blob(block_number=0, timestamp=0) # Returns blob gas per blob |
| 143 | +fork.target_blobs_per_block(block_number=0, timestamp=0) # Returns target blobs per block |
| 144 | +fork.max_blobs_per_block(block_number=0, timestamp=0) # Returns max blobs per block |
| 145 | +``` |
| 146 | + |
| 147 | +### Meta Information |
| 148 | + |
| 149 | +Methods for fork identification and comparison: |
| 150 | + |
| 151 | +```python |
| 152 | +fork.name() # Returns the name of the fork |
| 153 | +fork.transition_tool_name(block_number=0, timestamp=0) # Returns name for transition tools |
| 154 | +fork.solc_name() # Returns name for the solc compiler |
| 155 | +fork.solc_min_version() # Returns minimum solc version supporting this fork |
| 156 | +fork.blockchain_test_network_name() # Returns network name for blockchain tests |
| 157 | +fork.is_deployed() # Returns whether the fork is deployed to mainnet |
| 158 | +``` |
| 159 | + |
| 160 | +## Fork Transitions |
| 161 | + |
| 162 | +The framework supports creating transition forks that change behavior at specific block numbers or timestamps: |
| 163 | + |
| 164 | +```python |
| 165 | +@transition_fork(to_fork=Shanghai, at_timestamp=15_000) |
| 166 | +class ParisToShanghaiAtTime15k(Paris): |
| 167 | + """Paris to Shanghai transition at Timestamp 15k.""" |
| 168 | + pass |
| 169 | +``` |
| 170 | + |
| 171 | +With transition forks, you can test how behavior changes across fork boundaries: |
| 172 | + |
| 173 | +```python |
| 174 | +# Behavior changes at block 5 |
| 175 | +fork = BerlinToLondonAt5 |
| 176 | +assert not fork.header_base_fee_required(block_number=4) # Berlin doesn't require base fee |
| 177 | +assert fork.header_base_fee_required(block_number=5) # London requires base fee |
| 178 | +``` |
| 179 | + |
| 180 | +## Adding New Fork Methods |
| 181 | + |
| 182 | +When adding new fork methods, follow these guidelines: |
| 183 | + |
| 184 | +1. **Abstract Method Definition**: Add the new abstract method to `BaseFork` in `base_fork.py` |
| 185 | +2. **Consistent Parameter Pattern**: Use `block_number` and `timestamp` parameters with default values |
| 186 | +3. **Method Documentation**: Add docstrings explaining the purpose and behavior |
| 187 | +4. **Implementation in Subsequent Forks**: Implement the method in every subsequent fork class **only** if the fork updates the value from previous forks. |
| 188 | + |
| 189 | +Example of adding a new method: |
| 190 | + |
| 191 | +```python |
| 192 | +@classmethod |
| 193 | +@abstractmethod |
| 194 | +def supports_new_feature(cls, block_number: int = 0, timestamp: int = 0) -> bool: |
| 195 | + """Return whether the given fork supports the new feature.""" |
| 196 | + pass |
| 197 | +``` |
| 198 | + |
| 199 | +Implementation in a fork class: |
| 200 | + |
| 201 | +```python |
| 202 | +@classmethod |
| 203 | +def supports_new_feature(cls, block_number: int = 0, timestamp: int = 0) -> bool: |
| 204 | + """Return whether the given fork supports the new feature.""" |
| 205 | + return False # Frontier doesn't support this feature |
| 206 | +``` |
| 207 | + |
| 208 | +Implementation in a newer fork class: |
| 209 | + |
| 210 | +```python |
| 211 | +@classmethod |
| 212 | +def supports_new_feature(cls, block_number: int = 0, timestamp: int = 0) -> bool: |
| 213 | + """Return whether the given fork supports the new feature.""" |
| 214 | + return True # This fork does support the feature |
| 215 | +``` |
| 216 | + |
| 217 | +## When to Add a New Fork Method |
| 218 | + |
| 219 | +Add a new fork method when: |
| 220 | + |
| 221 | +1. **A New EIP Introduces a Feature**: Add methods describing the new feature's behavior |
| 222 | +2. **Tests Need to Behave Differently**: When tests need to adapt to different fork behaviors |
| 223 | +3. **Common Fork Information is Needed**: When multiple tests need the same fork-specific information |
| 224 | +4. **Intrinsic Fork Properties Change**: When gas costs, opcodes, or other intrinsic properties change |
| 225 | + |
| 226 | +Do not add a new fork method when: |
| 227 | + |
| 228 | +1. The information is only needed for one specific test |
| 229 | +2. The information is not directly related to fork behavior |
| 230 | +3. The information can be calculated using existing methods |
| 231 | + |
| 232 | +## Best Practices |
| 233 | + |
| 234 | +1. **Use Existing Methods**: Check if there's already a method that provides the information you need |
| 235 | +2. **Name Methods Clearly**: Method names should clearly describe what they return |
| 236 | +3. **Document Behavior**: Include clear docstrings explaining the method's purpose and return value |
| 237 | +4. **Avoid Hard-coding**: Use fork methods in tests instead of hard-coding fork-specific behavior |
| 238 | +5. **Test Transitions**: Ensure your method works correctly with transition forks |
| 239 | + |
| 240 | +## Example: Complete Test Using Fork Methods |
| 241 | + |
| 242 | +Here's an example of a test that fully utilizes fork methods to adapt its behavior: |
| 243 | + |
| 244 | +```python |
| 245 | +def test_transaction_with_fork_adaptability(fork, state_test): |
| 246 | + # Prepare pre-state |
| 247 | + pre = Alloc() |
| 248 | + sender = pre.fund_eoa() |
| 249 | + |
| 250 | + # Define transaction based on fork capabilities |
| 251 | + tx_params = { |
| 252 | + "gas_limit": 1_000_000, |
| 253 | + "sender": sender, |
| 254 | + } |
| 255 | + |
| 256 | + # Add appropriate transaction type based on fork |
| 257 | + tx_types = fork.tx_types(block_number=0, timestamp=0) |
| 258 | + if 3 in tx_types and fork.supports_blobs(block_number=0, timestamp=0): |
| 259 | + # EIP-4844 blob transaction (type 3) |
| 260 | + tx_params["blob_versioned_hashes"] = [Hash.generate_zero_hashes(1)[0]] |
| 261 | + elif 2 in tx_types: |
| 262 | + # EIP-1559 transaction (type 2) |
| 263 | + tx_params["max_fee_per_gas"] = 10 |
| 264 | + tx_params["max_priority_fee_per_gas"] = 1 |
| 265 | + elif 1 in tx_types: |
| 266 | + # EIP-2930 transaction (type 1) |
| 267 | + tx_params["access_list"] = [] |
| 268 | + |
| 269 | + # Create and run the test |
| 270 | + tx = Transaction(**tx_params) |
| 271 | + |
| 272 | + state_test( |
| 273 | + env=Environment(), |
| 274 | + pre=pre, |
| 275 | + tx=tx, |
| 276 | + post={ |
| 277 | + sender: Account(nonce=1), |
| 278 | + }, |
| 279 | + ) |
| 280 | +``` |
| 281 | + |
| 282 | +## Conclusion |
| 283 | + |
| 284 | +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. |
| 285 | + |
| 286 | +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. |
0 commit comments