Skip to main content

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_eip7928::bal::Bal;
8use alloy_eips::{
9    eip7002::WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, eip7251::CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
10};
11use alloy_rlp::Decodable;
12use rayon::iter::{IndexedParallelIterator, ParallelIterator};
13use reth_chainspec::{ChainSpec, EthereumHardforks};
14use reth_consensus::{Consensus, HeaderValidator};
15use reth_db_common::init::{insert_genesis_hashes, insert_genesis_history, insert_genesis_state};
16use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
17use reth_ethereum_primitives::Block;
18use reth_evm::{
19    database::StateProviderDatabase,
20    execute::{BlockExecutionOutput, Executor},
21    ConfigureEvm,
22};
23use reth_evm_ethereum::EthEvmConfig;
24use reth_primitives_traits::{ParallelBridgeBuffered, RecoveredBlock, SealedBlock};
25use reth_provider::{
26    providers::{RocksDBBuilder, RocksDBProvider, StaticFileProviderBuilder},
27    test_utils::MockNodeTypesWithDB,
28    BlockWriter, DatabaseProviderFactory, ExecutionOutcome, HashedPostStateProvider, HistoryWriter,
29    OriginalValuesKnown, ProviderFactory, StateProvider, StateWriteConfig, StateWriter,
30    StaticFileProviderFactory, StaticFileSegment, StaticFileWriter, StorageSettingsCache,
31    TrieWriter,
32};
33use reth_tasks::Runtime;
34use reth_trie::StateRoot;
35use reth_trie_db::DatabaseStateRoot;
36use std::{
37    collections::BTreeMap,
38    fs,
39    path::{Path, PathBuf},
40    sync::{Arc, LazyLock},
41};
42
43/// A handler for the blockchain test suite.
44#[derive(Debug)]
45pub struct BlockchainTests {
46    suite_path: PathBuf,
47}
48
49impl BlockchainTests {
50    /// Create a new suite for tests with blockchain tests format.
51    pub const fn new(suite_path: PathBuf) -> Self {
52        Self { suite_path }
53    }
54}
55
56impl Suite for BlockchainTests {
57    type Case = BlockchainTestCase;
58
59    fn suite_path(&self) -> &Path {
60        &self.suite_path
61    }
62}
63
64/// An Ethereum blockchain test.
65#[derive(Debug, PartialEq, Eq)]
66pub struct BlockchainTestCase {
67    /// The tests within this test case.
68    pub tests: BTreeMap<String, BlockchainTest>,
69    /// Whether to skip this test case.
70    pub skip: bool,
71}
72
73impl BlockchainTestCase {
74    /// Returns `true` if the fork is not supported.
75    const fn excluded_fork(network: ForkSpec) -> bool {
76        matches!(
77            network,
78            ForkSpec::ByzantiumToConstantinopleAt5 |
79                ForkSpec::Constantinople |
80                ForkSpec::ConstantinopleFix |
81                ForkSpec::MergeEOF |
82                ForkSpec::MergeMeterInitCode |
83                ForkSpec::MergePush0
84        )
85    }
86
87    /// Returns `true` if the case runs Prague blocks without the EIP-7002 and EIP-7251 request
88    /// contracts, which makes them invalid. Legacy fixtures predate those predeploys, and their
89    /// state roots rule out adding them.
90    fn lacks_request_contracts(case: &BlockchainTest) -> bool {
91        case.network.to_chain_spec().is_prague_active_at_timestamp(u64::MAX) &&
92            [WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS]
93                .into_iter()
94                .any(|address| {
95                    case.pre.get(&address).is_none_or(|account| account.code.is_empty())
96                })
97    }
98
99    /// Checks if the test case is a particular test called `UncleFromSideChain`
100    ///
101    /// This fixture fails as expected, however it fails at the wrong block number.
102    /// Given we no longer have uncle blocks, this test case was pulled out such
103    /// that we ensure it still fails as expected, however we do not check the block number.
104    #[inline]
105    fn is_uncle_sidechain_case(name: &str) -> bool {
106        name.contains("UncleFromSideChain")
107    }
108
109    /// If the test expects an exception, return the block number
110    /// at which it must occur together with the original message.
111    ///
112    /// Note: There is a +1 here because the genesis block is not included
113    /// in the set of blocks, so the first block is actually block number 1
114    /// and not block number 0.
115    #[inline]
116    fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
117        case.blocks.iter().enumerate().find_map(|(idx, blk)| {
118            blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
119        })
120    }
121
122    /// Execute a single `BlockchainTest`, validating the outcome against the
123    /// expectations encoded in the JSON file.
124    pub fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
125        let expectation = Self::expected_failure(case);
126        match run_case(case) {
127            // All blocks executed successfully.
128            Ok(()) => {
129                // Check if the test case specifies that it should have failed
130                if let Some((block, msg)) = expectation {
131                    Err(Error::Assertion(format!(
132                        "Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
133                    )))
134                } else {
135                    Ok(())
136                }
137            }
138
139            // A block processing failure occurred.
140            Err(Error::BlockProcessingFailed { block_number, err }) => {
141                match expectation {
142                    // It happened on exactly the block we were told to fail on
143                    Some((expected, _)) if block_number == expected => Ok(()),
144
145                    // Uncle side‑chain edge case, we accept as long as it failed.
146                    // But we don't check the exact block number.
147                    _ if Self::is_uncle_sidechain_case(name) => Ok(()),
148
149                    // Expected failure, but block number does not match
150                    Some((expected, _)) => Err(Error::Assertion(format!(
151                        "Test case: {name}\nExpected failure at block {expected}\nGot failure at block {block_number}",
152                    ))),
153
154                    // No failure expected at all - bubble up original error.
155                    None => Err(Error::BlockProcessingFailed { block_number, err }),
156                }
157            }
158
159            // Non‑processing error – forward as‑is.
160            //
161            // This should only happen if we get an unexpected error from processing the block.
162            // Since it is unexpected, we treat it as a test failure.
163            //
164            // One reason for this happening is when one forgets to wrap the error from `run_case`
165            // so that it produces an `Error::BlockProcessingFailed`
166            Err(other) => Err(other),
167        }
168    }
169}
170
171impl Case for BlockchainTestCase {
172    fn load(path: &Path) -> Result<Self, Error> {
173        Ok(Self {
174            tests: {
175                let s = fs::read_to_string(path)
176                    .map_err(|error| Error::Io { path: path.into(), error })?;
177                serde_json::from_str(&s)
178                    .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
179            },
180            skip: should_skip(path),
181        })
182    }
183
184    /// Runs the test cases for the Ethereum Forks test suite.
185    ///
186    /// # Errors
187    /// Returns an error if the test is flagged for skipping or encounters issues during execution.
188    fn run(self) -> Result<(), Error> {
189        // If the test is marked for skipping, return a Skipped error immediately.
190        if self.skip {
191            return Err(Error::Skipped);
192        }
193
194        // Iterate through test cases, filtering by the network type to exclude specific forks.
195        self.tests
196            .into_iter()
197            .filter(|(_, case)| {
198                !Self::excluded_fork(case.network) && !Self::lacks_request_contracts(case)
199            })
200            .par_bridge_buffered()
201            .with_min_len(64)
202            .try_for_each(|(name, case)| Self::run_single_case(&name, &case).map(|_| ()))
203    }
204}
205
206/// Executes a single `BlockchainTest` returning an error as soon as any block has a consensus
207/// validation failure.
208///
209/// A `BlockchainTest` represents a self-contained scenario:
210/// - It initializes a fresh blockchain state.
211/// - It sequentially decodes, executes, and inserts a predefined set of blocks.
212/// - It then verifies that the resulting blockchain state (post-state) matches the expected
213///   outcome.
214///
215/// Returns:
216/// - `Ok(())` if all blocks execute successfully.
217/// - `Err(Error)` if any block fails to execute correctly.
218fn run_case(case: &BlockchainTest) -> Result<(), Error> {
219    // Create a new test database and initialize a provider for the test case.
220    let chain_spec = case.network.to_chain_spec();
221    let factory = create_test_provider_factory(chain_spec.clone());
222    let provider = factory.database_provider_rw().unwrap();
223
224    // Insert initial test state into the provider.
225    let genesis_block = SealedBlock::<Block>::from_sealed_parts(
226        case.genesis_block_header.clone().into(),
227        Default::default(),
228    )
229    .try_recover()
230    .unwrap();
231
232    provider.insert_block(&genesis_block).map_err(|err| Error::block_failed(0, err))?;
233
234    // Increment block number for receipts static file
235    provider
236        .static_file_provider()
237        .latest_writer(StaticFileSegment::Receipts)
238        .and_then(|mut writer| writer.increment_block(0))
239        .map_err(|err| Error::block_failed(0, err))?;
240
241    let genesis_state = case.pre.clone().into_genesis_state();
242    insert_genesis_state(&provider, genesis_state.iter())
243        .map_err(|err| Error::block_failed(0, err))?;
244    insert_genesis_hashes(&provider, genesis_state.iter())
245        .map_err(|err| Error::block_failed(0, err))?;
246    insert_genesis_history(&provider, genesis_state.iter())
247        .map_err(|err| Error::block_failed(0, err))?;
248
249    // Build the genesis trie, as `init_genesis` does, so block 1 reads stored nodes.
250    let (_, trie_updates) = reth_trie_db::with_adapter!(provider, |A| {
251        StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::from_tx(provider.tx_ref())
252            .root_with_updates()
253    })
254    .map_err(|err| Error::block_failed(0, err))?;
255    provider.write_trie_updates(trie_updates).map_err(|err| Error::block_failed(0, err))?;
256
257    // Decode blocks
258    let blocks = decode_blocks(&case.blocks)?;
259
260    let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
261    let mut parent = genesis_block;
262    let mut bal_buf = Vec::new();
263
264    for (block_index, block) in blocks.iter().enumerate() {
265        // Note: same as the comment on `decode_blocks` as to why we cannot use block.number
266        let block_number = (block_index + 1) as u64;
267
268        // Insert the block into the database
269        provider.insert_block(block).map_err(|err| Error::block_failed(block_number, err))?;
270        provider
271            .static_file_provider()
272            .commit()
273            .map_err(|err| Error::block_failed(block_number, err))?;
274
275        // Consensus checks before block execution
276        pre_execution_checks(chain_spec.clone(), &parent, block)
277            .map_err(|err| Error::block_failed(block_number, err))?;
278
279        // Execute the block
280        let state_provider = provider.latest();
281        let state_db = StateProviderDatabase((&state_provider).into_evm_state_provider());
282        let mut executor = executor_provider.batch_executor(state_db);
283
284        let result = executor
285            .execute_one(&(*block).clone())
286            .map_err(|err| Error::block_failed(block_number, err))?;
287        // Check the block access list size cap and compute its hash for post-Amsterdam blocks so
288        // the consensus check below validates it.
289        let block_access_list_hash = executor
290            .take_bal()
291            .map(|bal| {
292                let bal = Bal::from(bal);
293                bal.validate_gas_limit(block.gas_limit)
294                    .map(|()| bal.compute_hash_with_buf(&mut bal_buf))
295            })
296            .transpose()
297            .map_err(|err| Error::block_failed(block_number, err))?;
298        let output = BlockExecutionOutput { state: executor.into_state(), result };
299
300        // Consensus checks after block execution
301        validate_block_post_execution(block, &chain_spec, &output, None, block_access_list_hash)
302            .map_err(|err| Error::block_failed(block_number, err))?;
303
304        // Compute and check the post state root
305        let hashed_state = state_provider
306            .hashed_post_state(&output.state)
307            .map_err(|err| Error::block_failed(block_number, err))?;
308        let sorted = hashed_state.clone_into_sorted();
309        let (computed_state_root, trie_updates) = reth_trie_db::with_adapter!(provider, |A| {
310            StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::overlay_root_with_updates(
311                provider.tx_ref(),
312                &sorted,
313            )
314        })
315        .map_err(|err| Error::block_failed(block_number, err))?;
316        if computed_state_root != block.state_root {
317            return Err(Error::block_failed(
318                block_number,
319                Error::Assertion("state root mismatch".to_string()),
320            ));
321        }
322
323        // Commit the post state/state diff to the database
324        provider
325            .write_state(
326                &ExecutionOutcome::single(block.number, output),
327                OriginalValuesKnown::Yes,
328                StateWriteConfig::default(),
329            )
330            .map_err(|err| Error::block_failed(block_number, err))?;
331
332        provider
333            .write_hashed_state(&hashed_state.into_sorted())
334            .map_err(|err| Error::block_failed(block_number, err))?;
335        // Persist the trie so later blocks read stored nodes.
336        provider
337            .write_trie_updates(trie_updates)
338            .map_err(|err| Error::block_failed(block_number, err))?;
339        provider
340            .update_history_indices(block.number..=block.number)
341            .map_err(|err| Error::block_failed(block_number, err))?;
342
343        // Since there were no errors, update the parent block
344        parent = block.clone()
345    }
346
347    match &case.post_state {
348        Some(expected_post_state) => {
349            // Validate the post-state for the test case.
350            //
351            // If we get here then it means that the post-state root checks
352            // made after we execute each block was successful.
353            //
354            // If an error occurs here, then it is:
355            // - Either an issue with the test setup
356            // - Possibly an error in the test case where the post-state root in the last block does
357            //   not match the post-state values.
358            for (address, account) in expected_post_state {
359                account.assert_db(*address, provider.tx_ref())?;
360            }
361        }
362        None => {
363            // Some tests may not have post-state (e.g., state-heavy benchmark tests).
364            // In this case, we can skip the post-state validation.
365        }
366    }
367
368    Ok(())
369}
370
371fn decode_blocks(
372    test_case_blocks: &[crate::models::Block],
373) -> Result<Vec<RecoveredBlock<Block>>, Error> {
374    let mut blocks = Vec::with_capacity(test_case_blocks.len());
375    for (block_index, block) in test_case_blocks.iter().enumerate() {
376        // The blocks do not include the genesis block which is why we have the plus one.
377        // We also cannot use block.number because for invalid blocks, this may be incorrect.
378        let block_number = (block_index + 1) as u64;
379
380        let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
381            .map_err(|err| Error::block_failed(block_number, err))?;
382
383        let recovered_block =
384            decoded.try_recover().map_err(|err| Error::block_failed(block_number, err))?;
385
386        blocks.push(recovered_block);
387    }
388
389    Ok(blocks)
390}
391
392fn pre_execution_checks(
393    chain_spec: Arc<ChainSpec>,
394    parent: &RecoveredBlock<Block>,
395    block: &RecoveredBlock<Block>,
396) -> Result<(), Error> {
397    let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
398
399    let sealed_header = block.sealed_header();
400
401    <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
402        &consensus,
403        block.body(),
404        sealed_header,
405    )?;
406    consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
407    consensus.validate_header(sealed_header)?;
408    consensus.validate_block_pre_execution(block)?;
409
410    Ok(())
411}
412
413/// Returns whether the test at the given path should be skipped.
414///
415/// Some tests are edge cases that cannot happen on mainnet, while others are skipped for
416/// convenience (e.g. they take a long time to run) or are temporarily disabled.
417///
418/// The reason should be documented in a comment above the file name(s).
419pub fn should_skip(path: &Path) -> bool {
420    let path_str = path.to_str().expect("Path is not valid UTF-8");
421    let name = path.file_name().unwrap().to_str().unwrap();
422    matches!(
423        name,
424        // funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
425        // custom json parser. https://github.com/ethereum/tests/issues/971
426        | "ValueOverflow.json"
427        | "ValueOverflowParis.json"
428
429        // txbyte is of type 02 and we don't parse tx bytes for this test to fail.
430        | "typeTwoBerlin.json"
431
432        // Test checks if nonce overflows. We are handling this correctly but we are not parsing
433        // exception in testsuite. There are more nonce overflow tests that are internal
434        // call/create, and those tests are passing and are enabled.
435        | "CreateTransactionHighNonce.json"
436
437        // Test check if gas price overflows, we handle this correctly but does not match tests specific
438        // exception.
439        | "HighGasPrice.json"
440        | "HighGasPriceParis.json"
441
442        // Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
443        // London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
444        // It is expected to not execute these tests.
445        | "accessListExample.json"
446        | "basefeeExample.json"
447        | "eip1559.json"
448        | "mergeTest.json"
449
450        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
451        | "loopExp.json"
452        | "Call50000_sha256.json"
453        | "static_Call50000_sha256.json"
454        | "loopMul.json"
455        | "CALLBlake2f_MaxRounds.json"
456        | "shiftCombinations.json"
457
458        // Skipped by revm as well: <https://github.com/bluealloy/revm/blob/be92e1db21f1c47b34c5a58cfbf019f6b97d7e4b/bins/revme/src/cmd/statetest/runner.rs#L115-L125>
459        | "RevertInCreateInInit_Paris.json"
460        | "RevertInCreateInInit.json"
461        | "dynamicAccountOverwriteEmpty.json"
462        | "dynamicAccountOverwriteEmpty_Paris.json"
463        | "RevertInCreateInInitCreate2Paris.json"
464        | "create2collisionStorage.json"
465        | "RevertInCreateInInitCreate2.json"
466        | "create2collisionStorageParis.json"
467        | "InitCollision.json"
468        | "InitCollisionParis.json"
469    )
470    // Ignore outdated EOF tests that haven't been updated for Cancun yet.
471    || path_contains(path_str, &["EIPTests", "stEOF"])
472}
473
474/// `str::contains` but for a path. Takes into account the OS path separator (`/` or `\`).
475fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
476    let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
477    path_str.contains(&rhs)
478}
479
480/// `RocksDB` instance shared by all test cases of the current process.
481///
482/// Opening `RocksDB` is expensive and ef-tests never use it: the storage settings stay at v1, so
483/// everything is stored in MDBX and static files. A fresh instance per case dominates the runtime.
484///
485/// The backing directory is created with [`reth_db::test_utils::tempdir_path`], which leaks it:
486/// statics are never dropped, so there is no point at which a temp dir guard could clean
487/// up. This costs one small, mostly empty directory in the system temp dir per test process.
488static SHARED_ROCKSDB: LazyLock<RocksDBProvider> = LazyLock::new(|| {
489    RocksDBBuilder::new(reth_db::test_utils::tempdir_path().join("rocksdb"))
490        .with_default_tables()
491        .build()
492        .expect("failed to create shared test RocksDB provider")
493});
494
495/// Creates a provider factory for a single test case.
496///
497/// Mirrors [`reth_provider::test_utils::create_test_provider_factory_with_chain_spec`], except
498/// that it reuses [`SHARED_ROCKSDB`] instead of opening a new `RocksDB` instance. The temp datadir,
499/// MDBX, static files and runtime stay isolated per case.
500fn create_test_provider_factory(
501    chain_spec: Arc<ChainSpec>,
502) -> ProviderFactory<MockNodeTypesWithDB> {
503    let genesis_block_number = chain_spec.genesis.number.unwrap_or_default();
504
505    // A single temp directory holds the MDBX and static file dirs. `TempDatabase` removes the
506    // entire directory on drop.
507    let datadir_path = reth_db::test_utils::tempdir_path();
508    let static_files_path = datadir_path.join("static_files");
509    fs::create_dir_all(&static_files_path).expect("failed to create static_files dir");
510
511    let db = reth_db::test_utils::create_test_rw_db_with_datadir(&datadir_path);
512
513    let factory = ProviderFactory::new(
514        db,
515        chain_spec,
516        StaticFileProviderBuilder::read_write(static_files_path)
517            .with_genesis_block_number(genesis_block_number)
518            .build()
519            .expect("static file provider"),
520        SHARED_ROCKSDB.clone(),
521        Runtime::test(),
522    )
523    .expect("failed to create test provider factory");
524
525    assert!(
526        !factory.cached_storage_settings().any_in_rocksdb(),
527        "the shared RocksDB is only valid while ef-tests run with v1 storage settings"
528    );
529
530    factory
531}