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