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