Skip to main content

revmc_statetest/
runner.rs

1// Vendored from revm's `bins/revme/src/cmd/statetest/runner.rs`.
2// Keep in sync with upstream; revmc-specific code lives in `compiled.rs`.
3
4use crate::merkle_trie::{TestValidationResult, compute_test_roots};
5use console::Term;
6use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
7use revm_context::{Context, block::BlockEnv, cfg::CfgEnv, tx::TxEnv};
8use revm_context_interface::result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction};
9use revm_database::{self as database, bal::EvmDatabaseError};
10use revm_database_interface::EmptyDB;
11use revm_handler::{ExecuteCommitEvm, MainBuilder, MainContext};
12use revm_primitives::{B256, Bytes, U256, hardfork::SpecId};
13use revm_statetest_types::{SpecName, Test, TestSuite, TestUnit};
14use serde_json::json;
15use std::{
16    convert::Infallible,
17    fmt::Debug,
18    path::{Path, PathBuf},
19    sync::{
20        Arc, Mutex,
21        atomic::{AtomicBool, AtomicUsize, Ordering},
22    },
23    time::{Duration, Instant},
24};
25use thiserror::Error;
26
27/// Error that occurs during test execution.
28#[derive(Debug, Error)]
29#[error("Path: {path}\nName: {name}\nError: {kind}")]
30pub struct TestError {
31    pub name: String,
32    pub path: String,
33    pub kind: TestErrorKind,
34}
35
36/// Specific kind of error that occurred during test execution.
37#[derive(Debug, Error)]
38pub enum TestErrorKind {
39    #[error("logs root mismatch: got {got}, expected {expected}")]
40    LogsRootMismatch { got: B256, expected: B256 },
41    #[error("state root mismatch: got {got}, expected {expected}")]
42    StateRootMismatch { got: B256, expected: B256 },
43    #[error("unknown private key: {0:?}")]
44    UnknownPrivateKey(B256),
45    #[error("unexpected exception: got {got_exception:?}, expected {expected_exception:?}")]
46    UnexpectedException { expected_exception: Option<String>, got_exception: Option<String> },
47    #[error("unexpected output: got {got_output:?}, expected {expected_output:?}")]
48    UnexpectedOutput { expected_output: Option<Bytes>, got_output: Option<Bytes> },
49    #[error(transparent)]
50    SerdeDeserialize(#[from] serde_json::Error),
51    #[error("thread panicked")]
52    Panic,
53    #[error("path does not exist")]
54    InvalidPath,
55    #[error("no JSON test files found in path")]
56    NoJsonFiles,
57    #[error("compilation failed: {0}")]
58    CompilationError(String),
59}
60
61/// Check if a test should be skipped based on its filename.
62/// Some tests are known to be problematic or take too long.
63pub fn skip_test(path: &Path) -> bool {
64    let path_str = path.to_str().unwrap_or_default();
65
66    // Skip tests that have storage for newly created account.
67    if path_str.contains("paris/eip7610_create_collision") {
68        return true;
69    }
70
71    let name = path.file_name().unwrap().to_str().unwrap_or_default();
72
73    matches!(
74        name,
75        // Test check if gas price overflows, we handle this correctly but does not match tests
76        // specific exception.
77        | "CreateTransactionHighNonce.json"
78
79        // Test with some storage check.
80        | "RevertInCreateInInit_Paris.json"
81        | "RevertInCreateInInit.json"
82        | "dynamicAccountOverwriteEmpty.json"
83        | "dynamicAccountOverwriteEmpty_Paris.json"
84        | "RevertInCreateInInitCreate2Paris.json"
85        | "create2collisionStorage.json"
86        | "RevertInCreateInInitCreate2.json"
87        | "create2collisionStorageParis.json"
88        | "InitCollision.json"
89        | "InitCollisionParis.json"
90        | "test_init_collision_create_opcode.json"
91
92        // Malformed value.
93        | "ValueOverflow.json"
94        | "ValueOverflowParis.json"
95
96        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
97        | "Call50000_sha256.json"
98        | "static_Call50000_sha256.json"
99        | "loopMul.json"
100        | "CALLBlake2f_MaxRounds.json"
101    )
102}
103
104struct TestExecutionContext<'a> {
105    name: &'a str,
106    unit: &'a TestUnit,
107    test: &'a Test,
108    cfg: &'a CfgEnv,
109    block: &'a BlockEnv,
110    tx: &'a TxEnv,
111    cache_state: &'a database::CacheState,
112    elapsed: &'a Arc<Mutex<Duration>>,
113    #[allow(dead_code)]
114    trace: bool,
115    print_json_outcome: bool,
116}
117
118fn build_json_output(
119    test: &Test,
120    test_name: &str,
121    exec_result: &Result<
122        ExecutionResult<HaltReason>,
123        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
124    >,
125    validation: &TestValidationResult,
126    spec: SpecId,
127    error: Option<String>,
128) -> serde_json::Value {
129    json!({
130        "stateRoot": validation.state_root,
131        "logsRoot": validation.logs_root,
132        "output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(),
133        "gasUsed": exec_result.as_ref().ok().map(|r| r.tx_gas_used()).unwrap_or_default(),
134        "pass": error.is_none(),
135        "errorMsg": error.unwrap_or_default(),
136        "evmResult": format_evm_result(exec_result),
137        "postLogsHash": validation.logs_root,
138        "fork": spec,
139        "test": test_name,
140        "d": test.indexes.data,
141        "g": test.indexes.gas,
142        "v": test.indexes.value,
143    })
144}
145
146fn format_evm_result(
147    exec_result: &Result<
148        ExecutionResult<HaltReason>,
149        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
150    >,
151) -> String {
152    match exec_result {
153        Ok(r) => match r {
154            ExecutionResult::Success { reason, .. } => format!("Success: {reason:?}"),
155            ExecutionResult::Revert { .. } => "Revert".to_string(),
156            ExecutionResult::Halt { reason, .. } => format!("Halt: {reason:?}"),
157        },
158        Err(e) => e.to_string(),
159    }
160}
161
162fn validate_exception(
163    test: &Test,
164    exec_result: &Result<
165        ExecutionResult<HaltReason>,
166        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
167    >,
168) -> Result<bool, TestErrorKind> {
169    match (&test.expect_exception, exec_result) {
170        (None, Ok(_)) => Ok(false),
171        (Some(_), Err(_)) => Ok(true),
172        _ => Err(TestErrorKind::UnexpectedException {
173            expected_exception: test.expect_exception.clone(),
174            got_exception: exec_result.as_ref().err().map(|e| e.to_string()),
175        }),
176    }
177}
178
179fn validate_output(
180    expected_output: Option<&Bytes>,
181    actual_result: &ExecutionResult<HaltReason>,
182) -> Result<(), TestErrorKind> {
183    if let Some((expected, actual)) = expected_output.zip(actual_result.output()) {
184        if expected != actual {
185            return Err(TestErrorKind::UnexpectedOutput {
186                expected_output: Some(expected.clone()),
187                got_output: actual_result.output().cloned(),
188            });
189        }
190    }
191    Ok(())
192}
193
194pub(crate) fn check_evm_execution(
195    test: &Test,
196    expected_output: Option<&Bytes>,
197    test_name: &str,
198    exec_result: &Result<
199        ExecutionResult<HaltReason>,
200        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
201    >,
202    db: &mut database::State<EmptyDB>,
203    spec: SpecId,
204    print_json_outcome: bool,
205) -> Result<(), TestErrorKind> {
206    let validation = compute_test_roots(exec_result, db);
207
208    let print_json = |error: Option<&TestErrorKind>| {
209        if print_json_outcome {
210            let json = build_json_output(
211                test,
212                test_name,
213                exec_result,
214                &validation,
215                spec,
216                error.map(|e| e.to_string()),
217            );
218            eprintln!("{json}");
219        }
220    };
221
222    // Check if exception handling is correct.
223    let exception_expected = validate_exception(test, exec_result).inspect_err(|e| {
224        print_json(Some(e));
225    })?;
226
227    // If exception was expected and occurred, we're done.
228    if exception_expected {
229        print_json(None);
230        return Ok(());
231    }
232
233    // Validate output if execution succeeded.
234    if let Ok(result) = exec_result {
235        validate_output(expected_output, result).inspect_err(|e| {
236            print_json(Some(e));
237        })?;
238    }
239
240    // Validate logs root.
241    if validation.logs_root != test.logs {
242        let error =
243            TestErrorKind::LogsRootMismatch { got: validation.logs_root, expected: test.logs };
244        print_json(Some(&error));
245        return Err(error);
246    }
247
248    // Validate state root.
249    if validation.state_root != test.hash {
250        let error =
251            TestErrorKind::StateRootMismatch { got: validation.state_root, expected: test.hash };
252        print_json(Some(&error));
253        return Err(error);
254    }
255
256    print_json(None);
257    Ok(())
258}
259
260/// Execute a single test suite file containing multiple tests.
261pub fn execute_test_suite(
262    path: &Path,
263    elapsed: &Arc<Mutex<Duration>>,
264    trace: bool,
265    print_json_outcome: bool,
266) -> Result<(), TestError> {
267    if skip_test(path) {
268        return Ok(());
269    }
270
271    let s = std::fs::read_to_string(path).unwrap();
272    let path = path.to_string_lossy().into_owned();
273    let suite: TestSuite = serde_json::from_str(&s).map_err(|e| TestError {
274        name: "Unknown".to_string(),
275        path: path.clone(),
276        kind: e.into(),
277    })?;
278
279    for (name, unit) in suite.0 {
280        // Prepare initial state.
281        let cache_state = unit.state();
282
283        // Setup base configuration.
284        let mut cfg = CfgEnv::default();
285        cfg.chain_id = unit.env.current_chain_id.unwrap_or(U256::ONE).try_into().unwrap_or(1);
286
287        // Post and execution.
288        for (spec_name, tests) in &unit.post {
289            // Skip Constantinople spec.
290            if *spec_name == SpecName::Constantinople {
291                continue;
292            }
293
294            cfg.set_spec_and_mainnet_gas_params(spec_name.to_spec_id());
295
296            // Configure max blobs per spec.
297            if cfg.spec().is_enabled_in(SpecId::OSAKA) {
298                cfg.set_max_blobs_per_tx(6);
299            } else if cfg.spec().is_enabled_in(SpecId::PRAGUE) {
300                cfg.set_max_blobs_per_tx(9);
301            } else {
302                cfg.set_max_blobs_per_tx(6);
303            }
304
305            // Setup block environment for this spec.
306            let block = unit.block_env(&mut cfg);
307
308            for test in tests.iter() {
309                // Setup transaction environment.
310                let tx = match test.tx_env(&unit) {
311                    Ok(tx) => tx,
312                    Err(_) if test.expect_exception.is_some() => continue,
313                    Err(_) => {
314                        return Err(TestError {
315                            name,
316                            path,
317                            kind: TestErrorKind::UnknownPrivateKey(unit.transaction.secret_key),
318                        });
319                    }
320                };
321
322                // Execute the test.
323                let result = execute_single_test(TestExecutionContext {
324                    name: &name,
325                    unit: &unit,
326                    test,
327                    cfg: &cfg,
328                    block: &block,
329                    tx: &tx,
330                    cache_state: &cache_state,
331                    elapsed,
332                    trace,
333                    print_json_outcome,
334                });
335
336                if let Err(e) = result {
337                    // Handle error with debug trace if needed.
338                    static FAILED: AtomicBool = AtomicBool::new(false);
339                    if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) {
340                        return Err(TestError { name, path, kind: e });
341                    }
342
343                    return Err(TestError { path, name, kind: e });
344                }
345            }
346        }
347    }
348    Ok(())
349}
350
351fn execute_single_test(ctx: TestExecutionContext) -> Result<(), TestErrorKind> {
352    // Prepare state.
353    let cache = ctx.cache_state.clone();
354    let mut state =
355        database::State::builder().with_cached_prestate(cache).with_bundle_update().build();
356
357    let evm_context = Context::mainnet()
358        .with_block(ctx.block)
359        .with_tx(ctx.tx)
360        .with_cfg(ctx.cfg.clone())
361        .with_db(&mut state);
362
363    // Execute.
364    let timer = Instant::now();
365    let mut evm = evm_context.build_mainnet();
366    let exec_result = evm.transact_commit(ctx.tx);
367    let db = evm.ctx.journaled_state.database;
368    *ctx.elapsed.lock().unwrap() += timer.elapsed();
369
370    // Check results.
371    check_evm_execution(
372        ctx.test,
373        ctx.unit.out.as_ref(),
374        ctx.name,
375        &exec_result,
376        db,
377        *ctx.cfg.spec(),
378        ctx.print_json_outcome,
379    )
380}
381
382#[derive(Clone)]
383pub(crate) struct TestRunnerState {
384    pub(crate) n_errors: Arc<AtomicUsize>,
385    pub(crate) console_bar: Arc<ProgressBar>,
386    pub(crate) queue: Arc<Mutex<(usize, Vec<PathBuf>)>>,
387    pub(crate) elapsed: Arc<Mutex<Duration>>,
388    /// Set when any worker thread requests all others to stop (e.g. on panic).
389    pub(crate) stop: Arc<AtomicBool>,
390}
391
392fn console_bar(n_files: usize) -> ProgressBar {
393    let bar = ProgressBar::with_draw_target(
394        Some(n_files as u64),
395        ProgressDrawTarget::term_like_with_hz(Box::new(Term::buffered_stderr()), 1),
396    );
397    bar.set_style(
398        ProgressStyle::with_template(
399            "[{elapsed_precise}] {wide_bar} {pos}/{len} ({per_sec}, eta {eta})",
400        )
401        .unwrap()
402        .progress_chars("=>-"),
403    );
404    bar.enable_steady_tick(Duration::from_secs(1));
405    bar
406}
407
408impl TestRunnerState {
409    pub(crate) fn new(test_files: Vec<PathBuf>) -> Self {
410        let n_files = test_files.len();
411        Self {
412            n_errors: Arc::new(AtomicUsize::new(0)),
413            console_bar: Arc::new(console_bar(n_files)),
414            queue: Arc::new(Mutex::new((0usize, test_files))),
415            elapsed: Arc::new(Mutex::new(Duration::ZERO)),
416            stop: Arc::new(AtomicBool::new(false)),
417        }
418    }
419
420    pub(crate) fn next_test(&self) -> Option<PathBuf> {
421        if self.stop.load(Ordering::Relaxed) {
422            return None;
423        }
424        let (current_idx, queue) = &mut *self.queue.lock().unwrap();
425        let idx = *current_idx;
426        let test_path = queue.get(idx).cloned()?;
427        *current_idx = idx + 1;
428        Some(test_path)
429    }
430}