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