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, TransactionSigned};
14use reth_evm::{execute::Executor, ConfigureEvm};
15use reth_evm_ethereum::EthEvmConfig;
16use reth_primitives_traits::{Block as BlockTrait, 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, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
21};
22use reth_revm::{database::StateProviderDatabase, witness::ExecutionWitnessRecord, State};
23use reth_stateless::{
24    trie::StatelessSparseTrie, validation::stateless_validation_with_trie, ExecutionWitness,
25    UncompressedPublicKey,
26};
27use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot};
28use reth_trie_db::DatabaseStateRoot;
29use std::{
30    collections::BTreeMap,
31    fs,
32    path::{Path, PathBuf},
33    sync::Arc,
34};
35
36/// A handler for the blockchain test suite.
37#[derive(Debug)]
38pub struct BlockchainTests {
39    suite_path: PathBuf,
40}
41
42impl BlockchainTests {
43    /// Create a new suite for tests with blockchain tests format.
44    pub const fn new(suite_path: PathBuf) -> Self {
45        Self { suite_path }
46    }
47}
48
49impl Suite for BlockchainTests {
50    type Case = BlockchainTestCase;
51
52    fn suite_path(&self) -> &Path {
53        &self.suite_path
54    }
55}
56
57/// An Ethereum blockchain test.
58#[derive(Debug, PartialEq, Eq)]
59pub struct BlockchainTestCase {
60    /// The tests within this test case.
61    pub tests: BTreeMap<String, BlockchainTest>,
62    /// Whether to skip this test case.
63    pub skip: bool,
64}
65
66impl BlockchainTestCase {
67    /// Returns `true` if the fork is not supported.
68    const fn excluded_fork(network: ForkSpec) -> bool {
69        matches!(
70            network,
71            ForkSpec::ByzantiumToConstantinopleAt5 |
72                ForkSpec::Constantinople |
73                ForkSpec::ConstantinopleFix |
74                ForkSpec::MergeEOF |
75                ForkSpec::MergeMeterInitCode |
76                ForkSpec::MergePush0
77        )
78    }
79
80    /// Checks if the test case is a particular test called `UncleFromSideChain`
81    ///
82    /// This fixture fails as expected, however it fails at the wrong block number.
83    /// Given we no longer have uncle blocks, this test case was pulled out such
84    /// that we ensure it still fails as expected, however we do not check the block number.
85    #[inline]
86    fn is_uncle_sidechain_case(name: &str) -> bool {
87        name.contains("UncleFromSideChain")
88    }
89
90    /// If the test expects an exception, return the block number
91    /// at which it must occur together with the original message.
92    ///
93    /// Note: There is a +1 here because the genesis block is not included
94    /// in the set of blocks, so the first block is actually block number 1
95    /// and not block number 0.
96    #[inline]
97    fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
98        case.blocks.iter().enumerate().find_map(|(idx, blk)| {
99            blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
100        })
101    }
102
103    /// Execute a single `BlockchainTest`, validating the outcome against the
104    /// expectations encoded in the JSON file. Returns the list of executed blocks
105    /// with their execution witnesses.
106    pub fn run_single_case(
107        name: &str,
108        case: &BlockchainTest,
109    ) -> Result<Vec<(RecoveredBlock<Block>, ExecutionWitness)>, Error> {
110        let expectation = Self::expected_failure(case);
111        match run_case(case) {
112            // All blocks executed successfully.
113            Ok(program_inputs) => {
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(program_inputs)
121                }
122            }
123
124            // A block processing failure occurred.
125            Err(Error::BlockProcessingFailed { block_number, partial_program_inputs, 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(partial_program_inputs),
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(partial_program_inputs),
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, partial_program_inputs, 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            .iter()
182            .filter(|(_, case)| !Self::excluded_fork(case.network))
183            .par_bridge()
184            .try_for_each(|(name, case)| Self::run_single_case(name, case).map(|_| ()))?;
185
186        Ok(())
187    }
188}
189
190/// Executes a single `BlockchainTest` returning an error as soon as any block has a consensus
191/// validation failure.
192///
193/// A `BlockchainTest` represents a self-contained scenario:
194/// - It initializes a fresh blockchain state.
195/// - It sequentially decodes, executes, and inserts a predefined set of blocks.
196/// - It then verifies that the resulting blockchain state (post-state) matches the expected
197///   outcome.
198///
199/// Returns:
200/// - `Ok(_)` if all blocks execute successfully, returning recovered blocks and full block
201///   execution witness.
202/// - `Err(Error)` if any block fails to execute correctly, returning a partial block execution
203///   witness if the error is of variant `BlockProcessingFailed`.
204fn run_case(
205    case: &BlockchainTest,
206) -> Result<Vec<(RecoveredBlock<Block>, ExecutionWitness)>, Error> {
207    // Create a new test database and initialize a provider for the test case.
208    let chain_spec: Arc<ChainSpec> = Arc::new(case.network.into());
209    let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
210    let provider = factory.database_provider_rw().unwrap();
211
212    // Insert initial test state into the provider.
213    let genesis_block = SealedBlock::<Block>::from_sealed_parts(
214        case.genesis_block_header.clone().into(),
215        Default::default(),
216    )
217    .try_recover()
218    .unwrap();
219
220    provider
221        .insert_block(genesis_block.clone())
222        .map_err(|err| Error::block_failed(0, Default::default(), err))?;
223
224    // Increment block number for receipts static file
225    provider
226        .static_file_provider()
227        .latest_writer(StaticFileSegment::Receipts)
228        .and_then(|mut writer| writer.increment_block(0))
229        .map_err(|err| Error::block_failed(0, Default::default(), err))?;
230
231    let genesis_state = case.pre.clone().into_genesis_state();
232    insert_genesis_state(&provider, genesis_state.iter())
233        .map_err(|err| Error::block_failed(0, Default::default(), err))?;
234    insert_genesis_hashes(&provider, genesis_state.iter())
235        .map_err(|err| Error::block_failed(0, Default::default(), err))?;
236    insert_genesis_history(&provider, genesis_state.iter())
237        .map_err(|err| Error::block_failed(0, Default::default(), err))?;
238
239    // Decode blocks
240    let blocks = decode_blocks(&case.blocks)?;
241
242    let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
243    let mut parent = genesis_block;
244    let mut program_inputs = Vec::new();
245
246    for (block_index, block) in blocks.iter().enumerate() {
247        // Note: same as the comment on `decode_blocks` as to why we cannot use block.number
248        let block_number = (block_index + 1) as u64;
249
250        // Insert the block into the database
251        provider
252            .insert_block(block.clone())
253            .map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
254        // Commit static files, so we can query the headers for stateless execution below
255        provider
256            .static_file_provider()
257            .commit()
258            .map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
259
260        // Consensus checks before block execution
261        pre_execution_checks(chain_spec.clone(), &parent, block).map_err(|err| {
262            program_inputs.push((block.clone(), execution_witness_with_parent(&parent)));
263            Error::block_failed(block_number, program_inputs.clone(), err)
264        })?;
265
266        let mut witness_record = ExecutionWitnessRecord::default();
267
268        // Execute the block
269        let state_provider = provider.latest();
270        let state_db = StateProviderDatabase(&state_provider);
271        let executor = executor_provider.batch_executor(state_db);
272
273        let output = executor
274            .execute_with_state_closure_always(&(*block).clone(), |statedb: &State<_>| {
275                witness_record.record_executed_state(statedb);
276            })
277            .map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
278
279        // Consensus checks after block execution
280        validate_block_post_execution(block, &chain_spec, &output.receipts, &output.requests)
281            .map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
282
283        // Generate the stateless witness
284        // TODO: Most of this code is copy-pasted from debug_executionWitness
285        let ExecutionWitnessRecord { hashed_state, codes, keys, lowest_block_number } =
286            witness_record;
287        let state = state_provider.witness(Default::default(), hashed_state)?;
288        let mut exec_witness = ExecutionWitness { state, codes, keys, headers: Default::default() };
289
290        let smallest = lowest_block_number.unwrap_or_else(|| {
291            // Return only the parent header, if there were no calls to the
292            // BLOCKHASH opcode.
293            block_number.saturating_sub(1)
294        });
295
296        let range = smallest..block_number;
297
298        exec_witness.headers = provider
299            .headers_range(range)?
300            .into_iter()
301            .map(|header| {
302                let mut serialized_header = Vec::new();
303                header.encode(&mut serialized_header);
304                serialized_header.into()
305            })
306            .collect();
307
308        program_inputs.push((block.clone(), exec_witness));
309
310        // Compute and check the post state root
311        let hashed_state =
312            HashedPostState::from_bundle_state::<KeccakKeyHasher>(output.state.state());
313        let (computed_state_root, _) =
314            StateRoot::overlay_root_with_updates(provider.tx_ref(), hashed_state.clone())
315                .map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
316        if computed_state_root != block.state_root {
317            return Err(Error::block_failed(
318                block_number,
319                program_inputs.clone(),
320                Error::Assertion("state root mismatch".to_string()),
321            ));
322        }
323
324        // Commit the post state/state diff to the database
325        provider
326            .write_state(&ExecutionOutcome::single(block.number, output), OriginalValuesKnown::Yes)
327            .map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
328
329        provider
330            .write_hashed_state(&hashed_state.into_sorted())
331            .map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
332        provider
333            .update_history_indices(block.number..=block.number)
334            .map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
335
336        // Since there were no errors, update the parent block
337        parent = block.clone()
338    }
339
340    match &case.post_state {
341        Some(expected_post_state) => {
342            // Validate the post-state for the test case.
343            //
344            // If we get here then it means that the post-state root checks
345            // made after we execute each block was successful.
346            //
347            // If an error occurs here, then it is:
348            // - Either an issue with the test setup
349            // - Possibly an error in the test case where the post-state root in the last block does
350            //   not match the post-state values.
351            for (address, account) in expected_post_state {
352                account.assert_db(*address, provider.tx_ref())?;
353            }
354        }
355        None => {
356            // Some tests may not have post-state (e.g., state-heavy benchmark tests).
357            // In this case, we can skip the post-state validation.
358        }
359    }
360
361    // Now validate using the stateless client if everything else passes
362    for (recovered_block, execution_witness) in &program_inputs {
363        let block = recovered_block.clone().into_block();
364
365        // Recover the actual public keys from the transaction signatures
366        let public_keys = recover_signers(block.body().transactions())
367            .expect("Failed to recover public keys from transaction signatures");
368
369        stateless_validation_with_trie::<StatelessSparseTrie, _, _>(
370            block,
371            public_keys,
372            execution_witness.clone(),
373            chain_spec.clone(),
374            EthEvmConfig::new(chain_spec.clone()),
375        )
376        .expect("stateless validation failed");
377    }
378
379    Ok(program_inputs)
380}
381
382fn decode_blocks(
383    test_case_blocks: &[crate::models::Block],
384) -> Result<Vec<RecoveredBlock<Block>>, Error> {
385    let mut blocks = Vec::with_capacity(test_case_blocks.len());
386    for (block_index, block) in test_case_blocks.iter().enumerate() {
387        // The blocks do not include the genesis block which is why we have the plus one.
388        // We also cannot use block.number because for invalid blocks, this may be incorrect.
389        let block_number = (block_index + 1) as u64;
390
391        let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
392            .map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
393
394        let recovered_block = decoded
395            .clone()
396            .try_recover()
397            .map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
398
399        blocks.push(recovered_block);
400    }
401
402    Ok(blocks)
403}
404
405fn pre_execution_checks(
406    chain_spec: Arc<ChainSpec>,
407    parent: &RecoveredBlock<Block>,
408    block: &RecoveredBlock<Block>,
409) -> Result<(), Error> {
410    let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
411
412    let sealed_header = block.sealed_header();
413
414    <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
415        &consensus,
416        block.body(),
417        sealed_header,
418    )?;
419    consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
420    consensus.validate_header(sealed_header)?;
421    consensus.validate_block_pre_execution(block)?;
422
423    Ok(())
424}
425
426/// Recover public keys from transaction signatures.
427fn recover_signers<'a, I>(txs: I) -> Result<Vec<UncompressedPublicKey>, Box<dyn std::error::Error>>
428where
429    I: IntoIterator<Item = &'a TransactionSigned>,
430{
431    txs.into_iter()
432        .enumerate()
433        .map(|(i, tx)| {
434            tx.signature()
435                .recover_from_prehash(&tx.signature_hash())
436                .map(|keys| {
437                    UncompressedPublicKey(
438                        keys.to_encoded_point(false).as_bytes().try_into().unwrap(),
439                    )
440                })
441                .map_err(|e| format!("failed to recover signature for tx #{i}: {e}").into())
442        })
443        .collect::<Result<Vec<UncompressedPublicKey>, _>>()
444}
445
446/// Returns whether the test at the given path should be skipped.
447///
448/// Some tests are edge cases that cannot happen on mainnet, while others are skipped for
449/// convenience (e.g. they take a long time to run) or are temporarily disabled.
450///
451/// The reason should be documented in a comment above the file name(s).
452pub fn should_skip(path: &Path) -> bool {
453    let path_str = path.to_str().expect("Path is not valid UTF-8");
454    let name = path.file_name().unwrap().to_str().unwrap();
455    matches!(
456        name,
457        // funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
458        // custom json parser. https://github.com/ethereum/tests/issues/971
459        | "ValueOverflow.json"
460        | "ValueOverflowParis.json"
461
462        // txbyte is of type 02 and we don't parse tx bytes for this test to fail.
463        | "typeTwoBerlin.json"
464
465        // Test checks if nonce overflows. We are handling this correctly but we are not parsing
466        // exception in testsuite. There are more nonce overflow tests that are internal
467        // call/create, and those tests are passing and are enabled.
468        | "CreateTransactionHighNonce.json"
469
470        // Test check if gas price overflows, we handle this correctly but does not match tests specific
471        // exception.
472        | "HighGasPrice.json"
473        | "HighGasPriceParis.json"
474
475        // Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
476        // London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
477        // It is expected to not execute these tests.
478        | "accessListExample.json"
479        | "basefeeExample.json"
480        | "eip1559.json"
481        | "mergeTest.json"
482
483        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
484        | "loopExp.json"
485        | "Call50000_sha256.json"
486        | "static_Call50000_sha256.json"
487        | "loopMul.json"
488        | "CALLBlake2f_MaxRounds.json"
489        | "shiftCombinations.json"
490
491        // Skipped by revm as well: <https://github.com/bluealloy/revm/blob/be92e1db21f1c47b34c5a58cfbf019f6b97d7e4b/bins/revme/src/cmd/statetest/runner.rs#L115-L125>
492        | "RevertInCreateInInit_Paris.json"
493        | "RevertInCreateInInit.json"
494        | "dynamicAccountOverwriteEmpty.json"
495        | "dynamicAccountOverwriteEmpty_Paris.json"
496        | "RevertInCreateInInitCreate2Paris.json"
497        | "create2collisionStorage.json"
498        | "RevertInCreateInInitCreate2.json"
499        | "create2collisionStorageParis.json"
500        | "InitCollision.json"
501        | "InitCollisionParis.json"
502    )
503    // Ignore outdated EOF tests that haven't been updated for Cancun yet.
504    || path_contains(path_str, &["EIPTests", "stEOF"])
505}
506
507/// `str::contains` but for a path. Takes into account the OS path separator (`/` or `\`).
508fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
509    let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
510    path_str.contains(&rhs)
511}
512
513fn execution_witness_with_parent(parent: &RecoveredBlock<Block>) -> ExecutionWitness {
514    let mut serialized_header = Vec::new();
515    parent.header().encode(&mut serialized_header);
516    ExecutionWitness { headers: vec![serialized_header.into()], ..Default::default() }
517}