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::{ParallelBridge, ParallelIterator};
9use reth_chainspec::ChainSpec;
10use reth_primitives::{BlockBody, SealedBlock, StaticFileSegment};
11use reth_provider::{
12    providers::StaticFileWriter, test_utils::create_test_provider_factory_with_chain_spec,
13    DatabaseProviderFactory, HashingWriter, StaticFileProviderFactory,
14};
15use reth_stages::{stages::ExecutionStage, ExecInput, Stage};
16use std::{collections::BTreeMap, fs, path::Path, sync::Arc};
17
18/// A handler for the blockchain test suite.
19#[derive(Debug)]
20pub struct BlockchainTests {
21    suite: String,
22}
23
24impl BlockchainTests {
25    /// Create a new handler for a subset of the blockchain test suite.
26    pub const fn new(suite: String) -> Self {
27        Self { suite }
28    }
29}
30
31impl Suite for BlockchainTests {
32    type Case = BlockchainTestCase;
33
34    fn suite_name(&self) -> String {
35        format!("BlockchainTests/{}", self.suite)
36    }
37}
38
39/// An Ethereum blockchain test.
40#[derive(Debug, PartialEq, Eq)]
41pub struct BlockchainTestCase {
42    tests: BTreeMap<String, BlockchainTest>,
43    skip: bool,
44}
45
46impl Case for BlockchainTestCase {
47    fn load(path: &Path) -> Result<Self, Error> {
48        Ok(Self {
49            tests: {
50                let s = fs::read_to_string(path)
51                    .map_err(|error| Error::Io { path: path.into(), error })?;
52                serde_json::from_str(&s)
53                    .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
54            },
55            skip: should_skip(path),
56        })
57    }
58
59    /// Runs the test cases for the Ethereum Forks test suite.
60    ///
61    /// # Errors
62    /// Returns an error if the test is flagged for skipping or encounters issues during execution.
63    fn run(&self) -> Result<(), Error> {
64        // If the test is marked for skipping, return a Skipped error immediately.
65        if self.skip {
66            return Err(Error::Skipped)
67        }
68
69        // Iterate through test cases, filtering by the network type to exclude specific forks.
70        self.tests
71            .values()
72            .filter(|case| {
73                !matches!(
74                    case.network,
75                    ForkSpec::ByzantiumToConstantinopleAt5 |
76                        ForkSpec::Constantinople |
77                        ForkSpec::ConstantinopleFix |
78                        ForkSpec::MergeEOF |
79                        ForkSpec::MergeMeterInitCode |
80                        ForkSpec::MergePush0 |
81                        ForkSpec::Unknown
82                )
83            })
84            .par_bridge()
85            .try_for_each(|case| {
86                // Create a new test database and initialize a provider for the test case.
87                let chain_spec: Arc<ChainSpec> = Arc::new(case.network.into());
88                let provider = create_test_provider_factory_with_chain_spec(chain_spec.clone())
89                    .database_provider_rw()
90                    .unwrap();
91
92                // Insert initial test state into the provider.
93                provider.insert_historical_block(
94                    SealedBlock::new(
95                        case.genesis_block_header.clone().into(),
96                        BlockBody::default(),
97                    )
98                    .try_seal_with_senders()
99                    .unwrap(),
100                )?;
101                case.pre.write_to_db(provider.tx_ref())?;
102
103                // Initialize receipts static file with genesis
104                {
105                    let static_file_provider = provider.static_file_provider();
106                    let mut receipts_writer =
107                        static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
108                    receipts_writer.increment_block(0).unwrap();
109                    receipts_writer.commit_without_sync_all().unwrap();
110                }
111
112                // Decode and insert blocks, creating a chain of blocks for the test case.
113                let last_block = case.blocks.iter().try_fold(None, |_, block| {
114                    let decoded = SealedBlock::decode(&mut block.rlp.as_ref())?;
115                    provider.insert_historical_block(
116                        decoded.clone().try_seal_with_senders().unwrap(),
117                    )?;
118                    Ok::<Option<SealedBlock>, Error>(Some(decoded))
119                })?;
120                provider
121                    .static_file_provider()
122                    .latest_writer(StaticFileSegment::Headers)
123                    .unwrap()
124                    .commit_without_sync_all()
125                    .unwrap();
126
127                // Execute the execution stage using the EVM processor factory for the test case
128                // network.
129                let _ = ExecutionStage::new_with_executor(
130                    reth_evm_ethereum::execute::EthExecutorProvider::ethereum(chain_spec),
131                )
132                .execute(
133                    &provider,
134                    ExecInput { target: last_block.as_ref().map(|b| b.number), checkpoint: None },
135                );
136
137                // Validate the post-state for the test case.
138                match (&case.post_state, &case.post_state_hash) {
139                    (Some(state), None) => {
140                        // Validate accounts in the state against the provider's database.
141                        for (&address, account) in state {
142                            account.assert_db(address, provider.tx_ref())?;
143                        }
144                    }
145                    (None, Some(expected_state_root)) => {
146                        // Insert state hashes into the provider based on the expected state root.
147                        let last_block = last_block.unwrap_or_default();
148                        provider.insert_hashes(
149                            0..=last_block.number,
150                            last_block.hash(),
151                            *expected_state_root,
152                        )?;
153                    }
154                    _ => return Err(Error::MissingPostState),
155                }
156
157                // Drop the provider without committing to the database.
158                drop(provider);
159                Ok(())
160            })?;
161
162        Ok(())
163    }
164}
165
166/// Returns whether the test at the given path should be skipped.
167///
168/// Some tests are edge cases that cannot happen on mainnet, while others are skipped for
169/// convenience (e.g. they take a long time to run) or are temporarily disabled.
170///
171/// The reason should be documented in a comment above the file name(s).
172pub fn should_skip(path: &Path) -> bool {
173    let path_str = path.to_str().expect("Path is not valid UTF-8");
174    let name = path.file_name().unwrap().to_str().unwrap();
175    matches!(
176        name,
177        // funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
178        // custom json parser. https://github.com/ethereum/tests/issues/971
179        | "ValueOverflow.json"
180        | "ValueOverflowParis.json"
181
182        // txbyte is of type 02 and we don't parse tx bytes for this test to fail.
183        | "typeTwoBerlin.json"
184
185        // Test checks if nonce overflows. We are handling this correctly but we are not parsing
186        // exception in testsuite There are more nonce overflow tests that are in internal
187        // call/create, and those tests are passing and are enabled.
188        | "CreateTransactionHighNonce.json"
189
190        // Test check if gas price overflows, we handle this correctly but does not match tests specific
191        // exception.
192        | "HighGasPrice.json"
193        | "HighGasPriceParis.json"
194
195        // Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
196        // London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
197        // It is expected to not execute these tests.
198        | "accessListExample.json"
199        | "basefeeExample.json"
200        | "eip1559.json"
201        | "mergeTest.json"
202
203        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
204        | "loopExp.json"
205        | "Call50000_sha256.json"
206        | "static_Call50000_sha256.json"
207        | "loopMul.json"
208        | "CALLBlake2f_MaxRounds.json"
209        | "shiftCombinations.json"
210    )
211    // Ignore outdated EOF tests that haven't been updated for Cancun yet.
212    || path_contains(path_str, &["EIPTests", "stEOF"])
213}
214
215/// `str::contains` but for a path. Takes into account the OS path separator (`/` or `\`).
216fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
217    let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
218    path_str.contains(&rhs)
219}