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