ef_tests/cases/
blockchain_test.rs

1//! Test runners for `BlockchainTests` in <https://github.com/ethereum/tests>
2
3use crate::{
4    models::{BlockchainTest, ForkSpec},
5    Case, Error, Suite,
6};
7use alloy_rlp::Decodable;
8use rayon::iter::{ParallelBridge, ParallelIterator};
9use reth_chainspec::ChainSpec;
10use reth_consensus::{Consensus, HeaderValidator};
11use reth_db_common::init::{insert_genesis_hashes, insert_genesis_history, insert_genesis_state};
12use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
13use reth_ethereum_primitives::Block;
14use reth_evm::execute::{BlockExecutorProvider, Executor};
15use reth_evm_ethereum::execute::EthExecutorProvider;
16use reth_primitives_traits::{RecoveredBlock, SealedBlock};
17use reth_provider::{
18    test_utils::create_test_provider_factory_with_chain_spec, BlockWriter, DatabaseProviderFactory,
19    ExecutionOutcome, HistoryWriter, OriginalValuesKnown, StateWriter, StorageLocation,
20};
21use reth_revm::database::StateProviderDatabase;
22use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot};
23use reth_trie_db::DatabaseStateRoot;
24use std::{collections::BTreeMap, fs, path::Path, sync::Arc};
25
26/// A handler for the blockchain test suite.
27#[derive(Debug)]
28pub struct BlockchainTests {
29    suite: String,
30}
31
32impl BlockchainTests {
33    /// Create a new handler for a subset of the blockchain test suite.
34    pub const fn new(suite: String) -> Self {
35        Self { suite }
36    }
37}
38
39impl Suite for BlockchainTests {
40    type Case = BlockchainTestCase;
41
42    fn suite_name(&self) -> String {
43        format!("BlockchainTests/{}", self.suite)
44    }
45}
46
47/// An Ethereum blockchain test.
48#[derive(Debug, PartialEq, Eq)]
49pub struct BlockchainTestCase {
50    tests: BTreeMap<String, BlockchainTest>,
51    skip: bool,
52}
53
54impl BlockchainTestCase {
55    /// Returns `true` if the fork is not supported.
56    const fn excluded_fork(network: ForkSpec) -> bool {
57        matches!(
58            network,
59            ForkSpec::ByzantiumToConstantinopleAt5 |
60                ForkSpec::Constantinople |
61                ForkSpec::ConstantinopleFix |
62                ForkSpec::MergeEOF |
63                ForkSpec::MergeMeterInitCode |
64                ForkSpec::MergePush0 |
65                ForkSpec::Unknown
66        )
67    }
68
69    /// Checks if the test case is a particular test called `UncleFromSideChain`
70    ///
71    /// This fixture fails as expected, however it fails at the wrong block number.
72    /// Given we no longer have uncle blocks, this test case was pulled out such
73    /// that we ensure it still fails as expected, however we do not check the block number.
74    #[inline]
75    fn is_uncle_sidechain_case(name: &str) -> bool {
76        name.contains("UncleFromSideChain")
77    }
78
79    /// If the test expects an exception, return the block number
80    /// at which it must occur together with the original message.
81    ///
82    /// Note: There is a +1 here because the genesis block is not included
83    /// in the set of blocks, so the first block is actually block number 1
84    /// and not block number 0.
85    #[inline]
86    fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
87        case.blocks.iter().enumerate().find_map(|(idx, blk)| {
88            blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
89        })
90    }
91
92    /// Execute a single `BlockchainTest`, validating the outcome against the
93    /// expectations encoded in the JSON file.
94    fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
95        let expectation = Self::expected_failure(case);
96        match run_case(case) {
97            // All blocks executed successfully.
98            Ok(()) => {
99                // Check if the test case specifies that it should have failed
100                if let Some((block, msg)) = expectation {
101                    Err(Error::Assertion(format!(
102                        "Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
103                    )))
104                } else {
105                    Ok(())
106                }
107            }
108
109            // A block processing failure occurred.
110            Err(Error::BlockProcessingFailed { block_number }) => match expectation {
111                // It happened on exactly the block we were told to fail on
112                Some((expected, _)) if block_number == expected => Ok(()),
113
114                // Uncle side‑chain edge case, we accept as long as it failed.
115                // But we don't check the exact block number.
116                _ if Self::is_uncle_sidechain_case(name) => Ok(()),
117
118                // Expected failure, but block number does not match
119                Some((expected, _)) => Err(Error::Assertion(format!(
120                    "Test case: {name}\nExpected failure at block {expected}\nGot failure at block {block_number}",
121                ))),
122
123                // No failure expected at all - bubble up original error.
124                None => Err(Error::BlockProcessingFailed { block_number }),
125            },
126
127            // Non‑processing error – forward as‑is.
128            //
129            // This should only happen if we get an unexpected error from processing the block.
130            // Since it is unexpected, we treat it as a test failure. 
131            //
132            // One reason for this happening is when one forgets to wrap the error from `run_case`
133            // so that it produces a `Error::BlockProcessingFailed`  
134            Err(other) => Err(other),
135        }
136    }
137}
138
139impl Case for BlockchainTestCase {
140    fn load(path: &Path) -> Result<Self, Error> {
141        Ok(Self {
142            tests: {
143                let s = fs::read_to_string(path)
144                    .map_err(|error| Error::Io { path: path.into(), error })?;
145                serde_json::from_str(&s)
146                    .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
147            },
148            skip: should_skip(path),
149        })
150    }
151
152    /// Runs the test cases for the Ethereum Forks test suite.
153    ///
154    /// # Errors
155    /// Returns an error if the test is flagged for skipping or encounters issues during execution.
156    fn run(&self) -> Result<(), Error> {
157        // If the test is marked for skipping, return a Skipped error immediately.
158        if self.skip {
159            return Err(Error::Skipped)
160        }
161
162        // Iterate through test cases, filtering by the network type to exclude specific forks.
163        self.tests
164            .iter()
165            .filter(|(_, case)| !Self::excluded_fork(case.network))
166            .par_bridge()
167            .try_for_each(|(name, case)| Self::run_single_case(name, case))?;
168
169        Ok(())
170    }
171}
172
173/// Executes a single `BlockchainTest`, returning an error if the blockchain state
174/// does not match the expected outcome after all blocks are executed.
175///
176/// A `BlockchainTest` represents a self-contained scenario:
177/// - It initializes a fresh blockchain state.
178/// - It sequentially decodes, executes, and inserts a predefined set of blocks.
179/// - It then verifies that the resulting blockchain state (post-state) matches the expected
180///   outcome.
181///
182/// Returns:
183/// - `Ok(())` if all blocks execute successfully and the final state is correct.
184/// - `Err(Error)` if any block fails to execute correctly, or if the post-state validation fails.
185fn run_case(case: &BlockchainTest) -> Result<(), Error> {
186    // Create a new test database and initialize a provider for the test case.
187    let chain_spec: Arc<ChainSpec> = Arc::new(case.network.into());
188    let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
189    let provider = factory.database_provider_rw().unwrap();
190
191    // Insert initial test state into the provider.
192    let genesis_block = SealedBlock::<Block>::from_sealed_parts(
193        case.genesis_block_header.clone().into(),
194        Default::default(),
195    )
196    .try_recover()
197    .unwrap();
198
199    provider
200        .insert_block(genesis_block.clone(), StorageLocation::Database)
201        .map_err(|_| Error::BlockProcessingFailed { block_number: 0 })?;
202
203    let genesis_state = case.pre.clone().into_genesis_state();
204    insert_genesis_state(&provider, genesis_state.iter())
205        .map_err(|_| Error::BlockProcessingFailed { block_number: 0 })?;
206    insert_genesis_hashes(&provider, genesis_state.iter())
207        .map_err(|_| Error::BlockProcessingFailed { block_number: 0 })?;
208    insert_genesis_history(&provider, genesis_state.iter())
209        .map_err(|_| Error::BlockProcessingFailed { block_number: 0 })?;
210
211    // Decode blocks
212    let blocks = decode_blocks(&case.blocks)?;
213
214    let executor_provider = EthExecutorProvider::ethereum(chain_spec.clone());
215    let mut parent = genesis_block;
216
217    for (block_index, block) in blocks.iter().enumerate() {
218        // Note: same as the comment on `decode_blocks` as to why we cannot use block.number
219        let block_number = (block_index + 1) as u64;
220
221        // Insert the block into the database
222        provider
223            .insert_block(block.clone(), StorageLocation::Database)
224            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
225
226        // Consensus checks before block execution
227        pre_execution_checks(chain_spec.clone(), &parent, block)
228            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
229
230        // Execute the block
231        let state_db = StateProviderDatabase(provider.latest());
232        let executor = executor_provider.executor(state_db);
233        let output =
234            executor.execute(block).map_err(|_| Error::BlockProcessingFailed { block_number })?;
235
236        // Consensus checks after block execution
237        validate_block_post_execution(block, &chain_spec, &output.receipts, &output.requests)
238            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
239
240        // Compute and check the post state root
241        let hashed_state =
242            HashedPostState::from_bundle_state::<KeccakKeyHasher>(output.state.state());
243        let (computed_state_root, _) =
244            StateRoot::overlay_root_with_updates(provider.tx_ref(), hashed_state.clone())
245                .map_err(|_| Error::BlockProcessingFailed { block_number })?;
246        if computed_state_root != block.state_root {
247            return Err(Error::BlockProcessingFailed { block_number })
248        }
249
250        // Commit the post state/state diff to the database
251        provider
252            .write_state(
253                &ExecutionOutcome::single(block.number, output),
254                OriginalValuesKnown::Yes,
255                StorageLocation::Database,
256            )
257            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
258
259        provider
260            .write_hashed_state(&hashed_state.into_sorted())
261            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
262        provider
263            .update_history_indices(block.number..=block.number)
264            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
265
266        // Since there were no errors, update the parent block
267        parent = block.clone()
268    }
269
270    // Validate the post-state for the test case.
271    //
272    // If we get here then it means that the post-state root checks
273    // made after we execute each block was successful.
274    //
275    // If an error occurs here, then it is:
276    // - Either an issue with the test setup
277    // - Possibly an error in the test case where the post-state root in the last block does not
278    //   match the post-state values.
279    let expected_post_state = case.post_state.as_ref().ok_or(Error::MissingPostState)?;
280    for (&address, account) in expected_post_state {
281        account.assert_db(address, provider.tx_ref())?;
282    }
283
284    Ok(())
285}
286
287fn decode_blocks(
288    test_case_blocks: &[crate::models::Block],
289) -> Result<Vec<RecoveredBlock<Block>>, Error> {
290    let mut blocks = Vec::with_capacity(test_case_blocks.len());
291    for (block_index, block) in test_case_blocks.iter().enumerate() {
292        // The blocks do not include the genesis block which is why we have the plus one.
293        // We also cannot use block.number because for invalid blocks, this may be incorrect.
294        let block_number = (block_index + 1) as u64;
295
296        let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
297            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
298
299        let recovered_block = decoded
300            .clone()
301            .try_recover()
302            .map_err(|_| Error::BlockProcessingFailed { block_number })?;
303
304        blocks.push(recovered_block);
305    }
306
307    Ok(blocks)
308}
309
310fn pre_execution_checks(
311    chain_spec: Arc<ChainSpec>,
312    parent: &RecoveredBlock<Block>,
313    block: &RecoveredBlock<Block>,
314) -> Result<(), Error> {
315    let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
316
317    let sealed_header = block.sealed_header();
318
319    <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
320        &consensus,
321        block.body(),
322        sealed_header,
323    )?;
324    consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
325    consensus.validate_header(sealed_header)?;
326    consensus.validate_block_pre_execution(block)?;
327
328    Ok(())
329}
330
331/// Returns whether the test at the given path should be skipped.
332///
333/// Some tests are edge cases that cannot happen on mainnet, while others are skipped for
334/// convenience (e.g. they take a long time to run) or are temporarily disabled.
335///
336/// The reason should be documented in a comment above the file name(s).
337pub fn should_skip(path: &Path) -> bool {
338    let path_str = path.to_str().expect("Path is not valid UTF-8");
339    let name = path.file_name().unwrap().to_str().unwrap();
340    matches!(
341        name,
342        // funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
343        // custom json parser. https://github.com/ethereum/tests/issues/971
344        | "ValueOverflow.json"
345        | "ValueOverflowParis.json"
346
347        // txbyte is of type 02 and we don't parse tx bytes for this test to fail.
348        | "typeTwoBerlin.json"
349
350        // Test checks if nonce overflows. We are handling this correctly but we are not parsing
351        // exception in testsuite There are more nonce overflow tests that are in internal
352        // call/create, and those tests are passing and are enabled.
353        | "CreateTransactionHighNonce.json"
354
355        // Test check if gas price overflows, we handle this correctly but does not match tests specific
356        // exception.
357        | "HighGasPrice.json"
358        | "HighGasPriceParis.json"
359
360        // Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
361        // London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
362        // It is expected to not execute these tests.
363        | "accessListExample.json"
364        | "basefeeExample.json"
365        | "eip1559.json"
366        | "mergeTest.json"
367
368        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
369        | "loopExp.json"
370        | "Call50000_sha256.json"
371        | "static_Call50000_sha256.json"
372        | "loopMul.json"
373        | "CALLBlake2f_MaxRounds.json"
374        | "shiftCombinations.json"
375
376        // Skipped by revm as well: <https://github.com/bluealloy/revm/blob/be92e1db21f1c47b34c5a58cfbf019f6b97d7e4b/bins/revme/src/cmd/statetest/runner.rs#L115-L125>
377        | "RevertInCreateInInit_Paris.json"
378        | "RevertInCreateInInit.json"
379        | "dynamicAccountOverwriteEmpty.json"
380        | "dynamicAccountOverwriteEmpty_Paris.json"
381        | "RevertInCreateInInitCreate2Paris.json"
382        | "create2collisionStorage.json"
383        | "RevertInCreateInInitCreate2.json"
384        | "create2collisionStorageParis.json"
385        | "InitCollision.json"
386        | "InitCollisionParis.json"
387    )
388    // Ignore outdated EOF tests that haven't been updated for Cancun yet.
389    || path_contains(path_str, &["EIPTests", "stEOF"])
390}
391
392/// `str::contains` but for a path. Takes into account the OS path separator (`/` or `\`).
393fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
394    let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
395    path_str.contains(&rhs)
396}