revme/cmd/
blockchaintest.rs

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