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