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