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}