revme/cmd/statetest/
runner.rs

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