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            block_env = block_header.to_block_env(Some(BlobExcessGasAndPrice::new_with_spec(
633                parent_excess_blob_gas,
634                spec_id,
635            )));
636            this_excess_blob_gas = block_header.excess_blob_gas.map(|i| i.to::<u64>());
637        } else {
638            this_excess_blob_gas = None;
639        }
640
641        let bal_test = block
642            .block_access_list
643            .as_ref()
644            .and_then(|bal| Bal::try_from(bal.clone()).ok())
645            .map(Arc::new);
646
647        //state.set_bal(bal_test);
648        state.reset_bal_index();
649
650        // Create EVM context for each transaction to ensure fresh state access
651        let evm_context =
652            Context::mainnet().with_block(&block_env).with_cfg(cfg.clone()).with_db(&mut state);
653
654        // Build and execute with EVM - always use inspector when JSON output is enabled
655        let mut evm = evm_context.build_mainnet_with_inspector(TracerEip3155::new_stdout());
656
657        // Pre block system calls
658        pre_block::pre_block_transition(&mut evm, spec_id, parent_block_hash, beacon_root)
659            .map_err(|e| TestExecutionError::PreBlockSystemCall {
660                block_idx,
661                error: format!("{e:?}"),
662            })?;
663
664        // Track cumulative gas used across all transactions in this block.
665        // EIP-8037: Split gas accounting into regular (execution) and state gas.
666        let mut cumulative_tx_gas_used: u64 = 0;
667        let mut block_regular_gas_used: u64 = 0;
668        let mut block_state_gas_used: u64 = 0;
669        let mut block_completed = true;
670
671        // Execute each transaction in the block
672        for (tx_idx, tx) in transactions.iter().enumerate() {
673            if tx.sender.is_none() {
674                if print_env_on_error {
675                    let debug_info = DebugInfo {
676                        pre_state: pre_state_debug.clone(),
677                        tx_env: None,
678                        block_env: block_env.clone(),
679                        cfg_env: cfg.clone(),
680                        block_idx,
681                        tx_idx,
682                        withdrawals: block.withdrawals.clone(),
683                    };
684                    print_error_with_state(
685                        &debug_info,
686                        evm.ctx().db_ref(),
687                        test_case.post_state.as_ref(),
688                    );
689                }
690                if json_output {
691                    let output = json!({
692                        "block": block_idx,
693                        "tx": tx_idx,
694                        "error": "missing sender",
695                        "status": "skipped"
696                    });
697                    print_json(&output);
698                } else {
699                    eprintln!("⚠️  Skipping block {block_idx} due to missing sender");
700                }
701                block_completed = false;
702                break; // Skip to next block
703            }
704
705            let tx_env = match tx.to_tx_env() {
706                Ok(env) => env,
707                Err(e) => {
708                    if should_fail {
709                        // Expected failure during tx env creation
710                        continue;
711                    }
712                    if print_env_on_error {
713                        let debug_info = DebugInfo {
714                            pre_state: pre_state_debug.clone(),
715                            tx_env: None,
716                            block_env: block_env.clone(),
717                            cfg_env: cfg.clone(),
718                            block_idx,
719                            tx_idx,
720                            withdrawals: block.withdrawals.clone(),
721                        };
722                        print_error_with_state(
723                            &debug_info,
724                            evm.ctx().db_ref(),
725                            test_case.post_state.as_ref(),
726                        );
727                    }
728                    if json_output {
729                        let output = json!({
730                            "block": block_idx,
731                            "tx": tx_idx,
732                            "error": format!("tx env creation error: {e}"),
733                            "status": "skipped"
734                        });
735                        print_json(&output);
736                    } else {
737                        eprintln!(
738                            "⚠️  Skipping block {block_idx} due to transaction env creation error: {e}"
739                        );
740                    }
741                    block_completed = false;
742                    break; // Skip to next block
743                }
744            };
745
746            // bump bal index
747            evm.db_mut().bump_bal_index();
748
749            // If JSON output requested, output transaction details
750            let execution_result = if json_output {
751                evm.inspect_tx(tx_env.clone())
752            } else {
753                evm.transact(tx_env.clone())
754            };
755
756            match execution_result {
757                Ok(result) => {
758                    if should_fail {
759                        // Unexpected success - should have failed but didn't
760                        // If not expected to fail, use inspector to trace the transaction
761                        if print_env_on_error {
762                            // Re-run with inspector to get detailed trace
763                            if json_output {
764                                eprintln!("=== Transaction trace (unexpected success) ===");
765                            }
766                            let _ = evm.inspect_tx(tx_env.clone());
767                        }
768
769                        if print_env_on_error {
770                            let debug_info = DebugInfo {
771                                pre_state: pre_state_debug.clone(),
772                                tx_env: Some(tx_env.clone()),
773                                block_env: block_env.clone(),
774                                cfg_env: cfg.clone(),
775                                block_idx,
776                                tx_idx,
777                                withdrawals: block.withdrawals.clone(),
778                            };
779                            print_error_with_state(
780                                &debug_info,
781                                evm.ctx().db_ref(),
782                                test_case.post_state.as_ref(),
783                            );
784                        }
785                        let expected_exception = block.expect_exception.clone().unwrap_or_default();
786                        if json_output {
787                            let output = json!({
788                                "block": block_idx,
789                                "tx": tx_idx,
790                                "expected_exception": expected_exception,
791                                "gas_used": result.result.gas().tx_gas_used(),
792                                "status": "unexpected_success"
793                            });
794                            print_json(&output);
795                        } else {
796                            eprintln!(
797                                "⚠️  Skipping block {block_idx}: transaction unexpectedly succeeded (expected failure: {expected_exception})"
798                            );
799                        }
800                        block_completed = false;
801                        break; // Skip to next block
802                    }
803                    // EIP-8037: Split gas accounting.
804                    let gas = result.result.gas();
805                    cumulative_tx_gas_used += gas.tx_gas_used();
806                    block_regular_gas_used += gas.block_regular_gas_used();
807                    block_state_gas_used += gas.block_state_gas_used();
808                    evm.commit(result.state);
809                }
810                Err(e) => {
811                    if !should_fail {
812                        // Unexpected error - use inspector to trace the transaction
813                        if print_env_on_error {
814                            if json_output {
815                                eprintln!("=== Transaction trace (unexpected failure) ===");
816                            }
817                            let _ = evm.inspect_tx(tx_env.clone());
818                        }
819
820                        if print_env_on_error {
821                            let debug_info = DebugInfo {
822                                pre_state: pre_state_debug.clone(),
823                                tx_env: Some(tx_env.clone()),
824                                block_env: block_env.clone(),
825                                cfg_env: cfg.clone(),
826                                block_idx,
827                                tx_idx,
828                                withdrawals: block.withdrawals.clone(),
829                            };
830                            print_error_with_state(
831                                &debug_info,
832                                evm.ctx().db_ref(),
833                                test_case.post_state.as_ref(),
834                            );
835                        }
836                        if json_output {
837                            let output = json!({
838                                "block": block_idx,
839                                "tx": tx_idx,
840                                "error": format!("{e:?}"),
841                                "status": "unexpected_failure"
842                            });
843                            print_json(&output);
844                        } else {
845                            eprintln!(
846                                "⚠️  Skipping block {block_idx} due to unexpected failure: {e:?}"
847                            );
848                        }
849                        block_completed = false;
850                        break; // Skip to next block
851                    } else if json_output {
852                        // Expected failure
853                        let output = json!({
854                            "block": block_idx,
855                            "tx": tx_idx,
856                            "error": format!("{e:?}"),
857                            "status": "expected_failure"
858                        });
859                        print_json(&output);
860                    }
861                }
862            }
863        }
864
865        // Validate block gas used against header.
866        // EIP-8037 (Amsterdam+): block gas_used = max(regular_gas, state_gas).
867        // Pre-Amsterdam: block gas_used = cumulative tx_gas_used (includes refunds).
868        if block_completed && !should_fail {
869            if let Some(block_header) = block.block_header.as_ref() {
870                let expected_gas_used = block_header.gas_used.to::<u64>();
871                let actual_block_gas_used = if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
872                    block_regular_gas_used.max(block_state_gas_used)
873                } else {
874                    cumulative_tx_gas_used
875                };
876                if actual_block_gas_used != expected_gas_used {
877                    if print_env_on_error {
878                        eprintln!(
879                            "Block gas used mismatch at block {block_idx}: expected {expected_gas_used}, got {actual_block_gas_used} (regular: {block_regular_gas_used}, state: {block_state_gas_used}, tx: {cumulative_tx_gas_used})"
880                        );
881                    }
882                    return Err(TestExecutionError::BlockGasUsedMismatch {
883                        block_idx,
884                        expected: expected_gas_used,
885                        actual: actual_block_gas_used,
886                    });
887                }
888            }
889        }
890
891        // bump bal index
892        evm.db_mut().bump_bal_index();
893
894        // uncle rewards are not implemented yet
895        post_block::post_block_transition(
896            &mut evm,
897            &block_env,
898            block.withdrawals.as_deref().unwrap_or_default(),
899            spec_id,
900        )
901        .map_err(|e| TestExecutionError::PostBlockSystemCall {
902            block_idx,
903            error: format!("{e:?}"),
904        })?;
905
906        // insert present block hash.
907        state.block_hashes.insert(block_env.number.to::<u64>(), block_hash.unwrap_or_default());
908
909        if let Some(bal) = state.bal_state.bal_builder.take() {
910            if let Some(state_bal) = bal_test {
911                if &bal != state_bal.as_ref() {
912                    println!("Bal mismatch");
913                    println!("Test bal");
914                    state_bal.pretty_print();
915                    println!("Bal:");
916                    bal.pretty_print();
917                    return Err(TestExecutionError::BalMismatchError);
918                }
919            }
920        }
921
922        parent_block_hash = block_hash;
923        if let Some(excess_blob_gas) = this_excess_blob_gas {
924            parent_excess_blob_gas = excess_blob_gas;
925        }
926
927        state.merge_transitions(BundleRetention::Reverts);
928    }
929
930    // Validate post state if present
931    if let Some(expected_post_state) = &test_case.post_state {
932        // Create debug info for post-state validation
933        let debug_info = DebugInfo {
934            pre_state: pre_state_debug.clone(),
935            tx_env: None, // Last transaction is done
936            block_env: block_env.clone(),
937            cfg_env: cfg.clone(),
938            block_idx: test_case.blocks.len(),
939            tx_idx: 0,
940            withdrawals: test_case.blocks.last().and_then(|b| b.withdrawals.clone()),
941        };
942        validate_post_state(&mut state, expected_post_state, &debug_info, print_env_on_error)?;
943    }
944
945    Ok(())
946}
947
948/// Convert ForkSpec to SpecId
949fn fork_to_spec_id(fork: ForkSpec) -> SpecId {
950    match fork {
951        ForkSpec::Frontier => SpecId::FRONTIER,
952        ForkSpec::Homestead | ForkSpec::FrontierToHomesteadAt5 => SpecId::HOMESTEAD,
953        ForkSpec::EIP150 | ForkSpec::HomesteadToDaoAt5 | ForkSpec::HomesteadToEIP150At5 => {
954            SpecId::TANGERINE
955        }
956        ForkSpec::EIP158 => SpecId::SPURIOUS_DRAGON,
957        ForkSpec::Byzantium
958        | ForkSpec::EIP158ToByzantiumAt5
959        | ForkSpec::ByzantiumToConstantinopleFixAt5 => SpecId::BYZANTIUM,
960        ForkSpec::Constantinople | ForkSpec::ByzantiumToConstantinopleAt5 => SpecId::PETERSBURG,
961        ForkSpec::ConstantinopleFix => SpecId::PETERSBURG,
962        ForkSpec::Istanbul => SpecId::ISTANBUL,
963        ForkSpec::Berlin => SpecId::BERLIN,
964        ForkSpec::London | ForkSpec::BerlinToLondonAt5 => SpecId::LONDON,
965        ForkSpec::Paris | ForkSpec::ParisToShanghaiAtTime15k => SpecId::MERGE,
966        ForkSpec::Shanghai => SpecId::SHANGHAI,
967        ForkSpec::Cancun | ForkSpec::ShanghaiToCancunAtTime15k => SpecId::CANCUN,
968        ForkSpec::Prague | ForkSpec::CancunToPragueAtTime15k => SpecId::PRAGUE,
969        ForkSpec::Osaka | ForkSpec::PragueToOsakaAtTime15k => SpecId::OSAKA,
970        ForkSpec::Amsterdam => SpecId::AMSTERDAM,
971        _ => SpecId::AMSTERDAM, // For any unknown forks, use latest available
972    }
973}
974
975/// Check if a test should be skipped based on its filename
976fn skip_test(path: &Path) -> bool {
977    let path_str = path.to_str().unwrap_or_default();
978    // blobs excess gas calculation is not supported or osaka BPO configuration
979    if path_str.contains("paris/eip7610_create_collision")
980        || path_str.contains("cancun/eip4844_blobs")
981        || path_str.contains("prague/eip7251_consolidations")
982        || path_str.contains("prague/eip7685_general_purpose_el_requests")
983        || path_str.contains("prague/eip7002_el_triggerable_withdrawals")
984        || path_str.contains("osaka/eip7918_blob_reserve_price")
985    {
986        return true;
987    }
988
989    let name = path.file_name().unwrap().to_str().unwrap_or_default();
990    // Add any problematic tests here that should be skipped
991    matches!(
992        name,
993        // Test check if gas price overflows, we handle this correctly but does not match tests
994        // specific exception.
995        "CreateTransactionHighNonce.json"
996
997        // Test with some storage check.
998        | "RevertInCreateInInit_Paris.json"
999        | "RevertInCreateInInit.json"
1000        | "dynamicAccountOverwriteEmpty.json"
1001        | "dynamicAccountOverwriteEmpty_Paris.json"
1002        | "RevertInCreateInInitCreate2Paris.json"
1003        | "create2collisionStorage.json"
1004        | "RevertInCreateInInitCreate2.json"
1005        | "create2collisionStorageParis.json"
1006        | "InitCollision.json"
1007        | "InitCollisionParis.json"
1008
1009        // Malformed value.
1010        | "ValueOverflow.json"
1011        | "ValueOverflowParis.json"
1012
1013        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
1014        | "Call50000_sha256.json"
1015        | "static_Call50000_sha256.json"
1016        | "loopMul.json"
1017        | "CALLBlake2f_MaxRounds.json"
1018        // TODO tests not checked, maybe related to parent block hashes as it is currently not supported in test.
1019        | "scenarios.json"
1020        // IT seems that post state is wrong, we properly handle max blob gas and state should stay the same.
1021        | "invalid_tx_max_fee_per_blob_gas.json"
1022        | "correct_increasing_blob_gas_costs.json"
1023        | "correct_decreasing_blob_gas_costs.json"
1024
1025        // test-fixtures/main/develop/blockchain_tests/prague/eip2935_historical_block_hashes_from_state/block_hashes/block_hashes_history.json
1026        | "block_hashes_history.json"
1027    )
1028}
1029
1030#[derive(Debug, Error)]
1031pub enum TestExecutionError {
1032    #[error("Database error: {0}")]
1033    Database(String),
1034
1035    #[error("Skipped fork: {0}")]
1036    SkippedFork(String),
1037
1038    #[error("Sender is required")]
1039    SenderRequired,
1040
1041    #[error("Expected failure at block {block_idx}, tx {tx_idx}: {message}")]
1042    ExpectedFailure { block_idx: usize, tx_idx: usize, message: String },
1043
1044    #[error("Unexpected failure at block {block_idx}, tx {tx_idx}: {error}")]
1045    UnexpectedFailure { block_idx: usize, tx_idx: usize, error: String },
1046
1047    #[error("Transaction env creation failed at block {block_idx}, tx {tx_idx}: {error}")]
1048    TransactionEnvCreation { block_idx: usize, tx_idx: usize, error: String },
1049
1050    #[error("Unexpected revert at block {block_idx}, tx {tx_idx}, gas used: {gas_used}")]
1051    UnexpectedRevert { block_idx: usize, tx_idx: usize, gas_used: u64 },
1052
1053    #[error("Unexpected halt at block {block_idx}, tx {tx_idx}: {reason:?}, gas used: {gas_used}")]
1054    UnexpectedHalt { block_idx: usize, tx_idx: usize, reason: HaltReason, gas_used: u64 },
1055
1056    #[error("Block gas used mismatch at block {block_idx}: expected {expected}, got {actual}")]
1057    BlockGasUsedMismatch { block_idx: usize, expected: u64, actual: u64 },
1058
1059    #[error("Pre-block system call failed at block {block_idx}: {error}")]
1060    PreBlockSystemCall { block_idx: usize, error: String },
1061
1062    #[error("Post-block system call failed at block {block_idx}: {error}")]
1063    PostBlockSystemCall { block_idx: usize, error: String },
1064
1065    #[error("BAL error")]
1066    BalMismatchError,
1067
1068    #[error(
1069        "Post-state validation failed for {address:?}.{field}: expected {expected}, got {actual}"
1070    )]
1071    PostStateValidation { address: Address, field: String, expected: String, actual: String },
1072}
1073
1074#[derive(Debug, Error)]
1075pub enum Error {
1076    #[error("Path not found: {0}")]
1077    PathNotFound(PathBuf),
1078
1079    #[error("No JSON files found in: {0}")]
1080    NoJsonFiles(PathBuf),
1081
1082    #[error("Failed to read file {0}: {1}")]
1083    FileRead(PathBuf, std::io::Error),
1084
1085    #[error("Failed to decode JSON from {0}: {1}")]
1086    JsonDecode(PathBuf, serde_json::Error),
1087
1088    #[error("Test execution failed for {test_name} in {test_path}: {error}")]
1089    TestExecution { test_name: String, test_path: PathBuf, error: String },
1090
1091    #[error("Directory traversal error: {0}")]
1092    WalkDir(#[from] walkdir::Error),
1093
1094    #[error("{failed} tests failed")]
1095    TestsFailed { failed: usize },
1096}