1use crate::{
4 models::{BlockchainTest, ForkSpec},
5 Case, Error, Suite,
6};
7use alloy_eip7928::bal::Bal;
8use alloy_eips::{
9 eip7002::WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, eip7251::CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
10};
11use alloy_rlp::Decodable;
12use rayon::iter::{IndexedParallelIterator, ParallelIterator};
13use reth_chainspec::{ChainSpec, EthereumHardforks};
14use reth_consensus::{Consensus, HeaderValidator};
15use reth_db_common::init::{insert_genesis_hashes, insert_genesis_history, insert_genesis_state};
16use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
17use reth_ethereum_primitives::Block;
18use reth_evm::{
19 database::StateProviderDatabase,
20 execute::{BlockExecutionOutput, Executor},
21 ConfigureEvm,
22};
23use reth_evm_ethereum::EthEvmConfig;
24use reth_primitives_traits::{ParallelBridgeBuffered, RecoveredBlock, SealedBlock};
25use reth_provider::{
26 providers::{RocksDBBuilder, RocksDBProvider, StaticFileProviderBuilder},
27 test_utils::MockNodeTypesWithDB,
28 BlockWriter, DatabaseProviderFactory, ExecutionOutcome, HashedPostStateProvider, HistoryWriter,
29 OriginalValuesKnown, ProviderFactory, StateProvider, StateWriteConfig, StateWriter,
30 StaticFileProviderFactory, StaticFileSegment, StaticFileWriter, StorageSettingsCache,
31 TrieWriter,
32};
33use reth_tasks::Runtime;
34use reth_trie::StateRoot;
35use reth_trie_db::DatabaseStateRoot;
36use std::{
37 collections::BTreeMap,
38 fs,
39 path::{Path, PathBuf},
40 sync::{Arc, LazyLock},
41};
42
43#[derive(Debug)]
45pub struct BlockchainTests {
46 suite_path: PathBuf,
47}
48
49impl BlockchainTests {
50 pub const fn new(suite_path: PathBuf) -> Self {
52 Self { suite_path }
53 }
54}
55
56impl Suite for BlockchainTests {
57 type Case = BlockchainTestCase;
58
59 fn suite_path(&self) -> &Path {
60 &self.suite_path
61 }
62}
63
64#[derive(Debug, PartialEq, Eq)]
66pub struct BlockchainTestCase {
67 pub tests: BTreeMap<String, BlockchainTest>,
69 pub skip: bool,
71}
72
73impl BlockchainTestCase {
74 const fn excluded_fork(network: ForkSpec) -> bool {
76 matches!(
77 network,
78 ForkSpec::ByzantiumToConstantinopleAt5 |
79 ForkSpec::Constantinople |
80 ForkSpec::ConstantinopleFix |
81 ForkSpec::MergeEOF |
82 ForkSpec::MergeMeterInitCode |
83 ForkSpec::MergePush0
84 )
85 }
86
87 fn lacks_request_contracts(case: &BlockchainTest) -> bool {
91 case.network.to_chain_spec().is_prague_active_at_timestamp(u64::MAX) &&
92 [WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS]
93 .into_iter()
94 .any(|address| {
95 case.pre.get(&address).is_none_or(|account| account.code.is_empty())
96 })
97 }
98
99 #[inline]
105 fn is_uncle_sidechain_case(name: &str) -> bool {
106 name.contains("UncleFromSideChain")
107 }
108
109 #[inline]
116 fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
117 case.blocks.iter().enumerate().find_map(|(idx, blk)| {
118 blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
119 })
120 }
121
122 pub fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
125 let expectation = Self::expected_failure(case);
126 match run_case(case) {
127 Ok(()) => {
129 if let Some((block, msg)) = expectation {
131 Err(Error::Assertion(format!(
132 "Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
133 )))
134 } else {
135 Ok(())
136 }
137 }
138
139 Err(Error::BlockProcessingFailed { block_number, err }) => {
141 match expectation {
142 Some((expected, _)) if block_number == expected => Ok(()),
144
145 _ if Self::is_uncle_sidechain_case(name) => Ok(()),
148
149 Some((expected, _)) => Err(Error::Assertion(format!(
151 "Test case: {name}\nExpected failure at block {expected}\nGot failure at block {block_number}",
152 ))),
153
154 None => Err(Error::BlockProcessingFailed { block_number, err }),
156 }
157 }
158
159 Err(other) => Err(other),
167 }
168 }
169}
170
171impl Case for BlockchainTestCase {
172 fn load(path: &Path) -> Result<Self, Error> {
173 Ok(Self {
174 tests: {
175 let s = fs::read_to_string(path)
176 .map_err(|error| Error::Io { path: path.into(), error })?;
177 serde_json::from_str(&s)
178 .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
179 },
180 skip: should_skip(path),
181 })
182 }
183
184 fn run(self) -> Result<(), Error> {
189 if self.skip {
191 return Err(Error::Skipped);
192 }
193
194 self.tests
196 .into_iter()
197 .filter(|(_, case)| {
198 !Self::excluded_fork(case.network) && !Self::lacks_request_contracts(case)
199 })
200 .par_bridge_buffered()
201 .with_min_len(64)
202 .try_for_each(|(name, case)| Self::run_single_case(&name, &case).map(|_| ()))
203 }
204}
205
206fn run_case(case: &BlockchainTest) -> Result<(), Error> {
219 let chain_spec = case.network.to_chain_spec();
221 let factory = create_test_provider_factory(chain_spec.clone());
222 let provider = factory.database_provider_rw().unwrap();
223
224 let genesis_block = SealedBlock::<Block>::from_sealed_parts(
226 case.genesis_block_header.clone().into(),
227 Default::default(),
228 )
229 .try_recover()
230 .unwrap();
231
232 provider.insert_block(&genesis_block).map_err(|err| Error::block_failed(0, err))?;
233
234 provider
236 .static_file_provider()
237 .latest_writer(StaticFileSegment::Receipts)
238 .and_then(|mut writer| writer.increment_block(0))
239 .map_err(|err| Error::block_failed(0, err))?;
240
241 let genesis_state = case.pre.clone().into_genesis_state();
242 insert_genesis_state(&provider, genesis_state.iter())
243 .map_err(|err| Error::block_failed(0, err))?;
244 insert_genesis_hashes(&provider, genesis_state.iter())
245 .map_err(|err| Error::block_failed(0, err))?;
246 insert_genesis_history(&provider, genesis_state.iter())
247 .map_err(|err| Error::block_failed(0, err))?;
248
249 let (_, trie_updates) = reth_trie_db::with_adapter!(provider, |A| {
251 StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::from_tx(provider.tx_ref())
252 .root_with_updates()
253 })
254 .map_err(|err| Error::block_failed(0, err))?;
255 provider.write_trie_updates(trie_updates).map_err(|err| Error::block_failed(0, err))?;
256
257 let blocks = decode_blocks(&case.blocks)?;
259
260 let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
261 let mut parent = genesis_block;
262 let mut bal_buf = Vec::new();
263
264 for (block_index, block) in blocks.iter().enumerate() {
265 let block_number = (block_index + 1) as u64;
267
268 provider.insert_block(block).map_err(|err| Error::block_failed(block_number, err))?;
270 provider
271 .static_file_provider()
272 .commit()
273 .map_err(|err| Error::block_failed(block_number, err))?;
274
275 pre_execution_checks(chain_spec.clone(), &parent, block)
277 .map_err(|err| Error::block_failed(block_number, err))?;
278
279 let state_provider = provider.latest();
281 let state_db = StateProviderDatabase((&state_provider).into_evm_state_provider());
282 let mut executor = executor_provider.batch_executor(state_db);
283
284 let result = executor
285 .execute_one(&(*block).clone())
286 .map_err(|err| Error::block_failed(block_number, err))?;
287 let block_access_list_hash = executor
290 .take_bal()
291 .map(|bal| {
292 let bal = Bal::from(bal);
293 bal.validate_gas_limit(block.gas_limit)
294 .map(|()| bal.compute_hash_with_buf(&mut bal_buf))
295 })
296 .transpose()
297 .map_err(|err| Error::block_failed(block_number, err))?;
298 let output = BlockExecutionOutput { state: executor.into_state(), result };
299
300 validate_block_post_execution(block, &chain_spec, &output, None, block_access_list_hash)
302 .map_err(|err| Error::block_failed(block_number, err))?;
303
304 let hashed_state = state_provider
306 .hashed_post_state(&output.state)
307 .map_err(|err| Error::block_failed(block_number, err))?;
308 let sorted = hashed_state.clone_into_sorted();
309 let (computed_state_root, trie_updates) = reth_trie_db::with_adapter!(provider, |A| {
310 StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::overlay_root_with_updates(
311 provider.tx_ref(),
312 &sorted,
313 )
314 })
315 .map_err(|err| Error::block_failed(block_number, err))?;
316 if computed_state_root != block.state_root {
317 return Err(Error::block_failed(
318 block_number,
319 Error::Assertion("state root mismatch".to_string()),
320 ));
321 }
322
323 provider
325 .write_state(
326 &ExecutionOutcome::single(block.number, output),
327 OriginalValuesKnown::Yes,
328 StateWriteConfig::default(),
329 )
330 .map_err(|err| Error::block_failed(block_number, err))?;
331
332 provider
333 .write_hashed_state(&hashed_state.into_sorted())
334 .map_err(|err| Error::block_failed(block_number, err))?;
335 provider
337 .write_trie_updates(trie_updates)
338 .map_err(|err| Error::block_failed(block_number, err))?;
339 provider
340 .update_history_indices(block.number..=block.number)
341 .map_err(|err| Error::block_failed(block_number, err))?;
342
343 parent = block.clone()
345 }
346
347 match &case.post_state {
348 Some(expected_post_state) => {
349 for (address, account) in expected_post_state {
359 account.assert_db(*address, provider.tx_ref())?;
360 }
361 }
362 None => {
363 }
366 }
367
368 Ok(())
369}
370
371fn decode_blocks(
372 test_case_blocks: &[crate::models::Block],
373) -> Result<Vec<RecoveredBlock<Block>>, Error> {
374 let mut blocks = Vec::with_capacity(test_case_blocks.len());
375 for (block_index, block) in test_case_blocks.iter().enumerate() {
376 let block_number = (block_index + 1) as u64;
379
380 let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
381 .map_err(|err| Error::block_failed(block_number, err))?;
382
383 let recovered_block =
384 decoded.try_recover().map_err(|err| Error::block_failed(block_number, err))?;
385
386 blocks.push(recovered_block);
387 }
388
389 Ok(blocks)
390}
391
392fn pre_execution_checks(
393 chain_spec: Arc<ChainSpec>,
394 parent: &RecoveredBlock<Block>,
395 block: &RecoveredBlock<Block>,
396) -> Result<(), Error> {
397 let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
398
399 let sealed_header = block.sealed_header();
400
401 <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
402 &consensus,
403 block.body(),
404 sealed_header,
405 )?;
406 consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
407 consensus.validate_header(sealed_header)?;
408 consensus.validate_block_pre_execution(block)?;
409
410 Ok(())
411}
412
413pub fn should_skip(path: &Path) -> bool {
420 let path_str = path.to_str().expect("Path is not valid UTF-8");
421 let name = path.file_name().unwrap().to_str().unwrap();
422 matches!(
423 name,
424 | "ValueOverflow.json"
427 | "ValueOverflowParis.json"
428
429 | "typeTwoBerlin.json"
431
432 | "CreateTransactionHighNonce.json"
436
437 | "HighGasPrice.json"
440 | "HighGasPriceParis.json"
441
442 | "accessListExample.json"
446 | "basefeeExample.json"
447 | "eip1559.json"
448 | "mergeTest.json"
449
450 | "loopExp.json"
452 | "Call50000_sha256.json"
453 | "static_Call50000_sha256.json"
454 | "loopMul.json"
455 | "CALLBlake2f_MaxRounds.json"
456 | "shiftCombinations.json"
457
458 | "RevertInCreateInInit_Paris.json"
460 | "RevertInCreateInInit.json"
461 | "dynamicAccountOverwriteEmpty.json"
462 | "dynamicAccountOverwriteEmpty_Paris.json"
463 | "RevertInCreateInInitCreate2Paris.json"
464 | "create2collisionStorage.json"
465 | "RevertInCreateInInitCreate2.json"
466 | "create2collisionStorageParis.json"
467 | "InitCollision.json"
468 | "InitCollisionParis.json"
469 )
470 || path_contains(path_str, &["EIPTests", "stEOF"])
472}
473
474fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
476 let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
477 path_str.contains(&rhs)
478}
479
480static SHARED_ROCKSDB: LazyLock<RocksDBProvider> = LazyLock::new(|| {
489 RocksDBBuilder::new(reth_db::test_utils::tempdir_path().join("rocksdb"))
490 .with_default_tables()
491 .build()
492 .expect("failed to create shared test RocksDB provider")
493});
494
495fn create_test_provider_factory(
501 chain_spec: Arc<ChainSpec>,
502) -> ProviderFactory<MockNodeTypesWithDB> {
503 let genesis_block_number = chain_spec.genesis.number.unwrap_or_default();
504
505 let datadir_path = reth_db::test_utils::tempdir_path();
508 let static_files_path = datadir_path.join("static_files");
509 fs::create_dir_all(&static_files_path).expect("failed to create static_files dir");
510
511 let db = reth_db::test_utils::create_test_rw_db_with_datadir(&datadir_path);
512
513 let factory = ProviderFactory::new(
514 db,
515 chain_spec,
516 StaticFileProviderBuilder::read_write(static_files_path)
517 .with_genesis_block_number(genesis_block_number)
518 .build()
519 .expect("static file provider"),
520 SHARED_ROCKSDB.clone(),
521 Runtime::test(),
522 )
523 .expect("failed to create test provider factory");
524
525 assert!(
526 !factory.cached_storage_settings().any_in_rocksdb(),
527 "the shared RocksDB is only valid while ef-tests run with v1 storage settings"
528 );
529
530 factory
531}