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