1//! Test runners for `BlockchainTests` in <https://github.com/ethereum/tests>
23use crate::{
4 models::{BlockchainTest, ForkSpec},
5Case, 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};
1819/// A handler for the blockchain test suite.
20#[derive(Debug)]
21pub struct BlockchainTests {
22 suite: String,
23}
2425impl BlockchainTests {
26/// Create a new handler for a subset of the blockchain test suite.
27pub const fn new(suite: String) -> Self {
28Self { suite }
29 }
30}
3132impl Suitefor BlockchainTests {
33type Case = BlockchainTestCase;
3435fn suite_name(&self) -> String {
36format!("BlockchainTests/{}", self.suite)
37 }
38}
3940/// An Ethereum blockchain test.
41#[derive(Debug, PartialEq, Eq)]
42pub struct BlockchainTestCase {
43 tests: BTreeMap<String, BlockchainTest>,
44 skip: bool,
45}
4647impl Casefor BlockchainTestCase {
48fn load(path: &Path) -> Result<Self, Error> {
49Ok(Self {
50 tests: {
51let s = fs::read_to_string(path)
52 .map_err(|error| Error::Io { path: path.into(), error })?;
53serde_json::from_str(&s)
54 .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
55},
56 skip: should_skip(path),
57 })
58 }
5960/// 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.
64fn run(&self) -> Result<(), Error> {
65// If the test is marked for skipping, return a Skipped error immediately.
66if self.skip {
67return Err(Error::Skipped)
68 }
6970// Iterate through test cases, filtering by the network type to exclude specific forks.
71self.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.
88let chain_spec: Arc<ChainSpec> = Arc::new(case.network.into());
89let provider = create_test_provider_factory_with_chain_spec(chain_spec.clone())
90 .database_provider_rw()
91 .unwrap();
9293// Insert initial test state into the provider.
94provider.insert_historical_block(
95 SealedBlock::<reth_primitives::Block>::from_sealed_parts(
96case.genesis_block_header.clone().into(),
97BlockBody::default(),
98 )
99 .try_recover()
100 .unwrap(),
101 )?;
102case.pre.write_to_db(provider.tx_ref())?;
103104// Initialize receipts static file with genesis
105{
106let static_file_provider = provider.static_file_provider();
107let mut receipts_writer =
108static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
109receipts_writer.increment_block(0).unwrap();
110receipts_writer.commit_without_sync_all().unwrap();
111 }
112113// Decode and insert blocks, creating a chain of blocks for the test case.
114let last_block = case.blocks.iter().try_fold(None, |_, block| {
115let decoded =
116 SealedBlock::<reth_primitives::Block>::decode(&mut block.rlp.as_ref())?;
117provider.insert_historical_block(decoded.clone().try_recover().unwrap())?;
118Ok::<Option<SealedBlock>, Error>(Some(decoded))
119 })?;
120provider121 .static_file_provider()
122 .latest_writer(StaticFileSegment::Headers)
123 .unwrap()
124 .commit_without_sync_all()
125 .unwrap();
126127// Execute the execution stage using the EVM processor factory for the test case
128 // network.
129let _ = ExecutionStage::new_with_executor(
130 reth_evm_ethereum::execute::EthExecutorProvider::ethereum(chain_spec.clone()),
131Arc::new(EthBeaconConsensus::new(chain_spec)),
132 )
133 .execute(
134&provider,
135 ExecInput { target: last_block.as_ref().map(|b| b.number), checkpoint: None },
136 );
137138// Validate the post-state for the test case.
139match (&case.post_state, &case.post_state_hash) {
140 (Some(state), None) => {
141// Validate accounts in the state against the provider's database.
142for (&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.
148let last_block = last_block.unwrap_or_default();
149provider.insert_hashes(
1500..=last_block.number,
151last_block.hash(),
152*expected_state_root,
153 )?;
154 }
155_ => return Err(Error::MissingPostState),
156 }
157158// Drop the provider without committing to the database.
159drop(provider);
160Ok(())
161 })?;
162163Ok(())
164 }
165}
166167/// 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 {
174let path_str = path.to_str().expect("Path is not valid UTF-8");
175let name = path.file_name().unwrap().to_str().unwrap();
176matches!(
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"
182183// txbyte is of type 02 and we don't parse tx bytes for this test to fail.
184| "typeTwoBerlin.json"
185186// 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"
190191// Test check if gas price overflows, we handle this correctly but does not match tests specific
192 // exception.
193| "HighGasPrice.json"
194| "HighGasPriceParis.json"
195196// 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"
203204// 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"
211212// 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}
227228/// `str::contains` but for a path. Takes into account the OS path separator (`/` or `\`).
229fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
230let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
231path_str.contains(&rhs)
232}