1pub mod post_block;
2pub mod pre_block;
3
4use crate::dir_utils::find_all_json_tests;
5use clap::Parser;
6
7use revm::statetest_types::blockchain::{
8 Account, BlockchainTest, BlockchainTestCase, ForkSpec, Withdrawal,
9};
10use revm::{
11 bytecode::Bytecode,
12 context::{cfg::CfgEnv, ContextTr},
13 context_interface::{block::BlobExcessGasAndPrice, result::HaltReason},
14 database::{states::bundle_state::BundleRetention, EmptyDB, State},
15 handler::EvmTr,
16 inspector::inspectors::TracerEip3155,
17 primitives::{hardfork::SpecId, hex, Address, AddressMap, U256Map, U256},
18 state::{bal::Bal, AccountInfo},
19 Context, Database, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
20};
21use serde_json::json;
22use std::{
23 collections::BTreeMap,
24 fs,
25 path::{Path, PathBuf},
26 sync::Arc,
27 time::Instant,
28};
29use thiserror::Error;
30
31fn print_json<T: serde::Serialize>(value: &T) {
33 println!("{}", serde_json::to_string(value).unwrap());
34}
35
36#[derive(Parser, Debug)]
38pub struct Cmd {
39 #[arg(required = true, num_args = 1..)]
45 paths: Vec<PathBuf>,
46 #[arg(long)]
48 omit_progress: bool,
49 #[arg(long, alias = "no-fail-fast")]
51 keep_going: bool,
52 #[arg(long)]
54 print_env_on_error: bool,
55 #[arg(long)]
57 json: bool,
58}
59
60impl Cmd {
61 pub fn run(&self) -> Result<(), Error> {
63 for path in &self.paths {
64 if !path.exists() {
65 return Err(Error::PathNotFound(path.clone()));
66 }
67
68 if !self.json {
69 println!("\nRunning blockchain tests in {}...", path.display());
70 }
71 let test_files = find_all_json_tests(path);
72
73 if test_files.is_empty() {
74 return Err(Error::NoJsonFiles(path.clone()));
75 }
76
77 run_tests(
78 test_files,
79 self.omit_progress,
80 self.keep_going,
81 self.print_env_on_error,
82 self.json,
83 )?;
84 }
85 Ok(())
86 }
87}
88
89fn run_tests(
91 test_files: Vec<PathBuf>,
92 omit_progress: bool,
93 keep_going: bool,
94 print_env_on_error: bool,
95 json_output: bool,
96) -> Result<(), Error> {
97 let mut passed = 0;
98 let mut failed = 0;
99 let mut skipped = 0;
100 let mut failed_paths = Vec::new();
101
102 let start_time = Instant::now();
103 let total_files = test_files.len();
104
105 for (file_index, file_path) in test_files.into_iter().enumerate() {
106 let current_file = file_index + 1;
107 if skip_test(&file_path) {
108 skipped += 1;
109 if json_output {
110 let output = json!({
111 "file": file_path.display().to_string(),
112 "status": "skipped",
113 "reason": "known_issue"
114 });
115 print_json(&output);
116 } else if !omit_progress {
117 println!(
118 "Skipping ({}/{}): {}",
119 current_file,
120 total_files,
121 file_path.display()
122 );
123 }
124 continue;
125 }
126
127 let result = run_test_file(&file_path, json_output, print_env_on_error);
128
129 match result {
130 Ok(test_count) => {
131 passed += test_count;
132 if json_output {
133 } else if !omit_progress {
135 println!(
136 "ā ({}/{}) {} ({} tests)",
137 current_file,
138 total_files,
139 file_path.display(),
140 test_count
141 );
142 }
143 }
144 Err(e) => {
145 failed += 1;
146 if keep_going {
147 failed_paths.push(file_path.clone());
148 }
149 if json_output {
150 let output = json!({
151 "file": file_path.display().to_string(),
152 "error": e.to_string(),
153 "status": "failed"
154 });
155 print_json(&output);
156 } else if !omit_progress {
157 eprintln!(
158 "ā ({}/{}) {} - {}",
159 current_file,
160 total_files,
161 file_path.display(),
162 e
163 );
164 }
165
166 if !keep_going {
167 return Err(e);
168 }
169 }
170 }
171 }
172
173 let duration = start_time.elapsed();
174
175 if json_output {
176 let results = json!({
177 "summary": {
178 "passed": passed,
179 "failed": failed,
180 "skipped": skipped,
181 "duration_secs": duration.as_secs_f64(),
182 }
183 });
184 print_json(&results);
185 } else {
186 if keep_going && !failed_paths.is_empty() {
188 println!("\nFailed test files:");
189 for path in &failed_paths {
190 println!(" {}", path.display());
191 }
192 }
193
194 println!("\nTest results:");
195 println!(" Passed: {passed}");
196 println!(" Failed: {failed}");
197 println!(" Skipped: {skipped}");
198 println!(" Time: {:.2}s", duration.as_secs_f64());
199 }
200
201 if failed > 0 {
202 Err(Error::TestsFailed { failed })
203 } else {
204 Ok(())
205 }
206}
207
208fn run_test_file(
210 file_path: &Path,
211 json_output: bool,
212 print_env_on_error: bool,
213) -> Result<usize, Error> {
214 let content =
215 fs::read_to_string(file_path).map_err(|e| Error::FileRead(file_path.to_path_buf(), e))?;
216
217 let blockchain_test: BlockchainTest = serde_json::from_str(&content)
218 .map_err(|e| Error::JsonDecode(file_path.to_path_buf(), e))?;
219
220 let mut test_count = 0;
221
222 for (test_name, test_case) in blockchain_test.0 {
223 if json_output {
224 let output = json!({
226 "test": test_name,
227 "file": file_path.display().to_string(),
228 "status": "running"
229 });
230 print_json(&output);
231 } else {
232 println!(" Running: {test_name}");
233 }
234 let result = execute_blockchain_test(&test_case, print_env_on_error, json_output);
236
237 match result {
238 Ok(()) => {
239 if json_output {
240 let output = json!({
241 "test": test_name,
242 "file": file_path.display().to_string(),
243 "status": "passed"
244 });
245 print_json(&output);
246 }
247 test_count += 1;
248 }
249 Err(e) => {
250 if json_output {
251 let output = json!({
252 "test": test_name,
253 "file": file_path.display().to_string(),
254 "status": "failed",
255 "error": e.to_string()
256 });
257 print_json(&output);
258 }
259 return Err(Error::TestExecution {
260 test_name,
261 test_path: file_path.to_path_buf(),
262 error: e.to_string(),
263 });
264 }
265 }
266 }
267
268 Ok(test_count)
269}
270
271#[derive(Debug, Clone)]
273struct DebugInfo {
274 pre_state: AddressMap<(AccountInfo, U256Map<U256>)>,
276 tx_env: Option<revm::context::tx::TxEnv>,
278 block_env: revm::context::block::BlockEnv,
280 cfg_env: CfgEnv,
282 block_idx: usize,
284 tx_idx: usize,
286 withdrawals: Option<Vec<Withdrawal>>,
288}
289
290impl DebugInfo {
291 fn capture_committed_state(state: &State<EmptyDB>) -> AddressMap<(AccountInfo, U256Map<U256>)> {
293 let mut committed_state = AddressMap::default();
294
295 for (address, cache_account) in &state.cache.accounts {
297 if let Some(plain_account) = &cache_account.account {
298 let mut storage = U256Map::default();
299 for (key, value) in &plain_account.storage {
300 storage.insert(*key, *value);
301 }
302 committed_state.insert(*address, (plain_account.info.clone(), storage));
303 }
304 }
305
306 committed_state
307 }
308}
309
310fn validate_post_state(
312 state: &mut State<EmptyDB>,
313 expected_post_state: &BTreeMap<Address, Account>,
314 debug_info: &DebugInfo,
315 print_env_on_error: bool,
316) -> Result<(), TestExecutionError> {
317 #[expect(clippy::too_many_arguments)]
318 fn make_failure(
319 state: &mut State<EmptyDB>,
320 debug_info: &DebugInfo,
321 expected_post_state: &BTreeMap<Address, Account>,
322 print_env_on_error: bool,
323 address: Address,
324 field: String,
325 expected: String,
326 actual: String,
327 ) -> Result<(), TestExecutionError> {
328 if print_env_on_error {
329 print_error_with_state(debug_info, state, Some(expected_post_state));
330 }
331 Err(TestExecutionError::PostStateValidation {
332 address,
333 field,
334 expected,
335 actual,
336 })
337 }
338
339 for (address, expected_account) in expected_post_state {
340 let (info, actual_storage) = {
343 let actual_account = state
344 .load_cache_account(*address)
345 .map_err(|e| TestExecutionError::Database(format!("Account load failed: {e}")))?;
346 let account = actual_account.account.as_ref();
347 (
348 account.map(|a| a.info.clone()).unwrap_or_default(),
349 account.map(|a| a.storage.clone()).unwrap_or_default(),
350 )
351 };
352
353 if info.balance != expected_account.balance {
355 return make_failure(
356 state,
357 debug_info,
358 expected_post_state,
359 print_env_on_error,
360 *address,
361 "balance".to_string(),
362 format!("{}", expected_account.balance),
363 format!("{}", info.balance),
364 );
365 }
366
367 let expected_nonce = expected_account.nonce.to::<u64>();
369 if info.nonce != expected_nonce {
370 return make_failure(
371 state,
372 debug_info,
373 expected_post_state,
374 print_env_on_error,
375 *address,
376 "nonce".to_string(),
377 format!("{expected_nonce}"),
378 format!("{}", info.nonce),
379 );
380 }
381
382 if !expected_account.code.is_empty() {
385 let actual_code = match info.code.clone() {
386 Some(code) => Some(code),
387 None if !info.is_empty_code_hash() => {
388 Some(Database::code_by_hash(state, info.code_hash).map_err(|e| {
389 TestExecutionError::Database(format!("Code load failed: {e}"))
390 })?)
391 }
392 None => None,
393 };
394 if let Some(actual_code) = &actual_code {
395 if actual_code.original_bytes() != expected_account.code {
396 return make_failure(
397 state,
398 debug_info,
399 expected_post_state,
400 print_env_on_error,
401 *address,
402 "code".to_string(),
403 format!("0x{}", hex::encode(&expected_account.code)),
404 format!("0x{}", hex::encode(actual_code.original_byte_slice())),
405 );
406 }
407 } else {
408 return make_failure(
409 state,
410 debug_info,
411 expected_post_state,
412 print_env_on_error,
413 *address,
414 "code".to_string(),
415 format!("0x{}", hex::encode(&expected_account.code)),
416 "empty".to_string(),
417 );
418 }
419 }
420
421 for (slot, actual_value) in &actual_storage {
423 let slot = *slot;
424 let actual_value = *actual_value;
425 if !expected_account.storage.contains_key(&slot) && !actual_value.is_zero() {
426 return make_failure(
427 state,
428 debug_info,
429 expected_post_state,
430 print_env_on_error,
431 *address,
432 format!("storage_unexpected[{slot}]"),
433 "0x0".to_string(),
434 format!("{actual_value}"),
435 );
436 }
437 }
438
439 for (slot, expected_value) in &expected_account.storage {
441 let actual_value = state.storage(*address, *slot);
442 let actual_value = actual_value.unwrap_or_default();
443
444 if actual_value != *expected_value {
445 return make_failure(
446 state,
447 debug_info,
448 expected_post_state,
449 print_env_on_error,
450 *address,
451 format!("storage_validation[{slot}]"),
452 format!("{expected_value}"),
453 format!("{actual_value}"),
454 );
455 }
456 }
457 }
458 Ok(())
459}
460
461fn print_error_with_state(
463 debug_info: &DebugInfo,
464 current_state: &State<EmptyDB>,
465 expected_post_state: Option<&BTreeMap<Address, Account>>,
466) {
467 eprintln!("\n========== TEST EXECUTION ERROR ==========");
468
469 eprintln!(
471 "\nš Error occurred at block {} transaction {}",
472 debug_info.block_idx, debug_info.tx_idx
473 );
474
475 eprintln!("\nš Configuration Environment:");
477 eprintln!(" Spec ID: {:?}", debug_info.cfg_env.spec());
478 eprintln!(" Chain ID: {}", debug_info.cfg_env.chain_id);
479 eprintln!(
480 " Limit contract code size: {:?}",
481 debug_info.cfg_env.limit_contract_code_size
482 );
483 eprintln!(
484 " Limit contract initcode size: {:?}",
485 debug_info.cfg_env.limit_contract_initcode_size
486 );
487
488 eprintln!("\nšØ Block Environment:");
490 eprintln!(" Number: {}", debug_info.block_env.number);
491 eprintln!(" Timestamp: {}", debug_info.block_env.timestamp);
492 eprintln!(" Gas limit: {}", debug_info.block_env.gas_limit);
493 eprintln!(" Base fee: {:?}", debug_info.block_env.basefee);
494 eprintln!(" Difficulty: {}", debug_info.block_env.difficulty);
495 eprintln!(" Prevrandao: {:?}", debug_info.block_env.prevrandao);
496 eprintln!(" Beneficiary: {:?}", debug_info.block_env.beneficiary);
497 let blob = debug_info.block_env.blob_excess_gas_and_price;
498 eprintln!(" Blob excess gas: {:?}", blob.map(|a| a.excess_blob_gas));
499 eprintln!(" Blob gas price: {:?}", blob.map(|a| a.blob_gasprice));
500
501 if let Some(withdrawals) = &debug_info.withdrawals {
503 eprintln!(" Withdrawals: {} items", withdrawals.len());
504 if !withdrawals.is_empty() {
505 for (i, withdrawal) in withdrawals.iter().enumerate().take(3) {
506 eprintln!(" Withdrawal {i}:");
507 eprintln!(" Index: {}", withdrawal.index);
508 eprintln!(" Validator Index: {}", withdrawal.validator_index);
509 eprintln!(" Address: {:?}", withdrawal.address);
510 eprintln!(
511 " Amount: {} Gwei ({:.6} ETH)",
512 withdrawal.amount,
513 withdrawal.amount.to::<u128>() as f64 / 1_000_000_000.0
514 );
515 }
516 if withdrawals.len() > 3 {
517 eprintln!(" ... and {} more withdrawals", withdrawals.len() - 3);
518 }
519 }
520 }
521
522 if let Some(tx_env) = &debug_info.tx_env {
524 eprintln!("\nš Transaction Environment:");
525 eprintln!(" Transaction type: {}", tx_env.tx_type);
526 eprintln!(" Caller: {:?}", tx_env.caller);
527 eprintln!(" Gas limit: {}", tx_env.gas_limit);
528 eprintln!(" Gas price: {}", tx_env.gas_price);
529 eprintln!(" Gas priority fee: {:?}", tx_env.gas_priority_fee);
530 eprintln!(" Transaction kind: {:?}", tx_env.kind);
531 eprintln!(" Value: {}", tx_env.value);
532 eprintln!(" Data length: {} bytes", tx_env.data.len());
533 if !tx_env.data.is_empty() {
534 let preview_len = std::cmp::min(64, tx_env.data.len());
535 eprintln!(
536 " Data preview: 0x{}{}",
537 hex::encode(&tx_env.data[..preview_len]),
538 if tx_env.data.len() > 64 { "..." } else { "" }
539 );
540 }
541 eprintln!(" Nonce: {}", tx_env.nonce);
542 eprintln!(" Chain ID: {:?}", tx_env.chain_id);
543 eprintln!(" Access list: {} entries", tx_env.access_list.len());
544 if !tx_env.access_list.is_empty() {
545 for (i, access) in tx_env.access_list.iter().enumerate().take(3) {
546 eprintln!(
547 " Access {}: address={:?}, {} storage keys",
548 i,
549 access.address,
550 access.storage_keys.len()
551 );
552 }
553 if tx_env.access_list.len() > 3 {
554 eprintln!(
555 " ... and {} more access list entries",
556 tx_env.access_list.len() - 3
557 );
558 }
559 }
560 eprintln!(" Blob hashes: {} blobs", tx_env.blob_hashes.len());
561 if !tx_env.blob_hashes.is_empty() {
562 for (i, hash) in tx_env.blob_hashes.iter().enumerate().take(3) {
563 eprintln!(" Blob {i}: {hash:?}");
564 }
565 if tx_env.blob_hashes.len() > 3 {
566 eprintln!(
567 " ... and {} more blob hashes",
568 tx_env.blob_hashes.len() - 3
569 );
570 }
571 }
572 eprintln!(" Max fee per blob gas: {}", tx_env.max_fee_per_blob_gas);
573 eprintln!(
574 " Authorization list: {} items",
575 tx_env.authorization_list.len()
576 );
577 if !tx_env.authorization_list.is_empty() {
578 eprintln!(" (EIP-7702 authorizations present)");
579 }
580 } else {
581 eprintln!(
582 "\nš Transaction Environment: Not available (error occurred before tx creation)"
583 );
584 }
585
586 eprintln!("\nš¾ Pre-State (Initial):");
588 let mut sorted_accounts: Vec<_> = debug_info.pre_state.iter().collect();
590 sorted_accounts.sort_by_key(|(addr, _)| *addr);
591 for (address, (info, storage)) in sorted_accounts {
592 eprintln!(" Account {address:?}:");
593 eprintln!(" Balance: 0x{:x}", info.balance);
594 eprintln!(" Nonce: {}", info.nonce);
595 eprintln!(" Code hash: {:?}", info.code_hash);
596 eprintln!(
597 " Code size: {} bytes",
598 info.code.as_ref().map_or(0, |c| c.len())
599 );
600 if !storage.is_empty() {
601 eprintln!(" Storage ({} slots):", storage.len());
602 let mut sorted_storage: Vec<_> = storage.iter().collect();
603 sorted_storage.sort_by_key(|(key, _)| *key);
604 for (key, value) in sorted_storage.iter() {
605 eprintln!(" {key:?} => {value:?}");
606 }
607 }
608 }
609
610 eprintln!("\nš Current State (Actual):");
611 let committed_state = DebugInfo::capture_committed_state(current_state);
612 let mut sorted_current: Vec<_> = committed_state.iter().collect();
614 sorted_current.sort_by_key(|(addr, _)| *addr);
615 for (address, (info, storage)) in sorted_current {
616 eprintln!(" Account {address:?}:");
617 eprintln!(" Balance: 0x{:x}", info.balance);
618 eprintln!(" Nonce: {}", info.nonce);
619 eprintln!(" Code hash: {:?}", info.code_hash);
620 eprintln!(
621 " Code size: {} bytes",
622 info.code.as_ref().map_or(0, |c| c.len())
623 );
624 if !storage.is_empty() {
625 eprintln!(" Storage ({} slots):", storage.len());
626 let mut sorted_storage: Vec<_> = storage.iter().collect();
627 sorted_storage.sort_by_key(|(key, _)| *key);
628 for (key, value) in sorted_storage.iter() {
629 eprintln!(" {key:?} => {value:?}");
630 }
631 }
632 }
633
634 if let Some(expected_post_state) = expected_post_state {
636 eprintln!("\nā
Expected Post-State:");
637 for (address, account) in expected_post_state {
638 eprintln!(" Account {address:?}:");
639 eprintln!(" Balance: 0x{:x}", account.balance);
640 eprintln!(" Nonce: {}", account.nonce);
641 if !account.code.is_empty() {
642 eprintln!(" Code size: {} bytes", account.code.len());
643 }
644 if !account.storage.is_empty() {
645 eprintln!(" Storage ({} slots):", account.storage.len());
646 for (key, value) in account.storage.iter() {
647 eprintln!(" {key:?} => {value:?}");
648 }
649 }
650 }
651 }
652
653 eprintln!("\n===========================================\n");
654}
655
656fn execute_blockchain_test(
658 test_case: &BlockchainTestCase,
659 print_env_on_error: bool,
660 json_output: bool,
661) -> Result<(), TestExecutionError> {
662 if matches!(
664 test_case.network,
665 ForkSpec::ByzantiumToConstantinopleAt5
666 | ForkSpec::ParisToShanghaiAtTime15k
667 | ForkSpec::ShanghaiToCancunAtTime15k
668 | ForkSpec::CancunToPragueAtTime15k
669 | ForkSpec::PragueToOsakaAtTime15k
670 | ForkSpec::BPO1ToBPO2AtTime15k
671 | ForkSpec::BPO2ToAmsterdamAtTime15k
672 ) {
673 eprintln!("ā ļø Skipping transition fork: {:?}", test_case.network);
674 return Ok(());
675 }
676
677 let mut state = State::builder().with_bal_builder().build();
679
680 let mut pre_state_debug = AddressMap::default();
682
683 let genesis_state = test_case.pre.clone().into_genesis_state();
688 for (address, account) in genesis_state {
689 let code_hash = revm::primitives::keccak256(&account.code);
690 let bytecode = (!account.code.is_empty()).then(|| Bytecode::new_raw(account.code.clone()));
691 let account_info = AccountInfo {
692 balance: account.balance,
693 nonce: account.nonce,
694 code_hash,
695 code: None,
696 account_id: None,
697 };
698
699 if let Some(bytecode) = &bytecode {
700 state.cache.contracts.insert(code_hash, bytecode.clone());
701 }
702
703 if print_env_on_error {
705 pre_state_debug.insert(
706 address,
707 (
708 AccountInfo {
709 code: bytecode,
710 ..account_info.clone()
711 },
712 account.storage.clone(),
713 ),
714 );
715 }
716
717 state.insert_account_with_storage(address, account_info, account.storage);
718 }
719
720 state
722 .block_hashes
723 .insert(0, test_case.genesis_block_header.hash);
724
725 let spec_id = fork_to_spec_id(test_case.network);
727 let mut cfg = CfgEnv::default();
728 cfg.set_spec_and_mainnet_gas_params(spec_id);
729
730 let mut parent_block_hash = Some(test_case.genesis_block_header.hash);
732 let mut parent_excess_blob_gas = test_case
733 .genesis_block_header
734 .excess_blob_gas
735 .unwrap_or_default()
736 .to::<u64>();
737 let mut block_env = test_case.genesis_block_env();
738
739 for (block_idx, block) in test_case.blocks.iter().enumerate() {
741 if !json_output {
742 println!("Run block {block_idx}/{}", test_case.blocks.len());
743 }
744
745 let should_fail = block.expect_exception.is_some();
747
748 let transactions = block.transactions.as_deref().unwrap_or_default();
749
750 let mut block_hash = None;
753 let mut beacon_root = None;
754 let this_excess_blob_gas;
755
756 if let Some(block_header) = block.block_header.as_ref() {
757 block_hash = Some(block_header.hash);
758 beacon_root = block_header.parent_beacon_block_root;
759 block_env = block_header.to_block_env(Some(BlobExcessGasAndPrice::new_with_spec(
760 parent_excess_blob_gas,
761 spec_id,
762 )));
763 this_excess_blob_gas = block_header.excess_blob_gas.map(|i| i.to::<u64>());
764 } else {
765 this_excess_blob_gas = None;
766 }
767
768 let bal_test = block
769 .block_access_list
770 .as_ref()
771 .and_then(|bal| Bal::try_from(bal.clone()).ok())
772 .map(Arc::new);
773
774 state.reset_bal_index();
776
777 let evm_context = Context::mainnet()
779 .with_block(&block_env)
780 .with_cfg(cfg.clone())
781 .with_db(&mut state);
782
783 let mut evm = evm_context.build_mainnet_with_inspector(TracerEip3155::new_stdout());
785
786 pre_block::pre_block_transition(&mut evm, spec_id, parent_block_hash, beacon_root)
788 .map_err(|e| TestExecutionError::PreBlockSystemCall {
789 block_idx,
790 error: format!("{e:?}"),
791 })?;
792
793 let mut cumulative_tx_gas_used: u64 = 0;
796 let mut block_regular_gas_used: u64 = 0;
797 let mut block_state_gas_used: u64 = 0;
798 let mut block_completed = true;
799
800 for (tx_idx, tx) in transactions.iter().enumerate() {
802 if tx.sender.is_none() {
803 if print_env_on_error {
804 let debug_info = DebugInfo {
805 pre_state: pre_state_debug.clone(),
806 tx_env: None,
807 block_env: block_env.clone(),
808 cfg_env: cfg.clone(),
809 block_idx,
810 tx_idx,
811 withdrawals: block.withdrawals.clone(),
812 };
813 print_error_with_state(
814 &debug_info,
815 evm.ctx().db_ref(),
816 test_case.post_state.as_ref(),
817 );
818 }
819 if json_output {
820 let output = json!({
821 "block": block_idx,
822 "tx": tx_idx,
823 "error": "missing sender",
824 "status": "skipped"
825 });
826 print_json(&output);
827 } else {
828 eprintln!("ā ļø Skipping block {block_idx} due to missing sender");
829 }
830 block_completed = false;
831 break; }
833
834 let tx_env = match tx.to_tx_env() {
835 Ok(env) => env,
836 Err(e) => {
837 if should_fail {
838 continue;
840 }
841 if print_env_on_error {
842 let debug_info = DebugInfo {
843 pre_state: pre_state_debug.clone(),
844 tx_env: None,
845 block_env: block_env.clone(),
846 cfg_env: cfg.clone(),
847 block_idx,
848 tx_idx,
849 withdrawals: block.withdrawals.clone(),
850 };
851 print_error_with_state(
852 &debug_info,
853 evm.ctx().db_ref(),
854 test_case.post_state.as_ref(),
855 );
856 }
857 if json_output {
858 let output = json!({
859 "block": block_idx,
860 "tx": tx_idx,
861 "error": format!("tx env creation error: {e}"),
862 "status": "skipped"
863 });
864 print_json(&output);
865 } else {
866 eprintln!(
867 "ā ļø Skipping block {block_idx} due to transaction env creation error: {e}"
868 );
869 }
870 block_completed = false;
871 break; }
873 };
874
875 evm.db_mut().bump_bal_index();
877
878 let execution_result = if json_output {
880 evm.inspect_tx(tx_env.clone())
881 } else {
882 evm.transact(tx_env.clone())
883 };
884
885 match execution_result {
886 Ok(result) => {
887 if should_fail {
888 if print_env_on_error {
891 if json_output {
893 eprintln!("=== Transaction trace (unexpected success) ===");
894 }
895 let _ = evm.inspect_tx(tx_env.clone());
896 }
897
898 if print_env_on_error {
899 let debug_info = DebugInfo {
900 pre_state: pre_state_debug.clone(),
901 tx_env: Some(tx_env.clone()),
902 block_env: block_env.clone(),
903 cfg_env: cfg.clone(),
904 block_idx,
905 tx_idx,
906 withdrawals: block.withdrawals.clone(),
907 };
908 print_error_with_state(
909 &debug_info,
910 evm.ctx().db_ref(),
911 test_case.post_state.as_ref(),
912 );
913 }
914 let expected_exception = block.expect_exception.clone().unwrap_or_default();
915 if json_output {
916 let output = json!({
917 "block": block_idx,
918 "tx": tx_idx,
919 "expected_exception": expected_exception,
920 "gas_used": result.result.gas().tx_gas_used(),
921 "status": "unexpected_success"
922 });
923 print_json(&output);
924 } else {
925 eprintln!(
926 "ā ļø Skipping block {block_idx}: transaction unexpectedly succeeded (expected failure: {expected_exception})"
927 );
928 }
929 block_completed = false;
930 break; }
932 let gas = result.result.gas();
934 cumulative_tx_gas_used += gas.tx_gas_used();
935 block_regular_gas_used += gas.block_regular_gas_used();
936 block_state_gas_used += gas.block_state_gas_used();
937 evm.commit(result.state);
938 }
939 Err(e) => {
940 if !should_fail {
941 if print_env_on_error {
943 if json_output {
944 eprintln!("=== Transaction trace (unexpected failure) ===");
945 }
946 let _ = evm.inspect_tx(tx_env.clone());
947 }
948
949 if print_env_on_error {
950 let debug_info = DebugInfo {
951 pre_state: pre_state_debug.clone(),
952 tx_env: Some(tx_env.clone()),
953 block_env: block_env.clone(),
954 cfg_env: cfg.clone(),
955 block_idx,
956 tx_idx,
957 withdrawals: block.withdrawals.clone(),
958 };
959 print_error_with_state(
960 &debug_info,
961 evm.ctx().db_ref(),
962 test_case.post_state.as_ref(),
963 );
964 }
965 if json_output {
966 let output = json!({
967 "block": block_idx,
968 "tx": tx_idx,
969 "error": format!("{e:?}"),
970 "status": "unexpected_failure"
971 });
972 print_json(&output);
973 } else {
974 eprintln!(
975 "ā ļø Skipping block {block_idx} due to unexpected failure: {e:?}"
976 );
977 }
978 block_completed = false;
979 break; } else if json_output {
981 let output = json!({
983 "block": block_idx,
984 "tx": tx_idx,
985 "error": format!("{e:?}"),
986 "status": "expected_failure"
987 });
988 print_json(&output);
989 }
990 }
991 }
992 }
993
994 if block_completed && !should_fail {
998 if let Some(block_header) = block.block_header.as_ref() {
999 let expected_gas_used = block_header.gas_used.to::<u64>();
1000 let actual_block_gas_used = if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
1001 block_regular_gas_used.max(block_state_gas_used)
1002 } else {
1003 cumulative_tx_gas_used
1004 };
1005 if actual_block_gas_used != expected_gas_used {
1006 if print_env_on_error {
1007 eprintln!(
1008 "Block gas used mismatch at block {block_idx}: expected {expected_gas_used}, got {actual_block_gas_used} (regular: {block_regular_gas_used}, state: {block_state_gas_used}, tx: {cumulative_tx_gas_used})"
1009 );
1010 }
1011 return Err(TestExecutionError::BlockGasUsedMismatch {
1012 block_idx,
1013 expected: expected_gas_used,
1014 actual: actual_block_gas_used,
1015 });
1016 }
1017 }
1018 }
1019
1020 evm.db_mut().bump_bal_index();
1022
1023 post_block::post_block_transition(
1025 &mut evm,
1026 &block_env,
1027 block.withdrawals.as_deref().unwrap_or_default(),
1028 spec_id,
1029 )
1030 .map_err(|e| TestExecutionError::PostBlockSystemCall {
1031 block_idx,
1032 error: format!("{e:?}"),
1033 })?;
1034
1035 state
1037 .block_hashes
1038 .insert(block_env.number.to::<u64>(), block_hash.unwrap_or_default());
1039
1040 if let Some(bal) = state.bal_state.bal_builder.take() {
1041 if let Some(state_bal) = bal_test {
1042 if &bal != state_bal.as_ref() {
1043 println!("Bal mismatch");
1044 println!("Test bal");
1045 state_bal.pretty_print();
1046 println!("Bal:");
1047 bal.pretty_print();
1048 return Err(TestExecutionError::BalMismatchError);
1049 }
1050 }
1051 }
1052
1053 parent_block_hash = block_hash;
1054 if let Some(excess_blob_gas) = this_excess_blob_gas {
1055 parent_excess_blob_gas = excess_blob_gas;
1056 }
1057
1058 state.merge_transitions(BundleRetention::Reverts);
1059 }
1060
1061 if let Some(expected_post_state) = &test_case.post_state {
1063 let debug_info = DebugInfo {
1065 pre_state: pre_state_debug.clone(),
1066 tx_env: None, block_env: block_env.clone(),
1068 cfg_env: cfg.clone(),
1069 block_idx: test_case.blocks.len(),
1070 tx_idx: 0,
1071 withdrawals: test_case.blocks.last().and_then(|b| b.withdrawals.clone()),
1072 };
1073 validate_post_state(
1074 &mut state,
1075 expected_post_state,
1076 &debug_info,
1077 print_env_on_error,
1078 )?;
1079 }
1080
1081 Ok(())
1082}
1083
1084fn fork_to_spec_id(fork: ForkSpec) -> SpecId {
1086 match fork {
1087 ForkSpec::Frontier => SpecId::FRONTIER,
1088 ForkSpec::Homestead | ForkSpec::FrontierToHomesteadAt5 => SpecId::HOMESTEAD,
1089 ForkSpec::EIP150 | ForkSpec::HomesteadToDaoAt5 | ForkSpec::HomesteadToEIP150At5 => {
1090 SpecId::TANGERINE
1091 }
1092 ForkSpec::EIP158 => SpecId::SPURIOUS_DRAGON,
1093 ForkSpec::Byzantium
1094 | ForkSpec::EIP158ToByzantiumAt5
1095 | ForkSpec::ByzantiumToConstantinopleFixAt5 => SpecId::BYZANTIUM,
1096 ForkSpec::Constantinople
1097 | ForkSpec::ByzantiumToConstantinopleAt5
1098 | ForkSpec::ConstantinopleFix => SpecId::PETERSBURG,
1099 ForkSpec::Istanbul => SpecId::ISTANBUL,
1100 ForkSpec::Berlin => SpecId::BERLIN,
1101 ForkSpec::London | ForkSpec::BerlinToLondonAt5 => SpecId::LONDON,
1102 ForkSpec::Paris | ForkSpec::ParisToShanghaiAtTime15k => SpecId::MERGE,
1103 ForkSpec::Shanghai => SpecId::SHANGHAI,
1104 ForkSpec::Cancun | ForkSpec::ShanghaiToCancunAtTime15k => SpecId::CANCUN,
1105 ForkSpec::Prague | ForkSpec::CancunToPragueAtTime15k => SpecId::PRAGUE,
1106 ForkSpec::Osaka | ForkSpec::PragueToOsakaAtTime15k => SpecId::OSAKA,
1107 ForkSpec::Amsterdam => SpecId::AMSTERDAM,
1108 _ => SpecId::AMSTERDAM, }
1110}
1111
1112fn skip_test(path: &Path) -> bool {
1114 let path_str = path.to_str().unwrap_or_default();
1115 if path_str.contains("paris/eip7610_create_collision")
1117 || path_str.contains("cancun/eip4844_blobs")
1118 || path_str.contains("prague/eip7251_consolidations")
1119 || path_str.contains("prague/eip7685_general_purpose_el_requests")
1120 || path_str.contains("prague/eip7002_el_triggerable_withdrawals")
1121 || path_str.contains("osaka/eip7918_blob_reserve_price")
1122 {
1123 return true;
1124 }
1125
1126 let name = path.file_name().unwrap().to_str().unwrap_or_default();
1127 matches!(
1129 name,
1130 "RevertInCreateInInit_Paris.json"
1132 | "RevertInCreateInInit.json"
1133 | "dynamicAccountOverwriteEmpty.json"
1134 | "dynamicAccountOverwriteEmpty_Paris.json"
1135 | "RevertInCreateInInitCreate2Paris.json"
1136 | "create2collisionStorage.json"
1137 | "RevertInCreateInInitCreate2.json"
1138 | "create2collisionStorageParis.json"
1139 | "InitCollision.json"
1140 | "InitCollisionParis.json"
1141
1142 | "ValueOverflow.json"
1144 | "ValueOverflowParis.json"
1145
1146 | "Call50000_sha256.json"
1148 | "static_Call50000_sha256.json"
1149 | "loopMul.json"
1150 | "CALLBlake2f_MaxRounds.json"
1151 | "scenarios.json"
1153 | "invalid_tx_max_fee_per_blob_gas.json"
1155 | "correct_increasing_blob_gas_costs.json"
1156 | "correct_decreasing_blob_gas_costs.json"
1157
1158 | "block_hashes_history.json"
1160 )
1161}
1162
1163#[derive(Debug, Error)]
1164pub enum TestExecutionError {
1165 #[error("Database error: {0}")]
1166 Database(String),
1167
1168 #[error("Skipped fork: {0}")]
1169 SkippedFork(String),
1170
1171 #[error("Sender is required")]
1172 SenderRequired,
1173
1174 #[error("Expected failure at block {block_idx}, tx {tx_idx}: {message}")]
1175 ExpectedFailure {
1176 block_idx: usize,
1177 tx_idx: usize,
1178 message: String,
1179 },
1180
1181 #[error("Unexpected failure at block {block_idx}, tx {tx_idx}: {error}")]
1182 UnexpectedFailure {
1183 block_idx: usize,
1184 tx_idx: usize,
1185 error: String,
1186 },
1187
1188 #[error("Transaction env creation failed at block {block_idx}, tx {tx_idx}: {error}")]
1189 TransactionEnvCreation {
1190 block_idx: usize,
1191 tx_idx: usize,
1192 error: String,
1193 },
1194
1195 #[error("Unexpected revert at block {block_idx}, tx {tx_idx}, gas used: {gas_used}")]
1196 UnexpectedRevert {
1197 block_idx: usize,
1198 tx_idx: usize,
1199 gas_used: u64,
1200 },
1201
1202 #[error("Unexpected halt at block {block_idx}, tx {tx_idx}: {reason:?}, gas used: {gas_used}")]
1203 UnexpectedHalt {
1204 block_idx: usize,
1205 tx_idx: usize,
1206 reason: HaltReason,
1207 gas_used: u64,
1208 },
1209
1210 #[error("Block gas used mismatch at block {block_idx}: expected {expected}, got {actual}")]
1211 BlockGasUsedMismatch {
1212 block_idx: usize,
1213 expected: u64,
1214 actual: u64,
1215 },
1216
1217 #[error("Pre-block system call failed at block {block_idx}: {error}")]
1218 PreBlockSystemCall { block_idx: usize, error: String },
1219
1220 #[error("Post-block system call failed at block {block_idx}: {error}")]
1221 PostBlockSystemCall { block_idx: usize, error: String },
1222
1223 #[error("BAL error")]
1224 BalMismatchError,
1225
1226 #[error(
1227 "Post-state validation failed for {address:?}.{field}: expected {expected}, got {actual}"
1228 )]
1229 PostStateValidation {
1230 address: Address,
1231 field: String,
1232 expected: String,
1233 actual: String,
1234 },
1235}
1236
1237#[derive(Debug, Error)]
1238pub enum Error {
1239 #[error("Path not found: {0}")]
1240 PathNotFound(PathBuf),
1241
1242 #[error("No JSON files found in: {0}")]
1243 NoJsonFiles(PathBuf),
1244
1245 #[error("Failed to read file {0}: {1}")]
1246 FileRead(PathBuf, std::io::Error),
1247
1248 #[error("Failed to decode JSON from {0}: {1}")]
1249 JsonDecode(PathBuf, serde_json::Error),
1250
1251 #[error("Test execution failed for {test_name} in {test_path}: {error}")]
1252 TestExecution {
1253 test_name: String,
1254 test_path: PathBuf,
1255 error: String,
1256 },
1257
1258 #[error("Directory traversal error: {0}")]
1259 WalkDir(#[from] walkdir::Error),
1260
1261 #[error("{failed} tests failed")]
1262 TestsFailed { failed: usize },
1263}