Skip to main content

revmc_statetest/btest/
mod.rs

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