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, HashedPostStateProvider, HistoryWriter, OriginalValuesKnown,
20 StateWriteConfig, StateWriter, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
21 StorageSettingsCache,
22};
23use reth_revm::database::StateProviderDatabase;
24use reth_trie::StateRoot;
25use reth_trie_db::DatabaseStateRoot;
26use std::{
27 collections::BTreeMap,
28 fs,
29 path::{Path, PathBuf},
30 sync::Arc,
31};
32
33#[derive(Debug)]
35pub struct BlockchainTests {
36 suite_path: PathBuf,
37}
38
39impl BlockchainTests {
40 pub const fn new(suite_path: PathBuf) -> Self {
42 Self { suite_path }
43 }
44}
45
46impl Suite for BlockchainTests {
47 type Case = BlockchainTestCase;
48
49 fn suite_path(&self) -> &Path {
50 &self.suite_path
51 }
52}
53
54#[derive(Debug, PartialEq, Eq)]
56pub struct BlockchainTestCase {
57 pub tests: BTreeMap<String, BlockchainTest>,
59 pub skip: bool,
61}
62
63impl BlockchainTestCase {
64 const fn excluded_fork(network: ForkSpec) -> bool {
66 matches!(
67 network,
68 ForkSpec::ByzantiumToConstantinopleAt5 |
69 ForkSpec::Constantinople |
70 ForkSpec::ConstantinopleFix |
71 ForkSpec::MergeEOF |
72 ForkSpec::MergeMeterInitCode |
73 ForkSpec::MergePush0
74 )
75 }
76
77 #[inline]
83 fn is_uncle_sidechain_case(name: &str) -> bool {
84 name.contains("UncleFromSideChain")
85 }
86
87 #[inline]
94 fn expected_failure(case: &BlockchainTest) -> Option<(u64, String)> {
95 case.blocks.iter().enumerate().find_map(|(idx, blk)| {
96 blk.expect_exception.as_ref().map(|msg| ((idx + 1) as u64, msg.clone()))
97 })
98 }
99
100 pub fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
103 let expectation = Self::expected_failure(case);
104 match run_case(case) {
105 Ok(()) => {
107 if let Some((block, msg)) = expectation {
109 Err(Error::Assertion(format!(
110 "Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
111 )))
112 } else {
113 Ok(())
114 }
115 }
116
117 Err(Error::BlockProcessingFailed { block_number, err }) => {
119 match expectation {
120 Some((expected, _)) if block_number == expected => Ok(()),
122
123 _ if Self::is_uncle_sidechain_case(name) => Ok(()),
126
127 Some((expected, _)) => Err(Error::Assertion(format!(
129 "Test case: {name}\nExpected failure at block {expected}\nGot failure at block {block_number}",
130 ))),
131
132 None => Err(Error::BlockProcessingFailed { block_number, err }),
134 }
135 }
136
137 Err(other) => Err(other),
145 }
146 }
147}
148
149impl Case for BlockchainTestCase {
150 fn load(path: &Path) -> Result<Self, Error> {
151 Ok(Self {
152 tests: {
153 let s = fs::read_to_string(path)
154 .map_err(|error| Error::Io { path: path.into(), error })?;
155 serde_json::from_str(&s)
156 .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })?
157 },
158 skip: should_skip(path),
159 })
160 }
161
162 fn run(self) -> Result<(), Error> {
167 if self.skip {
169 return Err(Error::Skipped);
170 }
171
172 self.tests
174 .into_iter()
175 .filter(|(_, case)| !Self::excluded_fork(case.network))
176 .par_bridge_buffered()
177 .with_min_len(64)
178 .try_for_each(|(name, case)| Self::run_single_case(&name, &case).map(|_| ()))
179 }
180}
181
182fn run_case(case: &BlockchainTest) -> Result<(), Error> {
195 let chain_spec = case.network.to_chain_spec();
197 let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
198 let provider = factory.database_provider_rw().unwrap();
199
200 let genesis_block = SealedBlock::<Block>::from_sealed_parts(
202 case.genesis_block_header.clone().into(),
203 Default::default(),
204 )
205 .try_recover()
206 .unwrap();
207
208 provider.insert_block(&genesis_block).map_err(|err| Error::block_failed(0, err))?;
209
210 provider
212 .static_file_provider()
213 .latest_writer(StaticFileSegment::Receipts)
214 .and_then(|mut writer| writer.increment_block(0))
215 .map_err(|err| Error::block_failed(0, err))?;
216
217 let genesis_state = case.pre.clone().into_genesis_state();
218 insert_genesis_state(&provider, genesis_state.iter())
219 .map_err(|err| Error::block_failed(0, err))?;
220 insert_genesis_hashes(&provider, genesis_state.iter())
221 .map_err(|err| Error::block_failed(0, err))?;
222 insert_genesis_history(&provider, genesis_state.iter())
223 .map_err(|err| Error::block_failed(0, err))?;
224
225 let blocks = decode_blocks(&case.blocks)?;
227
228 let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
229 let mut parent = genesis_block;
230
231 for (block_index, block) in blocks.iter().enumerate() {
232 let block_number = (block_index + 1) as u64;
234
235 provider.insert_block(block).map_err(|err| Error::block_failed(block_number, err))?;
237 provider
238 .static_file_provider()
239 .commit()
240 .map_err(|err| Error::block_failed(block_number, err))?;
241
242 pre_execution_checks(chain_spec.clone(), &parent, block)
244 .map_err(|err| Error::block_failed(block_number, err))?;
245
246 let state_provider = provider.latest();
248 let state_db = StateProviderDatabase(&state_provider);
249 let executor = executor_provider.batch_executor(state_db);
250
251 let output = executor
252 .execute(&(*block).clone())
253 .map_err(|err| Error::block_failed(block_number, err))?;
254
255 validate_block_post_execution(block, &chain_spec, &output, None, None)
257 .map_err(|err| Error::block_failed(block_number, err))?;
258
259 let hashed_state = state_provider
261 .hashed_post_state(&output.state)
262 .map_err(|err| Error::block_failed(block_number, err))?;
263 let sorted = hashed_state.clone_into_sorted();
264 let (computed_state_root, _) = reth_trie_db::with_adapter!(provider, |A| {
265 StateRoot::<reth_trie_db::DatabaseTrieCursorFactory<_, A>, _>::overlay_root_with_updates(
266 provider.tx_ref(),
267 &sorted,
268 )
269 })
270 .map_err(|err| Error::block_failed(block_number, err))?;
271 if computed_state_root != block.state_root {
272 return Err(Error::block_failed(
273 block_number,
274 Error::Assertion("state root mismatch".to_string()),
275 ));
276 }
277
278 provider
280 .write_state(
281 &ExecutionOutcome::single(block.number, output),
282 OriginalValuesKnown::Yes,
283 StateWriteConfig::default(),
284 )
285 .map_err(|err| Error::block_failed(block_number, err))?;
286
287 provider
288 .write_hashed_state(&hashed_state.into_sorted())
289 .map_err(|err| Error::block_failed(block_number, err))?;
290 provider
291 .update_history_indices(block.number..=block.number)
292 .map_err(|err| Error::block_failed(block_number, err))?;
293
294 parent = block.clone()
296 }
297
298 match &case.post_state {
299 Some(expected_post_state) => {
300 for (address, account) in expected_post_state {
310 account.assert_db(*address, provider.tx_ref())?;
311 }
312 }
313 None => {
314 }
317 }
318
319 Ok(())
320}
321
322fn decode_blocks(
323 test_case_blocks: &[crate::models::Block],
324) -> Result<Vec<RecoveredBlock<Block>>, Error> {
325 let mut blocks = Vec::with_capacity(test_case_blocks.len());
326 for (block_index, block) in test_case_blocks.iter().enumerate() {
327 let block_number = (block_index + 1) as u64;
330
331 let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
332 .map_err(|err| Error::block_failed(block_number, err))?;
333
334 let recovered_block =
335 decoded.clone().try_recover().map_err(|err| Error::block_failed(block_number, err))?;
336
337 blocks.push(recovered_block);
338 }
339
340 Ok(blocks)
341}
342
343fn pre_execution_checks(
344 chain_spec: Arc<ChainSpec>,
345 parent: &RecoveredBlock<Block>,
346 block: &RecoveredBlock<Block>,
347) -> Result<(), Error> {
348 let consensus: EthBeaconConsensus<ChainSpec> = EthBeaconConsensus::new(chain_spec);
349
350 let sealed_header = block.sealed_header();
351
352 <EthBeaconConsensus<ChainSpec> as Consensus<Block>>::validate_body_against_header(
353 &consensus,
354 block.body(),
355 sealed_header,
356 )?;
357 consensus.validate_header_against_parent(sealed_header, parent.sealed_header())?;
358 consensus.validate_header(sealed_header)?;
359 consensus.validate_block_pre_execution(block)?;
360
361 Ok(())
362}
363
364pub fn should_skip(path: &Path) -> bool {
371 let path_str = path.to_str().expect("Path is not valid UTF-8");
372 let name = path.file_name().unwrap().to_str().unwrap();
373 matches!(
374 name,
375 | "ValueOverflow.json"
378 | "ValueOverflowParis.json"
379
380 | "typeTwoBerlin.json"
382
383 | "CreateTransactionHighNonce.json"
387
388 | "HighGasPrice.json"
391 | "HighGasPriceParis.json"
392
393 | "accessListExample.json"
397 | "basefeeExample.json"
398 | "eip1559.json"
399 | "mergeTest.json"
400
401 | "loopExp.json"
403 | "Call50000_sha256.json"
404 | "static_Call50000_sha256.json"
405 | "loopMul.json"
406 | "CALLBlake2f_MaxRounds.json"
407 | "shiftCombinations.json"
408
409 | "RevertInCreateInInit_Paris.json"
411 | "RevertInCreateInInit.json"
412 | "dynamicAccountOverwriteEmpty.json"
413 | "dynamicAccountOverwriteEmpty_Paris.json"
414 | "RevertInCreateInInitCreate2Paris.json"
415 | "create2collisionStorage.json"
416 | "RevertInCreateInInitCreate2.json"
417 | "create2collisionStorageParis.json"
418 | "InitCollision.json"
419 | "InitCollisionParis.json"
420 )
421 || path_contains(path_str, &["EIPTests", "stEOF"])
423}
424
425fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
427 let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
428 path_str.contains(&rhs)
429}