1use crate::{
4 models::{BlockchainTest, ForkSpec},
5 Case, Error, Suite,
6};
7use alloy_rlp::Decodable;
8use rayon::iter::{IndexedParallelIterator, 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;
14use reth_evm::{execute::Executor, ConfigureEvm};
15use reth_evm_ethereum::EthEvmConfig;
16use reth_primitives_traits::{ParallelBridgeBuffered, RecoveredBlock, SealedBlock};
17use reth_provider::{
18 test_utils::create_test_provider_factory_with_chain_spec, BlockWriter, DatabaseProviderFactory,
19 ExecutionOutcome, HistoryWriter, OriginalValuesKnown, StateWriteConfig, StateWriter,
20 StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
21};
22use reth_revm::database::StateProviderDatabase;
23use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot};
24use reth_trie_db::DatabaseStateRoot;
25use std::{
26 collections::BTreeMap,
27 fs,
28 path::{Path, PathBuf},
29 sync::Arc,
30};
31
32#[derive(Debug)]
34pub struct BlockchainTests {
35 suite_path: PathBuf,
36}
37
38impl BlockchainTests {
39 pub const fn new(suite_path: PathBuf) -> Self {
41 Self { suite_path }
42 }
43}
44
45impl Suite for BlockchainTests {
46 type Case = BlockchainTestCase;
47
48 fn suite_path(&self) -> &Path {
49 &self.suite_path
50 }
51}
52
53#[derive(Debug, PartialEq, Eq)]
55pub struct BlockchainTestCase {
56 pub tests: BTreeMap<String, BlockchainTest>,
58 pub skip: bool,
60}
61
62impl BlockchainTestCase {
63 const fn excluded_fork(network: ForkSpec) -> bool {
65 matches!(
66 network,
67 ForkSpec::ByzantiumToConstantinopleAt5 |
68 ForkSpec::Constantinople |
69 ForkSpec::ConstantinopleFix |
70 ForkSpec::MergeEOF |
71 ForkSpec::MergeMeterInitCode |
72 ForkSpec::MergePush0
73 )
74 }
75
76 #[inline]
82 fn is_uncle_sidechain_case(name: &str) -> bool {
83 name.contains("UncleFromSideChain")
84 }
85
86 #[inline]
93 fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
94 case.blocks.iter().enumerate().find_map(|(idx, blk)| {
95 blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
96 })
97 }
98
99 pub fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
102 let expectation = Self::expected_failure(case);
103 match run_case(case) {
104 Ok(()) => {
106 if let Some((block, msg)) = expectation {
108 Err(Error::Assertion(format!(
109 "Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
110 )))
111 } else {
112 Ok(())
113 }
114 }
115
116 Err(Error::BlockProcessingFailed { block_number, err }) => {
118 match expectation {
119 Some((expected, _)) if block_number == expected => Ok(()),
121
122 _ if Self::is_uncle_sidechain_case(name) => Ok(()),
125
126 Some((expected, _)) => Err(Error::Assertion(format!(
128 "Test case: {name}\nExpected failure at block {expected}\nGot failure at block {block_number}",
129 ))),
130
131 None => Err(Error::BlockProcessingFailed { block_number, err }),
133 }
134 }
135
136 Err(other) => Err(other),
144 }
145 }
146}
147
148impl Case for BlockchainTestCase {
149 fn load(path: &Path) -> Result<Self, Error> {
150 Ok(Self {
151 tests: {
152 let s = fs::read_to_string(path)
153 .map_err(|error| Error::Io { path: path.into(), error })?;
154 serde_json::from_str(&s)
155 .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
156 },
157 skip: should_skip(path),
158 })
159 }
160
161 fn run(self) -> Result<(), Error> {
166 if self.skip {
168 return Err(Error::Skipped);
169 }
170
171 self.tests
173 .into_iter()
174 .filter(|(_, case)| !Self::excluded_fork(case.network))
175 .par_bridge_buffered()
176 .with_min_len(64)
177 .try_for_each(|(name, case)| Self::run_single_case(&name, &case).map(|_| ()))
178 }
179}
180
181fn run_case(case: &BlockchainTest) -> Result<(), Error> {
194 let chain_spec = case.network.to_chain_spec();
196 let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
197 let provider = factory.database_provider_rw().unwrap();
198
199 let genesis_block = SealedBlock::<Block>::from_sealed_parts(
201 case.genesis_block_header.clone().into(),
202 Default::default(),
203 )
204 .try_recover()
205 .unwrap();
206
207 provider.insert_block(&genesis_block).map_err(|err| Error::block_failed(0, err))?;
208
209 provider
211 .static_file_provider()
212 .latest_writer(StaticFileSegment::Receipts)
213 .and_then(|mut writer| writer.increment_block(0))
214 .map_err(|err| Error::block_failed(0, err))?;
215
216 let genesis_state = case.pre.clone().into_genesis_state();
217 insert_genesis_state(&provider, genesis_state.iter())
218 .map_err(|err| Error::block_failed(0, err))?;
219 insert_genesis_hashes(&provider, genesis_state.iter())
220 .map_err(|err| Error::block_failed(0, err))?;
221 insert_genesis_history(&provider, genesis_state.iter())
222 .map_err(|err| Error::block_failed(0, err))?;
223
224 let blocks = decode_blocks(&case.blocks)?;
226
227 let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
228 let mut parent = genesis_block;
229
230 for (block_index, block) in blocks.iter().enumerate() {
231 let block_number = (block_index + 1) as u64;
233
234 provider.insert_block(block).map_err(|err| Error::block_failed(block_number, err))?;
236 provider
237 .static_file_provider()
238 .commit()
239 .map_err(|err| Error::block_failed(block_number, err))?;
240
241 pre_execution_checks(chain_spec.clone(), &parent, block)
243 .map_err(|err| Error::block_failed(block_number, err))?;
244
245 let state_provider = provider.latest();
247 let state_db = StateProviderDatabase(&state_provider);
248 let executor = executor_provider.batch_executor(state_db);
249
250 let output = executor
251 .execute(&(*block).clone())
252 .map_err(|err| Error::block_failed(block_number, err))?;
253
254 validate_block_post_execution(block, &chain_spec, &output.receipts, &output.requests, None)
256 .map_err(|err| Error::block_failed(block_number, err))?;
257
258 let hashed_state =
260 HashedPostState::from_bundle_state::<KeccakKeyHasher>(output.state.state());
261 let (computed_state_root, _) = StateRoot::overlay_root_with_updates(
262 provider.tx_ref(),
263 &hashed_state.clone_into_sorted(),
264 )
265 .map_err(|err| Error::block_failed(block_number, err))?;
266 if computed_state_root != block.state_root {
267 return Err(Error::block_failed(
268 block_number,
269 Error::Assertion("state root mismatch".to_string()),
270 ));
271 }
272
273 provider
275 .write_state(
276 &ExecutionOutcome::single(block.number, output),
277 OriginalValuesKnown::Yes,
278 StateWriteConfig::default(),
279 )
280 .map_err(|err| Error::block_failed(block_number, err))?;
281
282 provider
283 .write_hashed_state(&hashed_state.into_sorted())
284 .map_err(|err| Error::block_failed(block_number, err))?;
285 provider
286 .update_history_indices(block.number..=block.number)
287 .map_err(|err| Error::block_failed(block_number, err))?;
288
289 parent = block.clone()
291 }
292
293 match &case.post_state {
294 Some(expected_post_state) => {
295 for (address, account) in expected_post_state {
305 account.assert_db(*address, provider.tx_ref())?;
306 }
307 }
308 None => {
309 }
312 }
313
314 Ok(())
315}
316
317fn decode_blocks(
318 test_case_blocks: &[crate::models::Block],
319) -> Result<Vec<RecoveredBlock<Block>>, Error> {
320 let mut blocks = Vec::with_capacity(test_case_blocks.len());
321 for (block_index, block) in test_case_blocks.iter().enumerate() {
322 let block_number = (block_index + 1) as u64;
325
326 let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
327 .map_err(|err| Error::block_failed(block_number, err))?;
328
329 let recovered_block =
330 decoded.clone().try_recover().map_err(|err| Error::block_failed(block_number, err))?;
331
332 blocks.push(recovered_block);
333 }
334
335 Ok(blocks)
336}
337
338fn pre_execution_checks(
339 chain_spec: Arc<ChainSpec>,
340 parent: &RecoveredBlock<Block>,
341 block: &RecoveredBlock<Block>,
342) -> Result<(), Error> {
343 let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
344
345 let sealed_header = block.sealed_header();
346
347 <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
348 &consensus,
349 block.body(),
350 sealed_header,
351 )?;
352 consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
353 consensus.validate_header(sealed_header)?;
354 consensus.validate_block_pre_execution(block)?;
355
356 Ok(())
357}
358
359pub fn should_skip(path: &Path) -> bool {
366 let path_str = path.to_str().expect("Path is not valid UTF-8");
367 let name = path.file_name().unwrap().to_str().unwrap();
368 matches!(
369 name,
370 | "ValueOverflow.json"
373 | "ValueOverflowParis.json"
374
375 | "typeTwoBerlin.json"
377
378 | "CreateTransactionHighNonce.json"
382
383 | "HighGasPrice.json"
386 | "HighGasPriceParis.json"
387
388 | "accessListExample.json"
392 | "basefeeExample.json"
393 | "eip1559.json"
394 | "mergeTest.json"
395
396 | "loopExp.json"
398 | "Call50000_sha256.json"
399 | "static_Call50000_sha256.json"
400 | "loopMul.json"
401 | "CALLBlake2f_MaxRounds.json"
402 | "shiftCombinations.json"
403
404 | "RevertInCreateInInit_Paris.json"
406 | "RevertInCreateInInit.json"
407 | "dynamicAccountOverwriteEmpty.json"
408 | "dynamicAccountOverwriteEmpty_Paris.json"
409 | "RevertInCreateInInitCreate2Paris.json"
410 | "create2collisionStorage.json"
411 | "RevertInCreateInInitCreate2.json"
412 | "create2collisionStorageParis.json"
413 | "InitCollision.json"
414 | "InitCollisionParis.json"
415 )
416 || path_contains(path_str, &["EIPTests", "stEOF"])
418}
419
420fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
422 let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
423 path_str.contains(&rhs)
424}