1pub mod post_block;
2pub mod pre_block;
3
4use clap::Parser;
5
6use revm::{
7 bytecode::Bytecode,
8 context::{cfg::CfgEnv, ContextTr},
9 context_interface::{block::BlobExcessGasAndPrice, result::HaltReason},
10 database::{states::bundle_state::BundleRetention, EmptyDB, State},
11 handler::EvmTr,
12 inspector::inspectors::TracerEip3155,
13 primitives::{hardfork::SpecId, hex, Address, HashMap, U256},
14 state::AccountInfo,
15 Context, Database, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
16};
17use serde_json::json;
18use statetest_types::blockchain::{
19 Account, BlockchainTest, BlockchainTestCase, ForkSpec, Withdrawal,
20};
21use std::collections::BTreeMap;
22use std::fs;
23use std::path::{Path, PathBuf};
24use std::time::Instant;
25use thiserror::Error;
26use walkdir::{DirEntry, WalkDir};
27
28#[derive(Parser, Debug)]
30pub struct Cmd {
31 #[arg(required = true, num_args = 1..)]
37 paths: Vec<PathBuf>,
38 #[arg(long)]
40 omit_progress: bool,
41 #[arg(long, alias = "no-fail-fast")]
43 keep_going: bool,
44 #[arg(long)]
46 print_env_on_error: bool,
47 #[arg(long)]
49 json: bool,
50}
51
52impl Cmd {
53 pub fn run(&self) -> Result<(), Error> {
55 for path in &self.paths {
56 if !path.exists() {
57 return Err(Error::PathNotFound(path.clone()));
58 }
59
60 if !self.json {
61 println!("\nRunning blockchain tests in {}...", path.display());
62 }
63 let test_files = find_all_json_tests(path);
64
65 if test_files.is_empty() {
66 return Err(Error::NoJsonFiles(path.clone()));
67 }
68
69 run_tests(
70 test_files,
71 self.omit_progress,
72 self.keep_going,
73 self.print_env_on_error,
74 self.json,
75 )?;
76 }
77 Ok(())
78 }
79}
80
81pub fn find_all_json_tests(path: &Path) -> Vec<PathBuf> {
85 if path.is_file() {
86 vec![path.to_path_buf()]
87 } else {
88 WalkDir::new(path)
89 .into_iter()
90 .filter_map(Result::ok)
91 .filter(|e| e.path().extension() == Some("json".as_ref()))
92 .map(DirEntry::into_path)
93 .collect()
94 }
95}
96
97fn run_tests(
99 test_files: Vec<PathBuf>,
100 omit_progress: bool,
101 keep_going: bool,
102 print_env_on_error: bool,
103 json_output: bool,
104) -> Result<(), Error> {
105 let mut passed = 0;
106 let mut failed = 0;
107 let mut skipped = 0;
108 let mut failed_paths = Vec::new();
109
110 let start_time = Instant::now();
111 let total_files = test_files.len();
112
113 for (file_index, file_path) in test_files.into_iter().enumerate() {
114 let current_file = file_index + 1;
115 if skip_test(&file_path) {
116 skipped += 1;
117 if json_output {
118 let output = json!({
119 "file": file_path.display().to_string(),
120 "status": "skipped",
121 "reason": "known_issue"
122 });
123 println!("{}", serde_json::to_string(&output).unwrap());
124 } else if !omit_progress {
125 println!(
126 "Skipping ({}/{}): {}",
127 current_file,
128 total_files,
129 file_path.display()
130 );
131 }
132 continue;
133 }
134
135 let result = run_test_file(&file_path, json_output, print_env_on_error);
136
137 match result {
138 Ok(test_count) => {
139 passed += test_count;
140 if json_output {
141 } else if !omit_progress {
143 println!(
144 "ā ({}/{}) {} ({} tests)",
145 current_file,
146 total_files,
147 file_path.display(),
148 test_count
149 );
150 }
151 }
152 Err(e) => {
153 failed += 1;
154 if keep_going {
155 failed_paths.push(file_path.clone());
156 }
157 if json_output {
158 let output = json!({
159 "file": file_path.display().to_string(),
160 "error": e.to_string(),
161 "status": "failed"
162 });
163 println!("{}", serde_json::to_string(&output).unwrap());
164 } else if !omit_progress {
165 eprintln!(
166 "ā ({}/{}) {} - {}",
167 current_file,
168 total_files,
169 file_path.display(),
170 e
171 );
172 }
173
174 if !keep_going {
175 return Err(e);
176 }
177 }
178 }
179 }
180
181 let duration = start_time.elapsed();
182
183 if json_output {
184 let results = json!({
185 "summary": {
186 "passed": passed,
187 "failed": failed,
188 "skipped": skipped,
189 "duration_secs": duration.as_secs_f64(),
190 }
191 });
192 println!("{}", serde_json::to_string(&results).unwrap());
193 } else {
194 if keep_going && !failed_paths.is_empty() {
196 println!("\nFailed test files:");
197 for path in &failed_paths {
198 println!(" {}", path.display());
199 }
200 }
201
202 println!("\nTest results:");
203 println!(" Passed: {passed}");
204 println!(" Failed: {failed}");
205 println!(" Skipped: {skipped}");
206 println!(" Time: {:.2}s", duration.as_secs_f64());
207 }
208
209 if failed > 0 {
210 Err(Error::TestsFailed { failed })
211 } else {
212 Ok(())
213 }
214}
215
216fn run_test_file(
218 file_path: &Path,
219 json_output: bool,
220 print_env_on_error: bool,
221) -> Result<usize, Error> {
222 let content =
223 fs::read_to_string(file_path).map_err(|e| Error::FileRead(file_path.to_path_buf(), e))?;
224
225 let blockchain_test: BlockchainTest = serde_json::from_str(&content)
226 .map_err(|e| Error::JsonDecode(file_path.to_path_buf(), e))?;
227
228 let mut test_count = 0;
229
230 for (test_name, test_case) in blockchain_test.0 {
231 if json_output {
232 let output = json!({
234 "test": test_name,
235 "file": file_path.display().to_string(),
236 "status": "running"
237 });
238 println!("{}", serde_json::to_string(&output).unwrap());
239 } else {
240 println!(" Running: {test_name}");
241 }
242 let result = execute_blockchain_test(&test_case, print_env_on_error, json_output);
244
245 match result {
246 Ok(()) => {
247 if json_output {
248 let output = json!({
249 "test": test_name,
250 "file": file_path.display().to_string(),
251 "status": "passed"
252 });
253 println!("{}", serde_json::to_string(&output).unwrap());
254 }
255 test_count += 1;
256 }
257 Err(e) => {
258 if json_output {
259 let output = json!({
260 "test": test_name,
261 "file": file_path.display().to_string(),
262 "status": "failed",
263 "error": e.to_string()
264 });
265 println!("{}", serde_json::to_string(&output).unwrap());
266 }
267 return Err(Error::TestExecution {
268 test_name: test_name.clone(),
269 test_path: file_path.to_path_buf(),
270 error: e.to_string(),
271 });
272 }
273 }
274 }
275
276 Ok(test_count)
277}
278
279#[derive(Debug, Clone)]
281struct DebugInfo {
282 pre_state: HashMap<Address, (AccountInfo, HashMap<U256, U256>)>,
284 tx_env: Option<revm::context::tx::TxEnv>,
286 block_env: revm::context::block::BlockEnv,
288 cfg_env: CfgEnv,
290 block_idx: usize,
292 tx_idx: usize,
294 withdrawals: Option<Vec<Withdrawal>>,
296}
297
298impl DebugInfo {
299 fn capture_committed_state(
301 state: &State<EmptyDB>,
302 ) -> HashMap<Address, (AccountInfo, HashMap<U256, U256>)> {
303 let mut committed_state = HashMap::new();
304
305 for (address, cache_account) in &state.cache.accounts {
307 if let Some(plain_account) = &cache_account.account {
308 let mut storage = HashMap::new();
309 for (key, value) in &plain_account.storage {
310 storage.insert(*key, *value);
311 }
312 committed_state.insert(*address, (plain_account.info.clone(), storage));
313 }
314 }
315
316 committed_state
317 }
318}
319
320fn validate_post_state(
322 state: &mut State<EmptyDB>,
323 expected_post_state: &BTreeMap<Address, Account>,
324 debug_info: &DebugInfo,
325 print_env_on_error: bool,
326) -> Result<(), TestExecutionError> {
327 for (address, expected_account) in expected_post_state {
328 let actual_account = state
330 .load_cache_account(*address)
331 .map_err(|e| TestExecutionError::Database(format!("Account load failed: {e}")))?;
332 let info = actual_account
333 .account
334 .as_ref()
335 .map(|a| a.info.clone())
336 .unwrap_or_default();
337
338 if info.balance != expected_account.balance {
340 if print_env_on_error {
341 print_error_with_state(debug_info, state, Some(expected_post_state));
342 }
343 return Err(TestExecutionError::PostStateValidation {
344 address: *address,
345 field: "balance".to_string(),
346 expected: format!("{}", expected_account.balance),
347 actual: format!("{}", info.balance),
348 });
349 }
350
351 let expected_nonce = expected_account.nonce.to::<u64>();
353 if info.nonce != expected_nonce {
354 if print_env_on_error {
355 print_error_with_state(debug_info, state, Some(expected_post_state));
356 }
357 return Err(TestExecutionError::PostStateValidation {
358 address: *address,
359 field: "nonce".to_string(),
360 expected: format!("{expected_nonce}"),
361 actual: format!("{}", info.nonce),
362 });
363 }
364
365 if !expected_account.code.is_empty() {
367 if let Some(actual_code) = &info.code {
368 if actual_code.original_bytes() != expected_account.code {
369 if print_env_on_error {
370 print_error_with_state(debug_info, state, Some(expected_post_state));
371 }
372 return Err(TestExecutionError::PostStateValidation {
373 address: *address,
374 field: "code".to_string(),
375 expected: format!("0x{}", hex::encode(&expected_account.code)),
376 actual: format!("0x{}", hex::encode(actual_code.bytecode())),
377 });
378 }
379 } else {
380 if print_env_on_error {
381 print_error_with_state(debug_info, state, Some(expected_post_state));
382 }
383 return Err(TestExecutionError::PostStateValidation {
384 address: *address,
385 field: "code".to_string(),
386 expected: format!("0x{}", hex::encode(&expected_account.code)),
387 actual: "empty".to_string(),
388 });
389 }
390 }
391
392 if let Some(acc) = actual_account.account.as_ref() {
394 for (slot, actual_value) in &acc.storage {
395 let slot = *slot;
396 let actual_value = *actual_value;
397 if !expected_account.storage.contains_key(&slot) && !actual_value.is_zero() {
398 if print_env_on_error {
399 print_error_with_state(debug_info, state, Some(expected_post_state));
400 }
401 return Err(TestExecutionError::PostStateValidation {
402 address: *address,
403 field: format!("storage_unexpected[{slot}]"),
404 expected: "0x0".to_string(),
405 actual: format!("{actual_value}"),
406 });
407 }
408 }
409 }
410
411 for (slot, expected_value) in &expected_account.storage {
413 let actual_value = state.storage(*address, *slot);
414 let actual_value = actual_value.unwrap_or_default();
415
416 if actual_value != *expected_value {
417 if print_env_on_error {
418 print_error_with_state(debug_info, state, Some(expected_post_state));
419 }
420
421 return Err(TestExecutionError::PostStateValidation {
422 address: *address,
423 field: format!("storage_validation[{slot}]"),
424 expected: format!("{expected_value}"),
425 actual: format!("{actual_value}"),
426 });
427 }
428 }
429 }
430 Ok(())
431}
432
433fn print_error_with_state(
435 debug_info: &DebugInfo,
436 current_state: &State<EmptyDB>,
437 expected_post_state: Option<&BTreeMap<Address, Account>>,
438) {
439 eprintln!("\n========== TEST EXECUTION ERROR ==========");
440
441 eprintln!(
443 "\nš Error occurred at block {} transaction {}",
444 debug_info.block_idx, debug_info.tx_idx
445 );
446
447 eprintln!("\nš Configuration Environment:");
449 eprintln!(" Spec ID: {:?}", debug_info.cfg_env.spec);
450 eprintln!(" Chain ID: {}", debug_info.cfg_env.chain_id);
451 eprintln!(
452 " Limit contract code size: {:?}",
453 debug_info.cfg_env.limit_contract_code_size
454 );
455 eprintln!(
456 " Limit contract initcode size: {:?}",
457 debug_info.cfg_env.limit_contract_initcode_size
458 );
459
460 eprintln!("\nšØ Block Environment:");
462 eprintln!(" Number: {}", debug_info.block_env.number);
463 eprintln!(" Timestamp: {}", debug_info.block_env.timestamp);
464 eprintln!(" Gas limit: {}", debug_info.block_env.gas_limit);
465 eprintln!(" Base fee: {:?}", debug_info.block_env.basefee);
466 eprintln!(" Difficulty: {}", debug_info.block_env.difficulty);
467 eprintln!(" Prevrandao: {:?}", debug_info.block_env.prevrandao);
468 eprintln!(" Beneficiary: {:?}", debug_info.block_env.beneficiary);
469 eprintln!(
470 " Blob excess gas: {:?}",
471 debug_info.block_env.blob_excess_gas_and_price
472 );
473
474 if let Some(withdrawals) = &debug_info.withdrawals {
476 eprintln!(" Withdrawals: {} items", withdrawals.len());
477 if !withdrawals.is_empty() {
478 for (i, withdrawal) in withdrawals.iter().enumerate().take(3) {
479 eprintln!(" Withdrawal {i}:");
480 eprintln!(" Index: {}", withdrawal.index);
481 eprintln!(" Validator Index: {}", withdrawal.validator_index);
482 eprintln!(" Address: {:?}", withdrawal.address);
483 eprintln!(
484 " Amount: {} Gwei ({:.6} ETH)",
485 withdrawal.amount,
486 withdrawal.amount.to::<u128>() as f64 / 1_000_000_000.0
487 );
488 }
489 if withdrawals.len() > 3 {
490 eprintln!(" ... and {} more withdrawals", withdrawals.len() - 3);
491 }
492 }
493 }
494
495 if let Some(tx_env) = &debug_info.tx_env {
497 eprintln!("\nš Transaction Environment:");
498 eprintln!(" Transaction type: {}", tx_env.tx_type);
499 eprintln!(" Caller: {:?}", tx_env.caller);
500 eprintln!(" Gas limit: {}", tx_env.gas_limit);
501 eprintln!(" Gas price: {}", tx_env.gas_price);
502 eprintln!(" Gas priority fee: {:?}", tx_env.gas_priority_fee);
503 eprintln!(" Transaction kind: {:?}", tx_env.kind);
504 eprintln!(" Value: {}", tx_env.value);
505 eprintln!(" Data length: {} bytes", tx_env.data.len());
506 if !tx_env.data.is_empty() {
507 let preview_len = std::cmp::min(64, tx_env.data.len());
508 eprintln!(
509 " Data preview: 0x{}{}",
510 hex::encode(&tx_env.data[..preview_len]),
511 if tx_env.data.len() > 64 { "..." } else { "" }
512 );
513 }
514 eprintln!(" Nonce: {}", tx_env.nonce);
515 eprintln!(" Chain ID: {:?}", tx_env.chain_id);
516 eprintln!(" Access list: {} entries", tx_env.access_list.len());
517 if !tx_env.access_list.is_empty() {
518 for (i, access) in tx_env.access_list.iter().enumerate().take(3) {
519 eprintln!(
520 " Access {}: address={:?}, {} storage keys",
521 i,
522 access.address,
523 access.storage_keys.len()
524 );
525 }
526 if tx_env.access_list.len() > 3 {
527 eprintln!(
528 " ... and {} more access list entries",
529 tx_env.access_list.len() - 3
530 );
531 }
532 }
533 eprintln!(" Blob hashes: {} blobs", tx_env.blob_hashes.len());
534 if !tx_env.blob_hashes.is_empty() {
535 for (i, hash) in tx_env.blob_hashes.iter().enumerate().take(3) {
536 eprintln!(" Blob {i}: {hash:?}");
537 }
538 if tx_env.blob_hashes.len() > 3 {
539 eprintln!(
540 " ... and {} more blob hashes",
541 tx_env.blob_hashes.len() - 3
542 );
543 }
544 }
545 eprintln!(" Max fee per blob gas: {}", tx_env.max_fee_per_blob_gas);
546 eprintln!(
547 " Authorization list: {} items",
548 tx_env.authorization_list.len()
549 );
550 if !tx_env.authorization_list.is_empty() {
551 eprintln!(" (EIP-7702 authorizations present)");
552 }
553 } else {
554 eprintln!(
555 "\nš Transaction Environment: Not available (error occurred before tx creation)"
556 );
557 }
558
559 eprintln!("\nš¾ Pre-State (Initial):");
561 for (address, (info, storage)) in &debug_info.pre_state {
562 eprintln!(" Account {address:?}:");
563 eprintln!(" Balance: 0x{:x}", info.balance);
564 eprintln!(" Nonce: {}", info.nonce);
565 eprintln!(" Code hash: {:?}", info.code_hash);
566 eprintln!(
567 " Code size: {} bytes",
568 info.code.as_ref().map_or(0, |c| c.bytecode().len())
569 );
570 if !storage.is_empty() {
571 eprintln!(" Storage ({} slots):", storage.len());
572 for (key, value) in storage.iter().take(5) {
573 eprintln!(" {key:?} => {value:?}");
574 }
575 if storage.len() > 5 {
576 eprintln!(" ... and {} more slots", storage.len() - 5);
577 }
578 }
579 }
580
581 eprintln!("\nš Current State (Actual):");
582 let committed_state = DebugInfo::capture_committed_state(current_state);
583 for (address, (info, storage)) in &committed_state {
584 eprintln!(" Account {address:?}:");
585 eprintln!(" Balance: 0x{:x}", info.balance);
586 eprintln!(" Nonce: {}", info.nonce);
587 eprintln!(" Code hash: {:?}", info.code_hash);
588 eprintln!(
589 " Code size: {} bytes",
590 info.code.as_ref().map_or(0, |c| c.bytecode().len())
591 );
592 if !storage.is_empty() {
593 eprintln!(" Storage ({} slots):", storage.len());
594 for (key, value) in storage.iter().take(5) {
595 eprintln!(" {key:?} => {value:?}");
596 }
597 if storage.len() > 5 {
598 eprintln!(" ... and {} more slots", storage.len() - 5);
599 }
600 }
601 }
602
603 if let Some(expected_post_state) = expected_post_state {
605 eprintln!("\nā
Expected Post-State:");
606 for (address, account) in expected_post_state {
607 eprintln!(" Account {address:?}:");
608 eprintln!(" Balance: 0x{:x}", account.balance);
609 eprintln!(" Nonce: {}", account.nonce);
610 if !account.code.is_empty() {
611 eprintln!(" Code size: {} bytes", account.code.len());
612 }
613 if !account.storage.is_empty() {
614 eprintln!(" Storage ({} slots):", account.storage.len());
615 for (key, value) in account.storage.iter().take(5) {
616 eprintln!(" {key:?} => {value:?}");
617 }
618 if account.storage.len() > 5 {
619 eprintln!(" ... and {} more slots", account.storage.len() - 5);
620 }
621 }
622 }
623 }
624
625 eprintln!("\n===========================================\n");
626}
627
628fn execute_blockchain_test(
630 test_case: &BlockchainTestCase,
631 print_env_on_error: bool,
632 json_output: bool,
633) -> Result<(), TestExecutionError> {
634 if matches!(
636 test_case.network,
637 ForkSpec::ByzantiumToConstantinopleAt5
638 | ForkSpec::ParisToShanghaiAtTime15k
639 | ForkSpec::ShanghaiToCancunAtTime15k
640 | ForkSpec::CancunToPragueAtTime15k
641 | ForkSpec::PragueToOsakaAtTime15k
642 | ForkSpec::BPO1ToBPO2AtTime15k
643 ) {
644 eprintln!("ā ļø Skipping transition fork: {:?}", test_case.network);
645 return Ok(());
646 }
647
648 let mut state = State::builder().build();
650
651 let mut pre_state_debug = HashMap::new();
653
654 let genesis_state = test_case.pre.clone().into_genesis_state();
656 for (address, account) in genesis_state {
657 let account_info = AccountInfo {
658 balance: account.balance,
659 nonce: account.nonce,
660 code_hash: revm::primitives::keccak256(&account.code),
661 code: Some(Bytecode::new_raw(account.code.clone())),
662 };
663
664 if print_env_on_error {
666 pre_state_debug.insert(address, (account_info.clone(), account.storage.clone()));
667 }
668
669 state.insert_account_with_storage(address, account_info, account.storage);
670 }
671
672 state
674 .block_hashes
675 .insert(0, test_case.genesis_block_header.hash);
676
677 let spec_id = fork_to_spec_id(test_case.network);
679 let mut cfg = CfgEnv::default();
680 cfg.spec = spec_id;
681
682 let mut parent_block_hash = Some(test_case.genesis_block_header.hash);
684 let mut parent_excess_blob_gas = test_case
685 .genesis_block_header
686 .excess_blob_gas
687 .unwrap_or_default()
688 .to::<u64>();
689 let mut block_env = test_case.genesis_block_env();
690
691 for (block_idx, block) in test_case.blocks.iter().enumerate() {
693 println!("Run block {block_idx}/{}", test_case.blocks.len());
694
695 let should_fail = block.expect_exception.is_some();
697
698 let transactions = block.transactions.as_deref().unwrap_or_default();
699
700 let mut block_hash = None;
703 let mut beacon_root = None;
704 let this_excess_blob_gas;
705
706 if let Some(block_header) = block.block_header.as_ref() {
707 block_hash = Some(block_header.hash);
708 beacon_root = block_header.parent_beacon_block_root;
709 block_env = block_header.to_block_env(Some(BlobExcessGasAndPrice::new_with_spec(
710 parent_excess_blob_gas,
711 spec_id,
712 )));
713 this_excess_blob_gas = block_header.excess_blob_gas.map(|i| i.to::<u64>());
714 } else {
715 this_excess_blob_gas = None;
716 }
717
718 let evm_context = Context::mainnet()
720 .with_block(&block_env)
721 .with_cfg(&cfg)
722 .with_db(&mut state);
723
724 let mut evm = evm_context.build_mainnet_with_inspector(TracerEip3155::new_stdout());
726
727 pre_block::pre_block_transition(&mut evm, spec_id, parent_block_hash, beacon_root);
729
730 for (tx_idx, tx) in transactions.iter().enumerate() {
732 if tx.sender.is_none() {
733 if print_env_on_error {
734 let debug_info = DebugInfo {
735 pre_state: pre_state_debug.clone(),
736 tx_env: None,
737 block_env: block_env.clone(),
738 cfg_env: cfg.clone(),
739 block_idx,
740 tx_idx,
741 withdrawals: block.withdrawals.clone(),
742 };
743 print_error_with_state(
744 &debug_info,
745 evm.ctx().db_ref(),
746 test_case.post_state.as_ref(),
747 );
748 }
749 if json_output {
750 let output = json!({
751 "block": block_idx,
752 "tx": tx_idx,
753 "error": "missing sender",
754 "status": "skipped"
755 });
756 println!("{}", serde_json::to_string(&output).unwrap());
757 } else {
758 eprintln!("ā ļø Skipping block {block_idx} due to missing sender");
759 }
760 break; }
762
763 let tx_env = match tx.to_tx_env() {
764 Ok(env) => env,
765 Err(e) => {
766 if should_fail {
767 continue;
769 }
770 if print_env_on_error {
771 let debug_info = DebugInfo {
772 pre_state: pre_state_debug.clone(),
773 tx_env: None,
774 block_env: block_env.clone(),
775 cfg_env: cfg.clone(),
776 block_idx,
777 tx_idx,
778 withdrawals: block.withdrawals.clone(),
779 };
780 print_error_with_state(
781 &debug_info,
782 evm.ctx().db_ref(),
783 test_case.post_state.as_ref(),
784 );
785 }
786 if json_output {
787 let output = json!({
788 "block": block_idx,
789 "tx": tx_idx,
790 "error": format!("tx env creation error: {e}"),
791 "status": "skipped"
792 });
793 println!("{}", serde_json::to_string(&output).unwrap());
794 } else {
795 eprintln!(
796 "ā ļø Skipping block {block_idx} due to transaction env creation error: {e}"
797 );
798 }
799 break; }
801 };
802
803 let execution_result = if json_output {
805 evm.inspect_tx(tx_env.clone())
806 } else {
807 evm.transact(tx_env.clone())
808 };
809
810 match execution_result {
811 Ok(result) => {
812 if should_fail {
813 if print_env_on_error {
816 if json_output {
818 eprintln!("=== Transaction trace (unexpected success) ===");
819 }
820 let _ = evm.inspect_tx(tx_env.clone());
821 }
822
823 if print_env_on_error {
824 let debug_info = DebugInfo {
825 pre_state: pre_state_debug.clone(),
826 tx_env: Some(tx_env.clone()),
827 block_env: block_env.clone(),
828 cfg_env: cfg.clone(),
829 block_idx,
830 tx_idx,
831 withdrawals: block.withdrawals.clone(),
832 };
833 print_error_with_state(
834 &debug_info,
835 evm.ctx().db_ref(),
836 test_case.post_state.as_ref(),
837 );
838 }
839 let exception = block.expect_exception.clone().unwrap_or_default();
840 if json_output {
841 let output = json!({
842 "block": block_idx,
843 "tx": tx_idx,
844 "error": format!("expected failure: {exception}"),
845 "gas_used": result.result.gas_used(),
846 "status": "unexpected_success"
847 });
848 println!("{}", serde_json::to_string(&output).unwrap());
849 } else {
850 eprintln!(
851 "ā ļø Skipping block {block_idx} due to expected failure: {exception}"
852 );
853 }
854 break; }
856 evm.commit(result.state);
857 }
858 Err(e) => {
859 if !should_fail {
860 if print_env_on_error {
862 if json_output {
863 eprintln!("=== Transaction trace (unexpected failure) ===");
864 }
865 let _ = evm.inspect_tx(tx_env.clone());
866 }
867
868 if print_env_on_error {
869 let debug_info = DebugInfo {
870 pre_state: pre_state_debug.clone(),
871 tx_env: Some(tx_env.clone()),
872 block_env: block_env.clone(),
873 cfg_env: cfg.clone(),
874 block_idx,
875 tx_idx,
876 withdrawals: block.withdrawals.clone(),
877 };
878 print_error_with_state(
879 &debug_info,
880 evm.ctx().db_ref(),
881 test_case.post_state.as_ref(),
882 );
883 }
884 if json_output {
885 let output = json!({
886 "block": block_idx,
887 "tx": tx_idx,
888 "error": format!("{e:?}"),
889 "status": "unexpected_failure"
890 });
891 println!("{}", serde_json::to_string(&output).unwrap());
892 } else {
893 eprintln!(
894 "ā ļø Skipping block {block_idx} due to unexpected failure: {e:?}"
895 );
896 }
897 break; } else if json_output {
899 let output = json!({
901 "block": block_idx,
902 "tx": tx_idx,
903 "error": format!("{e:?}"),
904 "status": "expected_failure"
905 });
906 println!("{}", serde_json::to_string(&output).unwrap());
907 }
908 }
909 }
910 }
911
912 post_block::post_block_transition(
914 &mut evm,
915 &block_env,
916 block.withdrawals.as_deref().unwrap_or_default(),
917 spec_id,
918 );
919
920 state
922 .block_hashes
923 .insert(block_env.number.to::<u64>(), block_hash.unwrap_or_default());
924
925 parent_block_hash = block_hash;
926 if let Some(excess_blob_gas) = this_excess_blob_gas {
927 parent_excess_blob_gas = excess_blob_gas;
928 }
929
930 state.merge_transitions(BundleRetention::Reverts);
931 }
932
933 if let Some(expected_post_state) = &test_case.post_state {
935 let debug_info = DebugInfo {
937 pre_state: pre_state_debug.clone(),
938 tx_env: None, block_env: block_env.clone(),
940 cfg_env: cfg.clone(),
941 block_idx: test_case.blocks.len(),
942 tx_idx: 0,
943 withdrawals: test_case.blocks.last().and_then(|b| b.withdrawals.clone()),
944 };
945 validate_post_state(
946 &mut state,
947 expected_post_state,
948 &debug_info,
949 print_env_on_error,
950 )?;
951 }
952
953 Ok(())
954}
955
956fn fork_to_spec_id(fork: ForkSpec) -> SpecId {
958 match fork {
959 ForkSpec::Frontier => SpecId::FRONTIER,
960 ForkSpec::Homestead | ForkSpec::FrontierToHomesteadAt5 => SpecId::HOMESTEAD,
961 ForkSpec::EIP150 | ForkSpec::HomesteadToDaoAt5 | ForkSpec::HomesteadToEIP150At5 => {
962 SpecId::TANGERINE
963 }
964 ForkSpec::EIP158 => SpecId::SPURIOUS_DRAGON,
965 ForkSpec::Byzantium
966 | ForkSpec::EIP158ToByzantiumAt5
967 | ForkSpec::ByzantiumToConstantinopleFixAt5 => SpecId::BYZANTIUM,
968 ForkSpec::Constantinople | ForkSpec::ByzantiumToConstantinopleAt5 => SpecId::PETERSBURG,
969 ForkSpec::ConstantinopleFix => SpecId::PETERSBURG,
970 ForkSpec::Istanbul => SpecId::ISTANBUL,
971 ForkSpec::Berlin => SpecId::BERLIN,
972 ForkSpec::London | ForkSpec::BerlinToLondonAt5 => SpecId::LONDON,
973 ForkSpec::Paris | ForkSpec::ParisToShanghaiAtTime15k => SpecId::MERGE,
974 ForkSpec::Shanghai => SpecId::SHANGHAI,
975 ForkSpec::Cancun | ForkSpec::ShanghaiToCancunAtTime15k => SpecId::CANCUN,
976 ForkSpec::Prague | ForkSpec::CancunToPragueAtTime15k => SpecId::PRAGUE,
977 ForkSpec::Osaka | ForkSpec::PragueToOsakaAtTime15k => SpecId::OSAKA,
978 _ => SpecId::OSAKA, }
980}
981
982fn skip_test(path: &Path) -> bool {
984 let path_str = path.to_str().unwrap_or_default();
985 if path_str.contains("paris/eip7610_create_collision")
987 || path_str.contains("cancun/eip4844_blobs")
988 || path_str.contains("prague/eip7251_consolidations")
989 || path_str.contains("prague/eip7685_general_purpose_el_requests")
990 || path_str.contains("prague/eip7002_el_triggerable_withdrawals")
991 || path_str.contains("osaka/eip7918_blob_reserve_price")
992 {
993 return true;
994 }
995
996 let name = path.file_name().unwrap().to_str().unwrap();
997 matches!(
999 name,
1000 "CreateTransactionHighNonce.json"
1002
1003 | "RevertInCreateInInit_Paris.json"
1005 | "RevertInCreateInInit.json"
1006 | "dynamicAccountOverwriteEmpty.json"
1007 | "dynamicAccountOverwriteEmpty_Paris.json"
1008 | "RevertInCreateInInitCreate2Paris.json"
1009 | "create2collisionStorage.json"
1010 | "RevertInCreateInInitCreate2.json"
1011 | "create2collisionStorageParis.json"
1012 | "InitCollision.json"
1013 | "InitCollisionParis.json"
1014
1015 | "ValueOverflow.json"
1017 | "ValueOverflowParis.json"
1018
1019 | "Call50000_sha256.json"
1021 | "static_Call50000_sha256.json"
1022 | "loopMul.json"
1023 | "CALLBlake2f_MaxRounds.json"
1024 | "scenarios.json"
1026 | "invalid_tx_max_fee_per_blob_gas.json"
1028 | "correct_increasing_blob_gas_costs.json"
1029 | "correct_decreasing_blob_gas_costs.json"
1030
1031 | "block_hashes_history.json"
1033 )
1034}
1035
1036#[derive(Debug, Error)]
1037pub enum TestExecutionError {
1038 #[error("Database error: {0}")]
1039 Database(String),
1040
1041 #[error("Skipped fork: {0}")]
1042 SkippedFork(String),
1043
1044 #[error("Sender is required")]
1045 SenderRequired,
1046
1047 #[error("Expected failure at block {block_idx}, tx {tx_idx}: {message}")]
1048 ExpectedFailure {
1049 block_idx: usize,
1050 tx_idx: usize,
1051 message: String,
1052 },
1053
1054 #[error("Unexpected failure at block {block_idx}, tx {tx_idx}: {error}")]
1055 UnexpectedFailure {
1056 block_idx: usize,
1057 tx_idx: usize,
1058 error: String,
1059 },
1060
1061 #[error("Transaction env creation failed at block {block_idx}, tx {tx_idx}: {error}")]
1062 TransactionEnvCreation {
1063 block_idx: usize,
1064 tx_idx: usize,
1065 error: String,
1066 },
1067
1068 #[error("Unexpected revert at block {block_idx}, tx {tx_idx}, gas used: {gas_used}")]
1069 UnexpectedRevert {
1070 block_idx: usize,
1071 tx_idx: usize,
1072 gas_used: u64,
1073 },
1074
1075 #[error("Unexpected halt at block {block_idx}, tx {tx_idx}: {reason:?}, gas used: {gas_used}")]
1076 UnexpectedHalt {
1077 block_idx: usize,
1078 tx_idx: usize,
1079 reason: HaltReason,
1080 gas_used: u64,
1081 },
1082
1083 #[error(
1084 "Post-state validation failed for {address:?}.{field}: expected {expected}, got {actual}"
1085 )]
1086 PostStateValidation {
1087 address: Address,
1088 field: String,
1089 expected: String,
1090 actual: String,
1091 },
1092}
1093
1094#[derive(Debug, Error)]
1095pub enum Error {
1096 #[error("Path not found: {0}")]
1097 PathNotFound(PathBuf),
1098
1099 #[error("No JSON files found in: {0}")]
1100 NoJsonFiles(PathBuf),
1101
1102 #[error("Failed to read file {0}: {1}")]
1103 FileRead(PathBuf, std::io::Error),
1104
1105 #[error("Failed to decode JSON from {0}: {1}")]
1106 JsonDecode(PathBuf, serde_json::Error),
1107
1108 #[error("Test execution failed for {test_name} in {test_path}: {error}")]
1109 TestExecution {
1110 test_name: String,
1111 test_path: PathBuf,
1112 error: String,
1113 },
1114
1115 #[error("Directory traversal error: {0}")]
1116 WalkDir(#[from] walkdir::Error),
1117
1118 #[error("{failed} tests failed")]
1119 TestsFailed { failed: usize },
1120}