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