revme/cmd/
blockchaintest.rs

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