1use crate::{
4 models::{BlockchainTest, ForkSpec},
5 Case, Error, Suite,
6};
7use alloy_eip7928::bal::Bal;
8use alloy_rlp::Decodable;
9use rayon::iter::{IndexedParallelIterator, ParallelIterator};
10use reth_chainspec::ChainSpec;
11use reth_consensus::{Consensus, HeaderValidator};
12use reth_db_common::init::{insert_genesis_hashes, insert_genesis_history, insert_genesis_state};
13use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
14use reth_ethereum_primitives::Block;
15use reth_evm::{
16 execute::{BlockExecutionOutput, Executor},
17 ConfigureEvm,
18};
19use reth_evm_ethereum::EthEvmConfig;
20use reth_primitives_traits::{ParallelBridgeBuffered, RecoveredBlock, SealedBlock};
21use reth_provider::{
22 providers::{RocksDBBuilder, RocksDBProvider, StaticFileProviderBuilder},
23 test_utils::MockNodeTypesWithDB,
24 BlockWriter, DatabaseProviderFactory, ExecutionOutcome, HashedPostStateProvider, HistoryWriter,
25 OriginalValuesKnown, ProviderFactory, StateProvider, StateWriteConfig, StateWriter,
26 StaticFileProviderFactory, StaticFileSegment, StaticFileWriter, StorageSettingsCache,
27 TrieWriter,
28};
29use reth_revm::database::StateProviderDatabase;
30use reth_tasks::Runtime;
31use reth_trie::StateRoot;
32use reth_trie_db::DatabaseStateRoot;
33use std::{
34 collections::BTreeMap,
35 fs,
36 path::{Path, PathBuf},
37 sync::{Arc, LazyLock},
38};
39
40#[derive(Debug)]
42pub struct BlockchainTests {
43 suite_path: PathBuf,
44}
45
46impl BlockchainTests {
47 pub const fn new(suite_path: PathBuf) -> Self {
49 Self { suite_path }
50 }
51}
52
53impl Suite for BlockchainTests {
54 type Case = BlockchainTestCase;
55
56 fn suite_path(&self) -> &Path {
57 &self.suite_path
58 }
59}
60
61#[derive(Debug, PartialEq, Eq)]
63pub struct BlockchainTestCase {
64 pub tests: BTreeMap<String, BlockchainTest>,
66 pub skip: bool,
68}
69
70impl BlockchainTestCase {
71 const fn excluded_fork(network: ForkSpec) -> bool {
73 matches!(
74 network,
75 ForkSpec::ByzantiumToConstantinopleAt5 |
76 ForkSpec::Constantinople |
77 ForkSpec::ConstantinopleFix |
78 ForkSpec::MergeEOF |
79 ForkSpec::MergeMeterInitCode |
80 ForkSpec::MergePush0
81 )
82 }
83
84 #[inline]
90 fn is_uncle_sidechain_case(name: &str) -> bool {
91 name.contains("UncleFromSideChain")
92 }
93
94 #[inline]
101 fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
102 case.blocks.iter().enumerate().find_map(|(idx, blk)| {
103 blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
104 })
105 }
106
107 pub fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
110 let expectation = Self::expected_failure(case);
111 match run_case(case) {
112 Ok(()) => {
114 if let Some((block, msg)) = expectation {
116 Err(Error::Assertion(format!(
117 "Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
118 )))
119 } else {
120 Ok(())
121 }
122 }
123
124 Err(Error::BlockProcessingFailed { block_number, err }) => {
126 match expectation {
127 Some((expected, _)) if block_number == expected => Ok(()),
129
130 _ if Self::is_uncle_sidechain_case(name) => Ok(()),
133
134 Some((expected, _)) => Err(Error::Assertion(format!(
136 "Test case: {name}\nExpected failure at block {expected}\nGot failure at block {block_number}",
137 ))),
138
139 None => Err(Error::BlockProcessingFailed { block_number, err }),
141 }
142 }
143
144 Err(other) => Err(other),
152 }
153 }
154}
155
156impl Case for BlockchainTestCase {
157 fn load(path: &Path) -> Result<Self, Error> {
158 Ok(Self {
159 tests: {
160 let s = fs::read_to_string(path)
161 .map_err(|error| Error::Io { path: path.into(), error })?;
162 serde_json::from_str(&s)
163 .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
164 },
165 skip: should_skip(path),
166 })
167 }
168
169 fn run(self) -> Result<(), Error> {
174 if self.skip {
176 return Err(Error::Skipped);
177 }
178
179 self.tests
181 .into_iter()
182 .filter(|(_, case)| !Self::excluded_fork(case.network))
183 .par_bridge_buffered()
184 .with_min_len(64)
185 .try_for_each(|(name, case)| Self::run_single_case(&name, &case).map(|_| ()))
186 }
187}
188
189fn run_case(case: &BlockchainTest) -> Result<(), Error> {
202 let chain_spec = case.network.to_chain_spec();
204 let factory = create_test_provider_factory(chain_spec.clone());
205 let provider = factory.database_provider_rw().unwrap();
206
207 let genesis_block = SealedBlock::<Block>::from_sealed_parts(
209 case.genesis_block_header.clone().into(),
210 Default::default(),
211 )
212 .try_recover()
213 .unwrap();
214
215 provider.insert_block(&genesis_block).map_err(|err| Error::block_failed(0, err))?;
216
217 provider
219 .static_file_provider()
220 .latest_writer(StaticFileSegment::Receipts)
221 .and_then(|mut writer| writer.increment_block(0))
222 .map_err(|err| Error::block_failed(0, err))?;
223
224 let genesis_state = case.pre.clone().into_genesis_state();
225 insert_genesis_state(&provider, genesis_state.iter())
226 .map_err(|err| Error::block_failed(0, err))?;
227 insert_genesis_hashes(&provider, genesis_state.iter())
228 .map_err(|err| Error::block_failed(0, err))?;
229 insert_genesis_history(&provider, genesis_state.iter())
230 .map_err(|err| Error::block_failed(0, err))?;
231
232 let (_, trie_updates) = reth_trie_db::with_adapter!(provider, |A| {
234 StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::from_tx(provider.tx_ref())
235 .root_with_updates()
236 })
237 .map_err(|err| Error::block_failed(0, err))?;
238 provider.write_trie_updates(trie_updates).map_err(|err| Error::block_failed(0, err))?;
239
240 let blocks = decode_blocks(&case.blocks)?;
242
243 let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
244 let mut parent = genesis_block;
245 let mut bal_buf = Vec::new();
246
247 for (block_index, block) in blocks.iter().enumerate() {
248 let block_number = (block_index + 1) as u64;
250
251 provider.insert_block(block).map_err(|err| Error::block_failed(block_number, err))?;
253 provider
254 .static_file_provider()
255 .commit()
256 .map_err(|err| Error::block_failed(block_number, err))?;
257
258 pre_execution_checks(chain_spec.clone(), &parent, block)
260 .map_err(|err| Error::block_failed(block_number, err))?;
261
262 let state_provider = provider.latest();
264 let state_db = StateProviderDatabase((&state_provider).into_evm_state_provider());
265 let mut executor = executor_provider.batch_executor(state_db);
266
267 let result = executor
268 .execute_one(&(*block).clone())
269 .map_err(|err| Error::block_failed(block_number, err))?;
270 let block_access_list_hash = executor
273 .take_bal()
274 .map(|bal| {
275 let bal = Bal::from(bal);
276 bal.validate_gas_limit(block.gas_limit)
277 .map(|()| bal.compute_hash_with_buf(&mut bal_buf))
278 })
279 .transpose()
280 .map_err(|err| Error::block_failed(block_number, err))?;
281 let output = BlockExecutionOutput { state: executor.into_state().take_bundle(), result };
282
283 validate_block_post_execution(block, &chain_spec, &output, None, block_access_list_hash)
285 .map_err(|err| Error::block_failed(block_number, err))?;
286
287 let hashed_state = state_provider
289 .hashed_post_state(&output.state)
290 .map_err(|err| Error::block_failed(block_number, err))?;
291 let sorted = hashed_state.clone_into_sorted();
292 let (computed_state_root, trie_updates) = reth_trie_db::with_adapter!(provider, |A| {
293 StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::overlay_root_with_updates(
294 provider.tx_ref(),
295 &sorted,
296 )
297 })
298 .map_err(|err| Error::block_failed(block_number, err))?;
299 if computed_state_root != block.state_root {
300 return Err(Error::block_failed(
301 block_number,
302 Error::Assertion("state root mismatch".to_string()),
303 ));
304 }
305
306 provider
308 .write_state(
309 &ExecutionOutcome::single(block.number, output),
310 OriginalValuesKnown::Yes,
311 StateWriteConfig::default(),
312 )
313 .map_err(|err| Error::block_failed(block_number, err))?;
314
315 provider
316 .write_hashed_state(&hashed_state.into_sorted())
317 .map_err(|err| Error::block_failed(block_number, err))?;
318 provider
320 .write_trie_updates(trie_updates)
321 .map_err(|err| Error::block_failed(block_number, err))?;
322 provider
323 .update_history_indices(block.number..=block.number)
324 .map_err(|err| Error::block_failed(block_number, err))?;
325
326 parent = block.clone()
328 }
329
330 match &case.post_state {
331 Some(expected_post_state) => {
332 for (address, account) in expected_post_state {
342 account.assert_db(*address, provider.tx_ref())?;
343 }
344 }
345 None => {
346 }
349 }
350
351 Ok(())
352}
353
354fn decode_blocks(
355 test_case_blocks: &[crate::models::Block],
356) -> Result<Vec<RecoveredBlock<Block>>, Error> {
357 let mut blocks = Vec::with_capacity(test_case_blocks.len());
358 for (block_index, block) in test_case_blocks.iter().enumerate() {
359 let block_number = (block_index + 1) as u64;
362
363 let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
364 .map_err(|err| Error::block_failed(block_number, err))?;
365
366 let recovered_block =
367 decoded.try_recover().map_err(|err| Error::block_failed(block_number, err))?;
368
369 blocks.push(recovered_block);
370 }
371
372 Ok(blocks)
373}
374
375fn pre_execution_checks(
376 chain_spec: Arc<ChainSpec>,
377 parent: &RecoveredBlock<Block>,
378 block: &RecoveredBlock<Block>,
379) -> Result<(), Error> {
380 let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
381
382 let sealed_header = block.sealed_header();
383
384 <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
385 &consensus,
386 block.body(),
387 sealed_header,
388 )?;
389 consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
390 consensus.validate_header(sealed_header)?;
391 consensus.validate_block_pre_execution(block)?;
392
393 Ok(())
394}
395
396pub fn should_skip(path: &Path) -> bool {
403 let path_str = path.to_str().expect("Path is not valid UTF-8");
404 let name = path.file_name().unwrap().to_str().unwrap();
405 matches!(
406 name,
407 | "ValueOverflow.json"
410 | "ValueOverflowParis.json"
411
412 | "typeTwoBerlin.json"
414
415 | "CreateTransactionHighNonce.json"
419
420 | "HighGasPrice.json"
423 | "HighGasPriceParis.json"
424
425 | "accessListExample.json"
429 | "basefeeExample.json"
430 | "eip1559.json"
431 | "mergeTest.json"
432
433 | "loopExp.json"
435 | "Call50000_sha256.json"
436 | "static_Call50000_sha256.json"
437 | "loopMul.json"
438 | "CALLBlake2f_MaxRounds.json"
439 | "shiftCombinations.json"
440
441 | "RevertInCreateInInit_Paris.json"
443 | "RevertInCreateInInit.json"
444 | "dynamicAccountOverwriteEmpty.json"
445 | "dynamicAccountOverwriteEmpty_Paris.json"
446 | "RevertInCreateInInitCreate2Paris.json"
447 | "create2collisionStorage.json"
448 | "RevertInCreateInInitCreate2.json"
449 | "create2collisionStorageParis.json"
450 | "InitCollision.json"
451 | "InitCollisionParis.json"
452 )
453 || path_contains(path_str, &["EIPTests", "stEOF"])
455}
456
457fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
459 let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
460 path_str.contains(&rhs)
461}
462
463static SHARED_ROCKSDB: LazyLock<RocksDBProvider> = LazyLock::new(|| {
472 RocksDBBuilder::new(reth_db::test_utils::tempdir_path().join("rocksdb"))
473 .with_default_tables()
474 .build()
475 .expect("failed to create shared test RocksDB provider")
476});
477
478fn create_test_provider_factory(
484 chain_spec: Arc<ChainSpec>,
485) -> ProviderFactory<MockNodeTypesWithDB> {
486 let genesis_block_number = chain_spec.genesis.number.unwrap_or_default();
487
488 let datadir_path = reth_db::test_utils::tempdir_path();
491 let static_files_path = datadir_path.join("static_files");
492 fs::create_dir_all(&static_files_path).expect("failed to create static_files dir");
493
494 let db = reth_db::test_utils::create_test_rw_db_with_datadir(&datadir_path);
495
496 let factory = ProviderFactory::new(
497 db,
498 chain_spec,
499 StaticFileProviderBuilder::read_write(static_files_path)
500 .with_genesis_block_number(genesis_block_number)
501 .build()
502 .expect("static file provider"),
503 SHARED_ROCKSDB.clone(),
504 Runtime::test(),
505 )
506 .expect("failed to create test provider factory");
507
508 assert!(
509 !factory.cached_storage_settings().any_in_rocksdb(),
510 "the shared RocksDB is only valid while ef-tests run with v1 storage settings"
511 );
512
513 factory
514}