revme/cmd/statetest/
runner.rs

1use super::{
2    merkle_trie::{log_rlp_hash, state_merkle_trie_root},
3    utils::recover_address,
4};
5use database::State;
6use indicatif::{ProgressBar, ProgressDrawTarget};
7use inspector::{inspectors::TracerEip3155, InspectCommitEvm};
8use revm::{
9    bytecode::Bytecode,
10    context::{block::BlockEnv, cfg::CfgEnv, tx::TxEnv},
11    context_interface::{
12        block::calc_excess_blob_gas,
13        result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction},
14        Cfg,
15    },
16    database_interface::EmptyDB,
17    primitives::{
18        eip4844::TARGET_BLOB_GAS_PER_BLOCK_CANCUN, hardfork::SpecId, keccak256, Bytes, TxKind, B256,
19    },
20    Context, ExecuteCommitEvm, MainBuilder, MainContext,
21};
22use serde_json::json;
23use statetest_types::{SpecName, Test, TestSuite};
24
25use std::{
26    convert::Infallible,
27    fmt::Debug,
28    io::stderr,
29    path::{Path, PathBuf},
30    sync::{
31        atomic::{AtomicBool, AtomicUsize, Ordering},
32        Arc, Mutex,
33    },
34    time::{Duration, Instant},
35};
36use thiserror::Error;
37use walkdir::{DirEntry, WalkDir};
38
39#[derive(Debug, Error)]
40#[error("Path: {path}\nName: {name}\nError: {kind}")]
41pub struct TestError {
42    pub name: String,
43    pub path: String,
44    pub kind: TestErrorKind,
45}
46
47#[derive(Debug, Error)]
48pub enum TestErrorKind {
49    #[error("logs root mismatch: got {got}, expected {expected}")]
50    LogsRootMismatch { got: B256, expected: B256 },
51    #[error("state root mismatch: got {got}, expected {expected}")]
52    StateRootMismatch { got: B256, expected: B256 },
53    #[error("unknown private key: {0:?}")]
54    UnknownPrivateKey(B256),
55    #[error("unexpected exception: got {got_exception:?}, expected {expected_exception:?}")]
56    UnexpectedException {
57        expected_exception: Option<String>,
58        got_exception: Option<String>,
59    },
60    #[error("unexpected output: got {got_output:?}, expected {expected_output:?}")]
61    UnexpectedOutput {
62        expected_output: Option<Bytes>,
63        got_output: Option<Bytes>,
64    },
65    #[error(transparent)]
66    SerdeDeserialize(#[from] serde_json::Error),
67    #[error("thread panicked")]
68    Panic,
69    #[error("path does not exist")]
70    InvalidPath,
71    #[error("no JSON test files found in path")]
72    NoJsonFiles,
73}
74
75pub fn find_all_json_tests(path: &Path) -> Vec<PathBuf> {
76    if path.is_file() {
77        vec![path.to_path_buf()]
78    } else {
79        WalkDir::new(path)
80            .into_iter()
81            .filter_map(Result::ok)
82            .filter(|e| e.path().extension() == Some("json".as_ref()))
83            .map(DirEntry::into_path)
84            .collect()
85    }
86}
87
88fn skip_test(path: &Path) -> bool {
89    let name = path.file_name().unwrap().to_str().unwrap();
90
91    matches!(
92        name,
93        // Funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
94        // custom json parser. https://github.com/ethereum/tests/issues/971
95        |"ValueOverflow.json"| "ValueOverflowParis.json"
96
97        // Precompiles having storage is not possible
98        | "RevertPrecompiledTouch_storage.json"
99        | "RevertPrecompiledTouch.json"
100
101        // `txbyte` is of type 02 and we don't parse tx bytes for this test to fail.
102        | "typeTwoBerlin.json"
103
104        // Need to handle Test errors
105        | "transactionIntinsicBug.json"
106
107        // Test check if gas price overflows, we handle this correctly but does not match tests specific exception.
108        | "HighGasPrice.json"
109        | "CREATE_HighNonce.json"
110        | "CREATE_HighNonceMinus1.json"
111        | "CreateTransactionHighNonce.json"
112
113        // Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
114        // London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
115        // It is expected to not execute these tests.
116        | "basefeeExample.json"
117        | "eip1559.json"
118        | "mergeTest.json"
119
120        // Test with some storage check.
121        | "RevertInCreateInInit_Paris.json"
122        | "RevertInCreateInInit.json"
123        | "dynamicAccountOverwriteEmpty.json"
124        | "dynamicAccountOverwriteEmpty_Paris.json"
125        | "RevertInCreateInInitCreate2Paris.json"
126        | "create2collisionStorage.json"
127        | "RevertInCreateInInitCreate2.json"
128        | "create2collisionStorageParis.json"
129        | "InitCollision.json"
130        | "InitCollisionParis.json"
131
132        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
133        | "loopExp.json"
134        | "Call50000_sha256.json"
135        | "static_Call50000_sha256.json"
136        | "loopMul.json"
137        | "CALLBlake2f_MaxRounds.json"
138    )
139}
140
141fn check_evm_execution(
142    test: &Test,
143    expected_output: Option<&Bytes>,
144    test_name: &str,
145    exec_result: &Result<ExecutionResult<HaltReason>, EVMError<Infallible, InvalidTransaction>>,
146    db: &mut State<EmptyDB>,
147    spec: SpecId,
148    print_json_outcome: bool,
149) -> Result<(), TestErrorKind> {
150    let logs_root = log_rlp_hash(exec_result.as_ref().map(|r| r.logs()).unwrap_or_default());
151    let state_root = state_merkle_trie_root(db.cache.trie_account());
152
153    let print_json_output = |error: Option<String>| {
154        if print_json_outcome {
155            let json = json!({
156                "stateRoot": state_root,
157                "logsRoot": logs_root,
158                "output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(),
159                "gasUsed": exec_result.as_ref().ok().map(|r| r.gas_used()).unwrap_or_default(),
160                "pass": error.is_none(),
161                "errorMsg": error.unwrap_or_default(),
162                "evmResult": match exec_result {
163                    Ok(r) => match r {
164                        ExecutionResult::Success { reason, .. } => format!("Success: {reason:?}"),
165                        ExecutionResult::Revert { .. } => "Revert".to_string(),
166                        ExecutionResult::Halt { reason, .. } => format!("Halt: {reason:?}"),
167                    },
168                    Err(e) => e.to_string(),
169                },
170                "postLogsHash": logs_root,
171                "fork": spec,
172                "test": test_name,
173                "d": test.indexes.data,
174                "g": test.indexes.gas,
175                "v": test.indexes.value,
176            });
177            eprintln!("{json}");
178        }
179    };
180
181    // If we expect exception revm should return error from execution.
182    // So we do not check logs and state root.
183    //
184    // Note that some tests that have exception and run tests from before state clear
185    // would touch the caller account and make it appear in state root calculation.
186    // This is not something that we would expect as invalid tx should not touch state.
187    // but as this is a cleanup of invalid tx it is not properly defined and in the end
188    // it does not matter.
189    // Test where this happens: `tests/GeneralStateTests/stTransactionTest/NoSrcAccountCreate.json`
190    // and you can check that we have only two "hash" values for before and after state clear.
191    match (&test.expect_exception, exec_result) {
192        // Do nothing
193        (None, Ok(result)) => {
194            // Check output
195            if let Some((expected_output, output)) = expected_output.zip(result.output()) {
196                if expected_output != output {
197                    let kind = TestErrorKind::UnexpectedOutput {
198                        expected_output: Some(expected_output.clone()),
199                        got_output: result.output().cloned(),
200                    };
201                    print_json_output(Some(kind.to_string()));
202                    return Err(kind);
203                }
204            }
205        }
206        // Return okay, exception is expected.
207        (Some(_), Err(_)) => return Ok(()),
208        _ => {
209            let kind = TestErrorKind::UnexpectedException {
210                expected_exception: test.expect_exception.clone(),
211                got_exception: exec_result.clone().err().map(|e| e.to_string()),
212            };
213            print_json_output(Some(kind.to_string()));
214            return Err(kind);
215        }
216    }
217
218    if logs_root != test.logs {
219        let kind = TestErrorKind::LogsRootMismatch {
220            got: logs_root,
221            expected: test.logs,
222        };
223        print_json_output(Some(kind.to_string()));
224        return Err(kind);
225    }
226
227    if state_root != test.hash {
228        let kind = TestErrorKind::StateRootMismatch {
229            got: state_root,
230            expected: test.hash,
231        };
232        print_json_output(Some(kind.to_string()));
233        return Err(kind);
234    }
235
236    print_json_output(None);
237
238    Ok(())
239}
240
241pub fn execute_test_suite(
242    path: &Path,
243    elapsed: &Arc<Mutex<Duration>>,
244    trace: bool,
245    print_json_outcome: bool,
246) -> Result<(), TestError> {
247    if skip_test(path) {
248        return Ok(());
249    }
250
251    let s = std::fs::read_to_string(path).unwrap();
252    let path = path.to_string_lossy().into_owned();
253    let suite: TestSuite = serde_json::from_str(&s).map_err(|e| TestError {
254        name: "Unknown".to_string(),
255        path: path.clone(),
256        kind: e.into(),
257    })?;
258
259    for (name, unit) in suite.0 {
260        // Create database and insert cache
261        let mut cache_state = database::CacheState::new(false);
262        for (address, info) in unit.pre {
263            let code_hash = keccak256(&info.code);
264            let bytecode = Bytecode::new_raw_checked(info.code.clone())
265                .unwrap_or(Bytecode::new_legacy(info.code));
266            let acc_info = revm::state::AccountInfo {
267                balance: info.balance,
268                code_hash,
269                code: Some(bytecode),
270                nonce: info.nonce,
271            };
272            cache_state.insert_account_with_storage(address, acc_info, info.storage);
273        }
274
275        let mut cfg = CfgEnv::default();
276        let mut block = BlockEnv::default();
277        let mut tx = TxEnv::default();
278        // For mainnet
279        cfg.chain_id = 1;
280
281        // Block env
282        block.number = unit.env.current_number.try_into().unwrap_or(u64::MAX);
283        block.beneficiary = unit.env.current_coinbase;
284        block.timestamp = unit.env.current_timestamp.try_into().unwrap_or(u64::MAX);
285        block.gas_limit = unit.env.current_gas_limit.try_into().unwrap_or(u64::MAX);
286        block.basefee = unit
287            .env
288            .current_base_fee
289            .unwrap_or_default()
290            .try_into()
291            .unwrap_or(u64::MAX);
292        block.difficulty = unit.env.current_difficulty;
293        // After the Merge prevrandao replaces mix_hash field in block and replaced difficulty opcode in EVM.
294        block.prevrandao = unit.env.current_random;
295
296        // Tx env
297        tx.caller = if let Some(address) = unit.transaction.sender {
298            address
299        } else {
300            recover_address(unit.transaction.secret_key.as_slice()).ok_or_else(|| TestError {
301                name: name.clone(),
302                path: path.clone(),
303                kind: TestErrorKind::UnknownPrivateKey(unit.transaction.secret_key),
304            })?
305        };
306        tx.gas_price = unit
307            .transaction
308            .gas_price
309            .or(unit.transaction.max_fee_per_gas)
310            .unwrap_or_default()
311            .try_into()
312            .unwrap_or(u128::MAX);
313        tx.gas_priority_fee = unit
314            .transaction
315            .max_priority_fee_per_gas
316            .map(|b| u128::try_from(b).expect("max priority fee less than u128::MAX"));
317        // EIP-4844
318        tx.blob_hashes = unit.transaction.blob_versioned_hashes.clone();
319        tx.max_fee_per_blob_gas = unit
320            .transaction
321            .max_fee_per_blob_gas
322            .map(|b| u128::try_from(b).expect("max fee less than u128::MAX"))
323            .unwrap_or(u128::MAX);
324
325        // Post and execution
326        for (spec_name, tests) in unit.post {
327            // Constantinople was immediately extended by Petersburg.
328            // There isn't any production Constantinople transaction
329            // so we don't support it and skip right to Petersburg.
330            if spec_name == SpecName::Constantinople {
331                continue;
332            }
333
334            cfg.spec = spec_name.to_spec_id();
335
336            // EIP-4844
337            if let Some(current_excess_blob_gas) = unit.env.current_excess_blob_gas {
338                block.set_blob_excess_gas_and_price(
339                    current_excess_blob_gas.to(),
340                    cfg.spec.is_enabled_in(SpecId::PRAGUE),
341                );
342            } else if let (Some(parent_blob_gas_used), Some(parent_excess_blob_gas)) = (
343                unit.env.parent_blob_gas_used,
344                unit.env.parent_excess_blob_gas,
345            ) {
346                block.set_blob_excess_gas_and_price(
347                    calc_excess_blob_gas(
348                        parent_blob_gas_used.to(),
349                        parent_excess_blob_gas.to(),
350                        unit.env
351                            .parent_target_blobs_per_block
352                            .map(|i| i.to())
353                            .unwrap_or(TARGET_BLOB_GAS_PER_BLOCK_CANCUN),
354                    ),
355                    cfg.spec.is_enabled_in(SpecId::PRAGUE),
356                );
357            }
358
359            if cfg.spec.is_enabled_in(SpecId::MERGE) && block.prevrandao.is_none() {
360                // If spec is merge and prevrandao is not set, set it to default
361                block.prevrandao = Some(B256::default());
362            }
363
364            for (index, test) in tests.into_iter().enumerate() {
365                let Some(tx_type) = unit.transaction.tx_type(test.indexes.data) else {
366                    if test.expect_exception.is_some() {
367                        continue;
368                    } else {
369                        panic!("Invalid transaction type without expected exception");
370                    }
371                };
372
373                tx.tx_type = tx_type as u8;
374
375                tx.gas_limit = unit.transaction.gas_limit[test.indexes.gas].saturating_to();
376
377                tx.data = unit
378                    .transaction
379                    .data
380                    .get(test.indexes.data)
381                    .unwrap()
382                    .clone();
383
384                tx.nonce = u64::try_from(unit.transaction.nonce).unwrap();
385                tx.value = unit.transaction.value[test.indexes.value];
386
387                tx.access_list = unit
388                    .transaction
389                    .access_lists
390                    .get(test.indexes.data)
391                    .cloned()
392                    .flatten()
393                    .unwrap_or_default();
394
395                tx.authorization_list = unit
396                    .transaction
397                    .authorization_list
398                    .clone()
399                    .map(|auth_list| auth_list.into_iter().map(Into::into).collect::<Vec<_>>())
400                    .unwrap_or_default();
401
402                let to = match unit.transaction.to {
403                    Some(add) => TxKind::Call(add),
404                    None => TxKind::Create,
405                };
406                tx.kind = to;
407
408                let mut cache = cache_state.clone();
409                cache.set_state_clear_flag(cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON));
410                let mut state = database::State::builder()
411                    .with_cached_prestate(cache)
412                    .with_bundle_update()
413                    .build();
414                let mut evm = Context::mainnet()
415                    .with_block(&block)
416                    .with_tx(&tx)
417                    .with_cfg(&cfg)
418                    .with_db(&mut state)
419                    .build_mainnet();
420
421                // Do the deed
422                let (e, exec_result) = if trace {
423                    let mut evm = Context::mainnet()
424                        .with_block(&block)
425                        .with_tx(&tx)
426                        .with_cfg(&cfg)
427                        .with_db(&mut state)
428                        .build_mainnet_with_inspector(
429                            TracerEip3155::buffered(stderr()).without_summary(),
430                        );
431
432                    let timer = Instant::now();
433                    let res = evm.inspect_replay_commit();
434                    *elapsed.lock().unwrap() += timer.elapsed();
435
436                    let spec = cfg.spec();
437                    let db = &mut evm.data.ctx.journaled_state.database;
438                    // Dump state and traces if test failed
439                    let output = check_evm_execution(
440                        &test,
441                        unit.out.as_ref(),
442                        &name,
443                        &res,
444                        db,
445                        spec,
446                        print_json_outcome,
447                    );
448                    let Err(e) = output else {
449                        continue;
450                    };
451                    (e, res)
452                } else {
453                    let timer = Instant::now();
454                    let res = evm.replay_commit();
455                    *elapsed.lock().unwrap() += timer.elapsed();
456
457                    let spec = cfg.spec();
458                    let db = evm.data.ctx.journaled_state.database;
459                    // Dump state and traces if test failed
460                    let output = check_evm_execution(
461                        &test,
462                        unit.out.as_ref(),
463                        &name,
464                        &res,
465                        db,
466                        spec,
467                        print_json_outcome,
468                    );
469                    let Err(e) = output else {
470                        continue;
471                    };
472                    (e, res)
473                };
474
475                // Print only once or if we are already in trace mode, just return error
476                // If trace is true that print_json_outcome will be also true.
477                static FAILED: AtomicBool = AtomicBool::new(false);
478                if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) {
479                    return Err(TestError {
480                        name: name.clone(),
481                        path: path.clone(),
482                        kind: e,
483                    });
484                }
485
486                // Re-build to run with tracing
487                let mut cache = cache_state.clone();
488                cache.set_state_clear_flag(cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON));
489                let mut state = database::State::builder()
490                    .with_cached_prestate(cache)
491                    .with_bundle_update()
492                    .build();
493
494                println!("\nTraces:");
495
496                let mut evm = Context::mainnet()
497                    .with_db(&mut state)
498                    .with_block(&block)
499                    .with_tx(&tx)
500                    .with_cfg(&cfg)
501                    .build_mainnet_with_inspector(
502                        TracerEip3155::buffered(stderr()).without_summary(),
503                    );
504
505                let _ = evm.inspect_replay_commit();
506
507                println!("\nExecution result: {exec_result:#?}");
508                println!("\nExpected exception: {:?}", test.expect_exception);
509                println!("\nState before: {cache_state:#?}");
510                println!(
511                    "\nState after: {:#?}",
512                    evm.data.ctx.journaled_state.database.cache
513                );
514                println!("\nSpecification: {:?}", cfg.spec);
515                println!("\nTx: {tx:#?}");
516                println!("Block: {block:#?}");
517                println!("Cfg: {cfg:#?}");
518                println!("\nTest name: {name:?} (index: {index}, path: {path:?}) failed:\n{e}");
519
520                return Err(TestError {
521                    path: path.clone(),
522                    name: name.clone(),
523                    kind: e,
524                });
525            }
526        }
527    }
528    Ok(())
529}
530
531pub fn run(
532    test_files: Vec<PathBuf>,
533    mut single_thread: bool,
534    trace: bool,
535    mut print_outcome: bool,
536    keep_going: bool,
537) -> Result<(), TestError> {
538    // Trace implies print_outcome
539    if trace {
540        print_outcome = true;
541    }
542    // `print_outcome` or trace implies single_thread
543    if print_outcome {
544        single_thread = true;
545    }
546    let n_files = test_files.len();
547
548    let n_errors = Arc::new(AtomicUsize::new(0));
549    let console_bar = Arc::new(ProgressBar::with_draw_target(
550        Some(n_files as u64),
551        ProgressDrawTarget::stdout(),
552    ));
553    let queue = Arc::new(Mutex::new((0usize, test_files)));
554    let elapsed = Arc::new(Mutex::new(std::time::Duration::ZERO));
555
556    let num_threads = match (single_thread, std::thread::available_parallelism()) {
557        (true, _) | (false, Err(_)) => 1,
558        (false, Ok(n)) => n.get(),
559    };
560    let num_threads = num_threads.min(n_files);
561    let mut handles = Vec::with_capacity(num_threads);
562    for i in 0..num_threads {
563        let queue = queue.clone();
564        let n_errors = n_errors.clone();
565        let console_bar = console_bar.clone();
566        let elapsed = elapsed.clone();
567
568        let thread = std::thread::Builder::new().name(format!("runner-{i}"));
569
570        let f = move || loop {
571            if !keep_going && n_errors.load(Ordering::SeqCst) > 0 {
572                return Ok(());
573            }
574
575            let (_index, test_path) = {
576                let (current_idx, queue) = &mut *queue.lock().unwrap();
577                let prev_idx = *current_idx;
578                let Some(test_path) = queue.get(prev_idx).cloned() else {
579                    return Ok(());
580                };
581                *current_idx = prev_idx + 1;
582                (prev_idx, test_path)
583            };
584
585            let result = execute_test_suite(&test_path, &elapsed, trace, print_outcome);
586
587            // Increment after the test is done.
588            console_bar.inc(1);
589
590            if let Err(err) = result {
591                n_errors.fetch_add(1, Ordering::SeqCst);
592                if !keep_going {
593                    return Err(err);
594                }
595            }
596        };
597        handles.push(thread.spawn(f).unwrap());
598    }
599
600    // join all threads before returning an error
601    let mut thread_errors = Vec::new();
602    for (i, handle) in handles.into_iter().enumerate() {
603        match handle.join() {
604            Ok(Ok(())) => {}
605            Ok(Err(e)) => thread_errors.push(e),
606            Err(_) => thread_errors.push(TestError {
607                name: format!("thread {i} panicked"),
608                path: "".to_string(),
609                kind: TestErrorKind::Panic,
610            }),
611        }
612    }
613    console_bar.finish();
614
615    println!(
616        "Finished execution. Total CPU time: {:.6}s",
617        elapsed.lock().unwrap().as_secs_f64()
618    );
619
620    let n_errors = n_errors.load(Ordering::SeqCst);
621    let n_thread_errors = thread_errors.len();
622    if n_errors == 0 && n_thread_errors == 0 {
623        println!("All tests passed!");
624        Ok(())
625    } else {
626        println!("Encountered {n_errors} errors out of {n_files} total tests");
627
628        if n_thread_errors == 0 {
629            std::process::exit(1);
630        }
631
632        if n_thread_errors > 1 {
633            println!("{n_thread_errors} threads returned an error, out of {num_threads} total:");
634            for error in &thread_errors {
635                println!("{error}");
636            }
637        }
638        Err(thread_errors.swap_remove(0))
639    }
640}