Skip to main content

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, AddressMap, U256Map, U256},
18    state::{bal::Bal, AccountInfo},
19    Context, Database, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
20};
21use serde_json::json;
22use std::{
23    collections::BTreeMap,
24    fs,
25    path::{Path, PathBuf},
26    sync::Arc,
27    time::Instant,
28};
29use thiserror::Error;
30
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: AddressMap<(AccountInfo, U256Map<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(state: &State<EmptyDB>) -> AddressMap<(AccountInfo, U256Map<U256>)> {
293        let mut committed_state = AddressMap::default();
294
295        // Access the cache state to get all accounts
296        for (address, cache_account) in &state.cache.accounts {
297            if let Some(plain_account) = &cache_account.account {
298                let mut storage = U256Map::default();
299                for (key, value) in &plain_account.storage {
300                    storage.insert(*key, *value);
301                }
302                committed_state.insert(*address, (plain_account.info.clone(), storage));
303            }
304        }
305
306        committed_state
307    }
308}
309
310/// Validate post state against expected values
311fn validate_post_state(
312    state: &mut State<EmptyDB>,
313    expected_post_state: &BTreeMap<Address, Account>,
314    debug_info: &DebugInfo,
315    print_env_on_error: bool,
316) -> Result<(), TestExecutionError> {
317    #[expect(clippy::too_many_arguments)]
318    fn make_failure(
319        state: &mut State<EmptyDB>,
320        debug_info: &DebugInfo,
321        expected_post_state: &BTreeMap<Address, Account>,
322        print_env_on_error: bool,
323        address: Address,
324        field: String,
325        expected: String,
326        actual: String,
327    ) -> Result<(), TestExecutionError> {
328        if print_env_on_error {
329            print_error_with_state(debug_info, state, Some(expected_post_state));
330        }
331        Err(TestExecutionError::PostStateValidation {
332            address,
333            field,
334            expected,
335            actual,
336        })
337    }
338
339    for (address, expected_account) in expected_post_state {
340        // Load account from final state. Info and storage are cloned so the borrow
341        // of `state` ends here and it can be queried again below (e.g. for code).
342        let (info, actual_storage) = {
343            let actual_account = state
344                .load_cache_account(*address)
345                .map_err(|e| TestExecutionError::Database(format!("Account load failed: {e}")))?;
346            let account = actual_account.account.as_ref();
347            (
348                account.map(|a| a.info.clone()).unwrap_or_default(),
349                account.map(|a| a.storage.clone()).unwrap_or_default(),
350            )
351        };
352
353        // Validate balance
354        if info.balance != expected_account.balance {
355            return make_failure(
356                state,
357                debug_info,
358                expected_post_state,
359                print_env_on_error,
360                *address,
361                "balance".to_string(),
362                format!("{}", expected_account.balance),
363                format!("{}", info.balance),
364            );
365        }
366
367        // Validate nonce
368        let expected_nonce = expected_account.nonce.to::<u64>();
369        if info.nonce != expected_nonce {
370            return make_failure(
371                state,
372                debug_info,
373                expected_post_state,
374                print_env_on_error,
375                *address,
376                "nonce".to_string(),
377                format!("{expected_nonce}"),
378                format!("{}", info.nonce),
379            );
380        }
381
382        // Validate code if present. The account may carry only the code hash when the
383        // code was never loaded during execution, so fall back to `code_by_hash`.
384        if !expected_account.code.is_empty() {
385            let actual_code = match info.code.clone() {
386                Some(code) => Some(code),
387                None if !info.is_empty_code_hash() => {
388                    Some(Database::code_by_hash(state, info.code_hash).map_err(|e| {
389                        TestExecutionError::Database(format!("Code load failed: {e}"))
390                    })?)
391                }
392                None => None,
393            };
394            if let Some(actual_code) = &actual_code {
395                if actual_code.original_bytes() != expected_account.code {
396                    return make_failure(
397                        state,
398                        debug_info,
399                        expected_post_state,
400                        print_env_on_error,
401                        *address,
402                        "code".to_string(),
403                        format!("0x{}", hex::encode(&expected_account.code)),
404                        format!("0x{}", hex::encode(actual_code.original_byte_slice())),
405                    );
406                }
407            } else {
408                return make_failure(
409                    state,
410                    debug_info,
411                    expected_post_state,
412                    print_env_on_error,
413                    *address,
414                    "code".to_string(),
415                    format!("0x{}", hex::encode(&expected_account.code)),
416                    "empty".to_string(),
417                );
418            }
419        }
420
421        // Check for unexpected storage entries.
422        for (slot, actual_value) in &actual_storage {
423            let slot = *slot;
424            let actual_value = *actual_value;
425            if !expected_account.storage.contains_key(&slot) && !actual_value.is_zero() {
426                return make_failure(
427                    state,
428                    debug_info,
429                    expected_post_state,
430                    print_env_on_error,
431                    *address,
432                    format!("storage_unexpected[{slot}]"),
433                    "0x0".to_string(),
434                    format!("{actual_value}"),
435                );
436            }
437        }
438
439        // Validate storage slots
440        for (slot, expected_value) in &expected_account.storage {
441            let actual_value = state.storage(*address, *slot);
442            let actual_value = actual_value.unwrap_or_default();
443
444            if actual_value != *expected_value {
445                return make_failure(
446                    state,
447                    debug_info,
448                    expected_post_state,
449                    print_env_on_error,
450                    *address,
451                    format!("storage_validation[{slot}]"),
452                    format!("{expected_value}"),
453                    format!("{actual_value}"),
454                );
455            }
456        }
457    }
458    Ok(())
459}
460
461/// Print comprehensive error information including environment and state comparison
462fn print_error_with_state(
463    debug_info: &DebugInfo,
464    current_state: &State<EmptyDB>,
465    expected_post_state: Option<&BTreeMap<Address, Account>>,
466) {
467    eprintln!("\n========== TEST EXECUTION ERROR ==========");
468
469    // Print error location
470    eprintln!(
471        "\nšŸ“ Error occurred at block {} transaction {}",
472        debug_info.block_idx, debug_info.tx_idx
473    );
474
475    // Print configuration environment
476    eprintln!("\nšŸ“‹ Configuration Environment:");
477    eprintln!("  Spec ID: {:?}", debug_info.cfg_env.spec());
478    eprintln!("  Chain ID: {}", debug_info.cfg_env.chain_id);
479    eprintln!(
480        "  Limit contract code size: {:?}",
481        debug_info.cfg_env.limit_contract_code_size
482    );
483    eprintln!(
484        "  Limit contract initcode size: {:?}",
485        debug_info.cfg_env.limit_contract_initcode_size
486    );
487
488    // Print block environment
489    eprintln!("\nšŸ”Ø Block Environment:");
490    eprintln!("  Number: {}", debug_info.block_env.number);
491    eprintln!("  Timestamp: {}", debug_info.block_env.timestamp);
492    eprintln!("  Gas limit: {}", debug_info.block_env.gas_limit);
493    eprintln!("  Base fee: {:?}", debug_info.block_env.basefee);
494    eprintln!("  Difficulty: {}", debug_info.block_env.difficulty);
495    eprintln!("  Prevrandao: {:?}", debug_info.block_env.prevrandao);
496    eprintln!("  Beneficiary: {:?}", debug_info.block_env.beneficiary);
497    let blob = debug_info.block_env.blob_excess_gas_and_price;
498    eprintln!("  Blob excess gas: {:?}", blob.map(|a| a.excess_blob_gas));
499    eprintln!("  Blob gas price: {:?}", blob.map(|a| a.blob_gasprice));
500
501    // Print withdrawals
502    if let Some(withdrawals) = &debug_info.withdrawals {
503        eprintln!("  Withdrawals: {} items", withdrawals.len());
504        if !withdrawals.is_empty() {
505            for (i, withdrawal) in withdrawals.iter().enumerate().take(3) {
506                eprintln!("    Withdrawal {i}:");
507                eprintln!("      Index: {}", withdrawal.index);
508                eprintln!("      Validator Index: {}", withdrawal.validator_index);
509                eprintln!("      Address: {:?}", withdrawal.address);
510                eprintln!(
511                    "      Amount: {} Gwei ({:.6} ETH)",
512                    withdrawal.amount,
513                    withdrawal.amount.to::<u128>() as f64 / 1_000_000_000.0
514                );
515            }
516            if withdrawals.len() > 3 {
517                eprintln!("    ... and {} more withdrawals", withdrawals.len() - 3);
518            }
519        }
520    }
521
522    // Print transaction environment if available
523    if let Some(tx_env) = &debug_info.tx_env {
524        eprintln!("\nšŸ“„ Transaction Environment:");
525        eprintln!("  Transaction type: {}", tx_env.tx_type);
526        eprintln!("  Caller: {:?}", tx_env.caller);
527        eprintln!("  Gas limit: {}", tx_env.gas_limit);
528        eprintln!("  Gas price: {}", tx_env.gas_price);
529        eprintln!("  Gas priority fee: {:?}", tx_env.gas_priority_fee);
530        eprintln!("  Transaction kind: {:?}", tx_env.kind);
531        eprintln!("  Value: {}", tx_env.value);
532        eprintln!("  Data length: {} bytes", tx_env.data.len());
533        if !tx_env.data.is_empty() {
534            let preview_len = std::cmp::min(64, tx_env.data.len());
535            eprintln!(
536                "  Data preview: 0x{}{}",
537                hex::encode(&tx_env.data[..preview_len]),
538                if tx_env.data.len() > 64 { "..." } else { "" }
539            );
540        }
541        eprintln!("  Nonce: {}", tx_env.nonce);
542        eprintln!("  Chain ID: {:?}", tx_env.chain_id);
543        eprintln!("  Access list: {} entries", tx_env.access_list.len());
544        if !tx_env.access_list.is_empty() {
545            for (i, access) in tx_env.access_list.iter().enumerate().take(3) {
546                eprintln!(
547                    "    Access {}: address={:?}, {} storage keys",
548                    i,
549                    access.address,
550                    access.storage_keys.len()
551                );
552            }
553            if tx_env.access_list.len() > 3 {
554                eprintln!(
555                    "    ... and {} more access list entries",
556                    tx_env.access_list.len() - 3
557                );
558            }
559        }
560        eprintln!("  Blob hashes: {} blobs", tx_env.blob_hashes.len());
561        if !tx_env.blob_hashes.is_empty() {
562            for (i, hash) in tx_env.blob_hashes.iter().enumerate().take(3) {
563                eprintln!("    Blob {i}: {hash:?}");
564            }
565            if tx_env.blob_hashes.len() > 3 {
566                eprintln!(
567                    "    ... and {} more blob hashes",
568                    tx_env.blob_hashes.len() - 3
569                );
570            }
571        }
572        eprintln!("  Max fee per blob gas: {}", tx_env.max_fee_per_blob_gas);
573        eprintln!(
574            "  Authorization list: {} items",
575            tx_env.authorization_list.len()
576        );
577        if !tx_env.authorization_list.is_empty() {
578            eprintln!("    (EIP-7702 authorizations present)");
579        }
580    } else {
581        eprintln!(
582            "\nšŸ“„ Transaction Environment: Not available (error occurred before tx creation)"
583        );
584    }
585
586    // Print state comparison
587    eprintln!("\nšŸ’¾ Pre-State (Initial):");
588    // Sort accounts by address for consistent output
589    let mut sorted_accounts: Vec<_> = debug_info.pre_state.iter().collect();
590    sorted_accounts.sort_by_key(|(addr, _)| *addr);
591    for (address, (info, storage)) in sorted_accounts {
592        eprintln!("  Account {address:?}:");
593        eprintln!("    Balance: 0x{:x}", info.balance);
594        eprintln!("    Nonce: {}", info.nonce);
595        eprintln!("    Code hash: {:?}", info.code_hash);
596        eprintln!(
597            "    Code size: {} bytes",
598            info.code.as_ref().map_or(0, |c| c.len())
599        );
600        if !storage.is_empty() {
601            eprintln!("    Storage ({} slots):", storage.len());
602            let mut sorted_storage: Vec<_> = storage.iter().collect();
603            sorted_storage.sort_by_key(|(key, _)| *key);
604            for (key, value) in sorted_storage.iter() {
605                eprintln!("      {key:?} => {value:?}");
606            }
607        }
608    }
609
610    eprintln!("\nšŸ“ Current State (Actual):");
611    let committed_state = DebugInfo::capture_committed_state(current_state);
612    // Sort accounts by address for consistent output
613    let mut sorted_current: Vec<_> = committed_state.iter().collect();
614    sorted_current.sort_by_key(|(addr, _)| *addr);
615    for (address, (info, storage)) in sorted_current {
616        eprintln!("  Account {address:?}:");
617        eprintln!("    Balance: 0x{:x}", info.balance);
618        eprintln!("    Nonce: {}", info.nonce);
619        eprintln!("    Code hash: {:?}", info.code_hash);
620        eprintln!(
621            "    Code size: {} bytes",
622            info.code.as_ref().map_or(0, |c| c.len())
623        );
624        if !storage.is_empty() {
625            eprintln!("    Storage ({} slots):", storage.len());
626            let mut sorted_storage: Vec<_> = storage.iter().collect();
627            sorted_storage.sort_by_key(|(key, _)| *key);
628            for (key, value) in sorted_storage.iter() {
629                eprintln!("      {key:?} => {value:?}");
630            }
631        }
632    }
633
634    // Print expected post-state if available
635    if let Some(expected_post_state) = expected_post_state {
636        eprintln!("\nāœ… Expected Post-State:");
637        for (address, account) in expected_post_state {
638            eprintln!("  Account {address:?}:");
639            eprintln!("    Balance: 0x{:x}", account.balance);
640            eprintln!("    Nonce: {}", account.nonce);
641            if !account.code.is_empty() {
642                eprintln!("    Code size: {} bytes", account.code.len());
643            }
644            if !account.storage.is_empty() {
645                eprintln!("    Storage ({} slots):", account.storage.len());
646                for (key, value) in account.storage.iter() {
647                    eprintln!("      {key:?} => {value:?}");
648                }
649            }
650        }
651    }
652
653    eprintln!("\n===========================================\n");
654}
655
656/// Execute a single blockchain test case
657fn execute_blockchain_test(
658    test_case: &BlockchainTestCase,
659    print_env_on_error: bool,
660    json_output: bool,
661) -> Result<(), TestExecutionError> {
662    // Skip all transition forks for now.
663    if matches!(
664        test_case.network,
665        ForkSpec::ByzantiumToConstantinopleAt5
666            | ForkSpec::ParisToShanghaiAtTime15k
667            | ForkSpec::ShanghaiToCancunAtTime15k
668            | ForkSpec::CancunToPragueAtTime15k
669            | ForkSpec::PragueToOsakaAtTime15k
670            | ForkSpec::BPO1ToBPO2AtTime15k
671            | ForkSpec::BPO2ToAmsterdamAtTime15k
672    ) {
673        eprintln!("āš ļø  Skipping transition fork: {:?}", test_case.network);
674        return Ok(());
675    }
676
677    // Create database with initial state
678    let mut state = State::builder().with_bal_builder().build();
679
680    // Capture pre-state for debug info
681    let mut pre_state_debug = AddressMap::default();
682
683    // Insert genesis state into database. Bytecode is stored separately from the
684    // account (in the contracts map, keyed by code hash) so that execution has to
685    // fetch it through `Database::code_by_hash`, like a node's state provider would
686    // serve it.
687    let genesis_state = test_case.pre.clone().into_genesis_state();
688    for (address, account) in genesis_state {
689        let code_hash = revm::primitives::keccak256(&account.code);
690        let bytecode = (!account.code.is_empty()).then(|| Bytecode::new_raw(account.code.clone()));
691        let account_info = AccountInfo {
692            balance: account.balance,
693            nonce: account.nonce,
694            code_hash,
695            code: None,
696            account_id: None,
697        };
698
699        if let Some(bytecode) = &bytecode {
700            state.cache.contracts.insert(code_hash, bytecode.clone());
701        }
702
703        // Store for debug info, with the code inlined so it shows up in the debug print.
704        if print_env_on_error {
705            pre_state_debug.insert(
706                address,
707                (
708                    AccountInfo {
709                        code: bytecode,
710                        ..account_info.clone()
711                    },
712                    account.storage.clone(),
713                ),
714            );
715        }
716
717        state.insert_account_with_storage(address, account_info, account.storage);
718    }
719
720    // insert genesis hash
721    state
722        .block_hashes
723        .insert(0, test_case.genesis_block_header.hash);
724
725    // Setup configuration based on fork
726    let spec_id = fork_to_spec_id(test_case.network);
727    let mut cfg = CfgEnv::default();
728    cfg.set_spec_and_mainnet_gas_params(spec_id);
729
730    // Genesis block is not used yet.
731    let mut parent_block_hash = Some(test_case.genesis_block_header.hash);
732    let mut parent_excess_blob_gas = test_case
733        .genesis_block_header
734        .excess_blob_gas
735        .unwrap_or_default()
736        .to::<u64>();
737    let mut block_env = test_case.genesis_block_env();
738
739    // Process each block in the test
740    for (block_idx, block) in test_case.blocks.iter().enumerate() {
741        if !json_output {
742            println!("Run block {block_idx}/{}", test_case.blocks.len());
743        }
744
745        // Check if this block should fail
746        let should_fail = block.expect_exception.is_some();
747
748        let transactions = block.transactions.as_deref().unwrap_or_default();
749
750        // Update block environment for this blockk
751
752        let mut block_hash = None;
753        let mut beacon_root = None;
754        let this_excess_blob_gas;
755
756        if let Some(block_header) = block.block_header.as_ref() {
757            block_hash = Some(block_header.hash);
758            beacon_root = block_header.parent_beacon_block_root;
759            block_env = block_header.to_block_env(Some(BlobExcessGasAndPrice::new_with_spec(
760                parent_excess_blob_gas,
761                spec_id,
762            )));
763            this_excess_blob_gas = block_header.excess_blob_gas.map(|i| i.to::<u64>());
764        } else {
765            this_excess_blob_gas = None;
766        }
767
768        let bal_test = block
769            .block_access_list
770            .as_ref()
771            .and_then(|bal| Bal::try_from(bal.clone()).ok())
772            .map(Arc::new);
773
774        //state.set_bal(bal_test);
775        state.reset_bal_index();
776
777        // Create EVM context for each transaction to ensure fresh state access
778        let evm_context = Context::mainnet()
779            .with_block(&block_env)
780            .with_cfg(cfg.clone())
781            .with_db(&mut state);
782
783        // Build and execute with EVM - always use inspector when JSON output is enabled
784        let mut evm = evm_context.build_mainnet_with_inspector(TracerEip3155::new_stdout());
785
786        // Pre block system calls
787        pre_block::pre_block_transition(&mut evm, spec_id, parent_block_hash, beacon_root)
788            .map_err(|e| TestExecutionError::PreBlockSystemCall {
789                block_idx,
790                error: format!("{e:?}"),
791            })?;
792
793        // Track cumulative gas used across all transactions in this block.
794        // EIP-8037: Split gas accounting into regular (execution) and state gas.
795        let mut cumulative_tx_gas_used: u64 = 0;
796        let mut block_regular_gas_used: u64 = 0;
797        let mut block_state_gas_used: u64 = 0;
798        let mut block_completed = true;
799
800        // Execute each transaction in the block
801        for (tx_idx, tx) in transactions.iter().enumerate() {
802            if tx.sender.is_none() {
803                if print_env_on_error {
804                    let debug_info = DebugInfo {
805                        pre_state: pre_state_debug.clone(),
806                        tx_env: None,
807                        block_env: block_env.clone(),
808                        cfg_env: cfg.clone(),
809                        block_idx,
810                        tx_idx,
811                        withdrawals: block.withdrawals.clone(),
812                    };
813                    print_error_with_state(
814                        &debug_info,
815                        evm.ctx().db_ref(),
816                        test_case.post_state.as_ref(),
817                    );
818                }
819                if json_output {
820                    let output = json!({
821                        "block": block_idx,
822                        "tx": tx_idx,
823                        "error": "missing sender",
824                        "status": "skipped"
825                    });
826                    print_json(&output);
827                } else {
828                    eprintln!("āš ļø  Skipping block {block_idx} due to missing sender");
829                }
830                block_completed = false;
831                break; // Skip to next block
832            }
833
834            let tx_env = match tx.to_tx_env() {
835                Ok(env) => env,
836                Err(e) => {
837                    if should_fail {
838                        // Expected failure during tx env creation
839                        continue;
840                    }
841                    if print_env_on_error {
842                        let debug_info = DebugInfo {
843                            pre_state: pre_state_debug.clone(),
844                            tx_env: None,
845                            block_env: block_env.clone(),
846                            cfg_env: cfg.clone(),
847                            block_idx,
848                            tx_idx,
849                            withdrawals: block.withdrawals.clone(),
850                        };
851                        print_error_with_state(
852                            &debug_info,
853                            evm.ctx().db_ref(),
854                            test_case.post_state.as_ref(),
855                        );
856                    }
857                    if json_output {
858                        let output = json!({
859                            "block": block_idx,
860                            "tx": tx_idx,
861                            "error": format!("tx env creation error: {e}"),
862                            "status": "skipped"
863                        });
864                        print_json(&output);
865                    } else {
866                        eprintln!(
867                            "āš ļø  Skipping block {block_idx} due to transaction env creation error: {e}"
868                        );
869                    }
870                    block_completed = false;
871                    break; // Skip to next block
872                }
873            };
874
875            // bump bal index
876            evm.db_mut().bump_bal_index();
877
878            // If JSON output requested, output transaction details
879            let execution_result = if json_output {
880                evm.inspect_tx(tx_env.clone())
881            } else {
882                evm.transact(tx_env.clone())
883            };
884
885            match execution_result {
886                Ok(result) => {
887                    if should_fail {
888                        // Unexpected success - should have failed but didn't
889                        // If not expected to fail, use inspector to trace the transaction
890                        if print_env_on_error {
891                            // Re-run with inspector to get detailed trace
892                            if json_output {
893                                eprintln!("=== Transaction trace (unexpected success) ===");
894                            }
895                            let _ = evm.inspect_tx(tx_env.clone());
896                        }
897
898                        if print_env_on_error {
899                            let debug_info = DebugInfo {
900                                pre_state: pre_state_debug.clone(),
901                                tx_env: Some(tx_env.clone()),
902                                block_env: block_env.clone(),
903                                cfg_env: cfg.clone(),
904                                block_idx,
905                                tx_idx,
906                                withdrawals: block.withdrawals.clone(),
907                            };
908                            print_error_with_state(
909                                &debug_info,
910                                evm.ctx().db_ref(),
911                                test_case.post_state.as_ref(),
912                            );
913                        }
914                        let expected_exception = block.expect_exception.clone().unwrap_or_default();
915                        if json_output {
916                            let output = json!({
917                                "block": block_idx,
918                                "tx": tx_idx,
919                                "expected_exception": expected_exception,
920                                "gas_used": result.result.gas().tx_gas_used(),
921                                "status": "unexpected_success"
922                            });
923                            print_json(&output);
924                        } else {
925                            eprintln!(
926                                "āš ļø  Skipping block {block_idx}: transaction unexpectedly succeeded (expected failure: {expected_exception})"
927                            );
928                        }
929                        block_completed = false;
930                        break; // Skip to next block
931                    }
932                    // EIP-8037: Split gas accounting.
933                    let gas = result.result.gas();
934                    cumulative_tx_gas_used += gas.tx_gas_used();
935                    block_regular_gas_used += gas.block_regular_gas_used();
936                    block_state_gas_used += gas.block_state_gas_used();
937                    evm.commit(result.state);
938                }
939                Err(e) => {
940                    if !should_fail {
941                        // Unexpected error - use inspector to trace the transaction
942                        if print_env_on_error {
943                            if json_output {
944                                eprintln!("=== Transaction trace (unexpected failure) ===");
945                            }
946                            let _ = evm.inspect_tx(tx_env.clone());
947                        }
948
949                        if print_env_on_error {
950                            let debug_info = DebugInfo {
951                                pre_state: pre_state_debug.clone(),
952                                tx_env: Some(tx_env.clone()),
953                                block_env: block_env.clone(),
954                                cfg_env: cfg.clone(),
955                                block_idx,
956                                tx_idx,
957                                withdrawals: block.withdrawals.clone(),
958                            };
959                            print_error_with_state(
960                                &debug_info,
961                                evm.ctx().db_ref(),
962                                test_case.post_state.as_ref(),
963                            );
964                        }
965                        if json_output {
966                            let output = json!({
967                                "block": block_idx,
968                                "tx": tx_idx,
969                                "error": format!("{e:?}"),
970                                "status": "unexpected_failure"
971                            });
972                            print_json(&output);
973                        } else {
974                            eprintln!(
975                                "āš ļø  Skipping block {block_idx} due to unexpected failure: {e:?}"
976                            );
977                        }
978                        block_completed = false;
979                        break; // Skip to next block
980                    } else if json_output {
981                        // Expected failure
982                        let output = json!({
983                            "block": block_idx,
984                            "tx": tx_idx,
985                            "error": format!("{e:?}"),
986                            "status": "expected_failure"
987                        });
988                        print_json(&output);
989                    }
990                }
991            }
992        }
993
994        // Validate block gas used against header.
995        // EIP-8037 (Amsterdam+): block gas_used = max(regular_gas, state_gas).
996        // Pre-Amsterdam: block gas_used = cumulative tx_gas_used (includes refunds).
997        if block_completed && !should_fail {
998            if let Some(block_header) = block.block_header.as_ref() {
999                let expected_gas_used = block_header.gas_used.to::<u64>();
1000                let actual_block_gas_used = if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
1001                    block_regular_gas_used.max(block_state_gas_used)
1002                } else {
1003                    cumulative_tx_gas_used
1004                };
1005                if actual_block_gas_used != expected_gas_used {
1006                    if print_env_on_error {
1007                        eprintln!(
1008                            "Block gas used mismatch at block {block_idx}: expected {expected_gas_used}, got {actual_block_gas_used} (regular: {block_regular_gas_used}, state: {block_state_gas_used}, tx: {cumulative_tx_gas_used})"
1009                        );
1010                    }
1011                    return Err(TestExecutionError::BlockGasUsedMismatch {
1012                        block_idx,
1013                        expected: expected_gas_used,
1014                        actual: actual_block_gas_used,
1015                    });
1016                }
1017            }
1018        }
1019
1020        // bump bal index
1021        evm.db_mut().bump_bal_index();
1022
1023        // uncle rewards are not implemented yet
1024        post_block::post_block_transition(
1025            &mut evm,
1026            &block_env,
1027            block.withdrawals.as_deref().unwrap_or_default(),
1028            spec_id,
1029        )
1030        .map_err(|e| TestExecutionError::PostBlockSystemCall {
1031            block_idx,
1032            error: format!("{e:?}"),
1033        })?;
1034
1035        // insert present block hash.
1036        state
1037            .block_hashes
1038            .insert(block_env.number.to::<u64>(), block_hash.unwrap_or_default());
1039
1040        if let Some(bal) = state.bal_state.bal_builder.take() {
1041            if let Some(state_bal) = bal_test {
1042                if &bal != state_bal.as_ref() {
1043                    println!("Bal mismatch");
1044                    println!("Test bal");
1045                    state_bal.pretty_print();
1046                    println!("Bal:");
1047                    bal.pretty_print();
1048                    return Err(TestExecutionError::BalMismatchError);
1049                }
1050            }
1051        }
1052
1053        parent_block_hash = block_hash;
1054        if let Some(excess_blob_gas) = this_excess_blob_gas {
1055            parent_excess_blob_gas = excess_blob_gas;
1056        }
1057
1058        state.merge_transitions(BundleRetention::Reverts);
1059    }
1060
1061    // Validate post state if present
1062    if let Some(expected_post_state) = &test_case.post_state {
1063        // Create debug info for post-state validation
1064        let debug_info = DebugInfo {
1065            pre_state: pre_state_debug.clone(),
1066            tx_env: None, // Last transaction is done
1067            block_env: block_env.clone(),
1068            cfg_env: cfg.clone(),
1069            block_idx: test_case.blocks.len(),
1070            tx_idx: 0,
1071            withdrawals: test_case.blocks.last().and_then(|b| b.withdrawals.clone()),
1072        };
1073        validate_post_state(
1074            &mut state,
1075            expected_post_state,
1076            &debug_info,
1077            print_env_on_error,
1078        )?;
1079    }
1080
1081    Ok(())
1082}
1083
1084/// Convert ForkSpec to SpecId
1085fn fork_to_spec_id(fork: ForkSpec) -> SpecId {
1086    match fork {
1087        ForkSpec::Frontier => SpecId::FRONTIER,
1088        ForkSpec::Homestead | ForkSpec::FrontierToHomesteadAt5 => SpecId::HOMESTEAD,
1089        ForkSpec::EIP150 | ForkSpec::HomesteadToDaoAt5 | ForkSpec::HomesteadToEIP150At5 => {
1090            SpecId::TANGERINE
1091        }
1092        ForkSpec::EIP158 => SpecId::SPURIOUS_DRAGON,
1093        ForkSpec::Byzantium
1094        | ForkSpec::EIP158ToByzantiumAt5
1095        | ForkSpec::ByzantiumToConstantinopleFixAt5 => SpecId::BYZANTIUM,
1096        ForkSpec::Constantinople
1097        | ForkSpec::ByzantiumToConstantinopleAt5
1098        | ForkSpec::ConstantinopleFix => SpecId::PETERSBURG,
1099        ForkSpec::Istanbul => SpecId::ISTANBUL,
1100        ForkSpec::Berlin => SpecId::BERLIN,
1101        ForkSpec::London | ForkSpec::BerlinToLondonAt5 => SpecId::LONDON,
1102        ForkSpec::Paris | ForkSpec::ParisToShanghaiAtTime15k => SpecId::MERGE,
1103        ForkSpec::Shanghai => SpecId::SHANGHAI,
1104        ForkSpec::Cancun | ForkSpec::ShanghaiToCancunAtTime15k => SpecId::CANCUN,
1105        ForkSpec::Prague | ForkSpec::CancunToPragueAtTime15k => SpecId::PRAGUE,
1106        ForkSpec::Osaka | ForkSpec::PragueToOsakaAtTime15k => SpecId::OSAKA,
1107        ForkSpec::Amsterdam => SpecId::AMSTERDAM,
1108        _ => SpecId::AMSTERDAM, // For any unknown forks, use latest available
1109    }
1110}
1111
1112/// Check if a test should be skipped based on its filename
1113fn skip_test(path: &Path) -> bool {
1114    let path_str = path.to_str().unwrap_or_default();
1115    // blobs excess gas calculation is not supported or osaka BPO configuration
1116    if path_str.contains("paris/eip7610_create_collision")
1117        || path_str.contains("cancun/eip4844_blobs")
1118        || path_str.contains("prague/eip7251_consolidations")
1119        || path_str.contains("prague/eip7685_general_purpose_el_requests")
1120        || path_str.contains("prague/eip7002_el_triggerable_withdrawals")
1121        || path_str.contains("osaka/eip7918_blob_reserve_price")
1122    {
1123        return true;
1124    }
1125
1126    let name = path.file_name().unwrap().to_str().unwrap_or_default();
1127    // Add any problematic tests here that should be skipped
1128    matches!(
1129        name,
1130        // Test with some storage check.
1131        "RevertInCreateInInit_Paris.json"
1132        | "RevertInCreateInInit.json"
1133        | "dynamicAccountOverwriteEmpty.json"
1134        | "dynamicAccountOverwriteEmpty_Paris.json"
1135        | "RevertInCreateInInitCreate2Paris.json"
1136        | "create2collisionStorage.json"
1137        | "RevertInCreateInInitCreate2.json"
1138        | "create2collisionStorageParis.json"
1139        | "InitCollision.json"
1140        | "InitCollisionParis.json"
1141
1142        // Malformed value.
1143        | "ValueOverflow.json"
1144        | "ValueOverflowParis.json"
1145
1146        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
1147        | "Call50000_sha256.json"
1148        | "static_Call50000_sha256.json"
1149        | "loopMul.json"
1150        | "CALLBlake2f_MaxRounds.json"
1151        // TODO tests not checked, maybe related to parent block hashes as it is currently not supported in test.
1152        | "scenarios.json"
1153        // IT seems that post state is wrong, we properly handle max blob gas and state should stay the same.
1154        | "invalid_tx_max_fee_per_blob_gas.json"
1155        | "correct_increasing_blob_gas_costs.json"
1156        | "correct_decreasing_blob_gas_costs.json"
1157
1158        // test-fixtures/main/develop/blockchain_tests/prague/eip2935_historical_block_hashes_from_state/block_hashes/block_hashes_history.json
1159        | "block_hashes_history.json"
1160    )
1161}
1162
1163#[derive(Debug, Error)]
1164pub enum TestExecutionError {
1165    #[error("Database error: {0}")]
1166    Database(String),
1167
1168    #[error("Skipped fork: {0}")]
1169    SkippedFork(String),
1170
1171    #[error("Sender is required")]
1172    SenderRequired,
1173
1174    #[error("Expected failure at block {block_idx}, tx {tx_idx}: {message}")]
1175    ExpectedFailure {
1176        block_idx: usize,
1177        tx_idx: usize,
1178        message: String,
1179    },
1180
1181    #[error("Unexpected failure at block {block_idx}, tx {tx_idx}: {error}")]
1182    UnexpectedFailure {
1183        block_idx: usize,
1184        tx_idx: usize,
1185        error: String,
1186    },
1187
1188    #[error("Transaction env creation failed at block {block_idx}, tx {tx_idx}: {error}")]
1189    TransactionEnvCreation {
1190        block_idx: usize,
1191        tx_idx: usize,
1192        error: String,
1193    },
1194
1195    #[error("Unexpected revert at block {block_idx}, tx {tx_idx}, gas used: {gas_used}")]
1196    UnexpectedRevert {
1197        block_idx: usize,
1198        tx_idx: usize,
1199        gas_used: u64,
1200    },
1201
1202    #[error("Unexpected halt at block {block_idx}, tx {tx_idx}: {reason:?}, gas used: {gas_used}")]
1203    UnexpectedHalt {
1204        block_idx: usize,
1205        tx_idx: usize,
1206        reason: HaltReason,
1207        gas_used: u64,
1208    },
1209
1210    #[error("Block gas used mismatch at block {block_idx}: expected {expected}, got {actual}")]
1211    BlockGasUsedMismatch {
1212        block_idx: usize,
1213        expected: u64,
1214        actual: u64,
1215    },
1216
1217    #[error("Pre-block system call failed at block {block_idx}: {error}")]
1218    PreBlockSystemCall { block_idx: usize, error: String },
1219
1220    #[error("Post-block system call failed at block {block_idx}: {error}")]
1221    PostBlockSystemCall { block_idx: usize, error: String },
1222
1223    #[error("BAL error")]
1224    BalMismatchError,
1225
1226    #[error(
1227        "Post-state validation failed for {address:?}.{field}: expected {expected}, got {actual}"
1228    )]
1229    PostStateValidation {
1230        address: Address,
1231        field: String,
1232        expected: String,
1233        actual: String,
1234    },
1235}
1236
1237#[derive(Debug, Error)]
1238pub enum Error {
1239    #[error("Path not found: {0}")]
1240    PathNotFound(PathBuf),
1241
1242    #[error("No JSON files found in: {0}")]
1243    NoJsonFiles(PathBuf),
1244
1245    #[error("Failed to read file {0}: {1}")]
1246    FileRead(PathBuf, std::io::Error),
1247
1248    #[error("Failed to decode JSON from {0}: {1}")]
1249    JsonDecode(PathBuf, serde_json::Error),
1250
1251    #[error("Test execution failed for {test_name} in {test_path}: {error}")]
1252    TestExecution {
1253        test_name: String,
1254        test_path: PathBuf,
1255        error: String,
1256    },
1257
1258    #[error("Directory traversal error: {0}")]
1259    WalkDir(#[from] walkdir::Error),
1260
1261    #[error("{failed} tests failed")]
1262    TestsFailed { failed: usize },
1263}