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