Skip to main content

revme/cmd/
blockchaintest.rs

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