1use crate::cmd::statetest::merkle_trie::{compute_test_roots, TestValidationResult};
2use indicatif::{ProgressBar, ProgressDrawTarget};
3use revm::{
4 context::{block::BlockEnv, cfg::CfgEnv, tx::TxEnv},
5 context_interface::result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction},
6 database::{self, bal::EvmDatabaseError},
7 database_interface::EmptyDB,
8 inspector::{inspectors::TracerEip3155, InspectCommitEvm},
9 primitives::{hardfork::SpecId, Bytes, B256, U256},
10 statetest_types::{SpecName, Test, TestSuite, TestUnit},
11 Context, ExecuteCommitEvm, MainBuilder, MainContext,
12};
13use serde_json::json;
14use std::{
15 convert::Infallible,
16 fmt::Debug,
17 io::stderr,
18 path::{Path, PathBuf},
19 sync::{
20 atomic::{AtomicBool, AtomicUsize, Ordering},
21 Arc, Mutex,
22 },
23 time::{Duration, Instant},
24};
25use thiserror::Error;
26
27#[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#[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 {
47 expected_exception: Option<String>,
48 got_exception: Option<String>,
49 },
50 #[error("unexpected output: got {got_output:?}, expected {expected_output:?}")]
51 UnexpectedOutput {
52 expected_output: Option<Bytes>,
53 got_output: Option<Bytes>,
54 },
55 #[error(transparent)]
56 SerdeDeserialize(#[from] serde_json::Error),
57 #[error("thread panicked")]
58 Panic,
59 #[error("path does not exist")]
60 InvalidPath,
61 #[error("no JSON test files found in path")]
62 NoJsonFiles,
63}
64
65fn skip_test(path: &Path) -> bool {
68 let path_str = path.to_str().unwrap_or_default();
69
70 if path_str.contains("paris/eip7610_create_collision") {
72 return true;
73 }
74
75 let name = path.file_name().unwrap().to_str().unwrap_or_default();
76
77 matches!(
78 name,
79 | "CreateTransactionHighNonce.json"
81
82 | "RevertInCreateInInit_Paris.json"
84 | "RevertInCreateInInit.json"
85 | "dynamicAccountOverwriteEmpty.json"
86 | "dynamicAccountOverwriteEmpty_Paris.json"
87 | "RevertInCreateInInitCreate2Paris.json"
88 | "create2collisionStorage.json"
89 | "RevertInCreateInInitCreate2.json"
90 | "create2collisionStorageParis.json"
91 | "InitCollision.json"
92 | "InitCollisionParis.json"
93 | "test_init_collision_create_opcode.json"
94
95 | "ValueOverflow.json"
97 | "ValueOverflowParis.json"
98
99 | "Call50000_sha256.json"
101 | "static_Call50000_sha256.json"
102 | "loopMul.json"
103 | "CALLBlake2f_MaxRounds.json"
104 )
105}
106
107struct TestExecutionContext<'a> {
108 name: &'a str,
109 unit: &'a TestUnit,
110 test: &'a Test,
111 cfg: &'a CfgEnv,
112 block: &'a BlockEnv,
113 tx: &'a TxEnv,
114 cache_state: &'a database::CacheState,
115 elapsed: &'a Arc<Mutex<Duration>>,
116 trace: bool,
117 print_json_outcome: bool,
118}
119
120struct DebugContext<'a> {
121 name: &'a str,
122 path: &'a str,
123 index: usize,
124 test: &'a Test,
125 cfg: &'a CfgEnv,
126 block: &'a BlockEnv,
127 tx: &'a TxEnv,
128 cache_state: &'a database::CacheState,
129 error: &'a TestErrorKind,
130}
131
132fn build_json_output(
133 test: &Test,
134 test_name: &str,
135 exec_result: &Result<
136 ExecutionResult<HaltReason>,
137 EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
138 >,
139 validation: &TestValidationResult,
140 spec: SpecId,
141 error: Option<String>,
142) -> serde_json::Value {
143 json!({
144 "stateRoot": validation.state_root,
145 "logsRoot": validation.logs_root,
146 "output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(),
147 "gasUsed": exec_result.as_ref().ok().map(|r| r.gas_used()).unwrap_or_default(),
148 "pass": error.is_none(),
149 "errorMsg": error.unwrap_or_default(),
150 "evmResult": format_evm_result(exec_result),
151 "postLogsHash": validation.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}
159
160fn format_evm_result(
161 exec_result: &Result<
162 ExecutionResult<HaltReason>,
163 EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
164 >,
165) -> String {
166 match exec_result {
167 Ok(r) => match r {
168 ExecutionResult::Success { reason, .. } => format!("Success: {reason:?}"),
169 ExecutionResult::Revert { .. } => "Revert".to_string(),
170 ExecutionResult::Halt { reason, .. } => format!("Halt: {reason:?}"),
171 },
172 Err(e) => e.to_string(),
173 }
174}
175
176fn validate_exception(
177 test: &Test,
178 exec_result: &Result<
179 ExecutionResult<HaltReason>,
180 EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
181 >,
182) -> Result<bool, TestErrorKind> {
183 match (&test.expect_exception, exec_result) {
184 (None, Ok(_)) => Ok(false), (Some(_), Err(_)) => Ok(true), _ => Err(TestErrorKind::UnexpectedException {
187 expected_exception: test.expect_exception.clone(),
188 got_exception: exec_result.as_ref().err().map(|e| e.to_string()),
189 }),
190 }
191}
192
193fn validate_output(
194 expected_output: Option<&Bytes>,
195 actual_result: &ExecutionResult<HaltReason>,
196) -> Result<(), TestErrorKind> {
197 if let Some((expected, actual)) = expected_output.zip(actual_result.output()) {
198 if expected != actual {
199 return Err(TestErrorKind::UnexpectedOutput {
200 expected_output: Some(expected.clone()),
201 got_output: actual_result.output().cloned(),
202 });
203 }
204 }
205 Ok(())
206}
207
208fn check_evm_execution(
209 test: &Test,
210 expected_output: Option<&Bytes>,
211 test_name: &str,
212 exec_result: &Result<
213 ExecutionResult<HaltReason>,
214 EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
215 >,
216 db: &mut database::State<EmptyDB>,
217 spec: SpecId,
218 print_json_outcome: bool,
219) -> Result<(), TestErrorKind> {
220 let validation = compute_test_roots(exec_result, db);
221
222 let print_json = |error: Option<&TestErrorKind>| {
223 if print_json_outcome {
224 let json = build_json_output(
225 test,
226 test_name,
227 exec_result,
228 &validation,
229 spec,
230 error.map(|e| e.to_string()),
231 );
232 eprintln!("{json}");
233 }
234 };
235
236 let exception_expected = validate_exception(test, exec_result).inspect_err(|e| {
238 print_json(Some(e));
239 })?;
240
241 if exception_expected {
243 print_json(None);
244 return Ok(());
245 }
246
247 if let Ok(result) = exec_result {
249 validate_output(expected_output, result).inspect_err(|e| {
250 print_json(Some(e));
251 })?;
252 }
253
254 if validation.logs_root != test.logs {
256 let error = TestErrorKind::LogsRootMismatch {
257 got: validation.logs_root,
258 expected: test.logs,
259 };
260 print_json(Some(&error));
261 return Err(error);
262 }
263
264 if validation.state_root != test.hash {
266 let error = TestErrorKind::StateRootMismatch {
267 got: validation.state_root,
268 expected: test.hash,
269 };
270 print_json(Some(&error));
271 return Err(error);
272 }
273
274 print_json(None);
275 Ok(())
276}
277
278pub fn execute_test_suite(
286 path: &Path,
287 elapsed: &Arc<Mutex<Duration>>,
288 trace: bool,
289 print_json_outcome: bool,
290) -> Result<(), TestError> {
291 if skip_test(path) {
292 return Ok(());
293 }
294
295 let s = std::fs::read_to_string(path).unwrap();
296 let path = path.to_string_lossy().into_owned();
297 let suite: TestSuite = serde_json::from_str(&s).map_err(|e| TestError {
298 name: "Unknown".to_string(),
299 path: path.clone(),
300 kind: e.into(),
301 })?;
302
303 for (name, unit) in suite.0 {
304 let cache_state = unit.state();
306
307 let mut cfg = CfgEnv::default();
309 cfg.chain_id = unit
310 .env
311 .current_chain_id
312 .unwrap_or(U256::ONE)
313 .try_into()
314 .unwrap_or(1);
315
316 for (spec_name, tests) in &unit.post {
318 if *spec_name == SpecName::Constantinople {
320 continue;
321 }
322
323 cfg.set_spec_and_mainnet_gas_params(spec_name.to_spec_id());
324
325 if cfg.spec().is_enabled_in(SpecId::OSAKA) {
327 cfg.set_max_blobs_per_tx(6);
328 } else if cfg.spec().is_enabled_in(SpecId::PRAGUE) {
329 cfg.set_max_blobs_per_tx(9);
330 } else {
331 cfg.set_max_blobs_per_tx(6);
332 }
333
334 let block = unit.block_env(&mut cfg);
336
337 for (index, test) in tests.iter().enumerate() {
338 let tx = match test.tx_env(&unit) {
340 Ok(tx) => tx,
341 Err(_) if test.expect_exception.is_some() => continue,
342 Err(_) => {
343 return Err(TestError {
344 name,
345 path,
346 kind: TestErrorKind::UnknownPrivateKey(unit.transaction.secret_key),
347 });
348 }
349 };
350
351 let result = execute_single_test(TestExecutionContext {
353 name: &name,
354 unit: &unit,
355 test,
356 cfg: &cfg,
357 block: &block,
358 tx: &tx,
359 cache_state: &cache_state,
360 elapsed,
361 trace,
362 print_json_outcome,
363 });
364
365 if let Err(e) = result {
366 static FAILED: AtomicBool = AtomicBool::new(false);
368 if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) {
369 return Err(TestError {
370 name,
371 path,
372 kind: e,
373 });
374 }
375
376 debug_failed_test(DebugContext {
378 name: &name,
379 path: &path,
380 index,
381 test,
382 cfg: &cfg,
383 block: &block,
384 tx: &tx,
385 cache_state: &cache_state,
386 error: &e,
387 });
388
389 return Err(TestError {
390 path,
391 name,
392 kind: e,
393 });
394 }
395 }
396 }
397 }
398 Ok(())
399}
400
401fn execute_single_test(ctx: TestExecutionContext) -> Result<(), TestErrorKind> {
402 let mut cache = ctx.cache_state.clone();
404 let spec = ctx.cfg.spec();
405 cache.set_state_clear_flag(spec.is_enabled_in(SpecId::SPURIOUS_DRAGON));
406 let mut state = database::State::builder()
407 .with_cached_prestate(cache)
408 .with_bundle_update()
409 .build();
410
411 let evm_context = Context::mainnet()
412 .with_block(ctx.block)
413 .with_tx(ctx.tx)
414 .with_cfg(ctx.cfg.clone())
415 .with_db(&mut state);
416
417 let timer = Instant::now();
419 let (db, exec_result) = if ctx.trace {
420 let mut evm = evm_context
421 .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary());
422 let res = evm.inspect_tx_commit(ctx.tx);
423 let db = evm.ctx.journaled_state.database;
424 (db, res)
425 } else {
426 let mut evm = evm_context.build_mainnet();
427 let res = evm.transact_commit(ctx.tx);
428 let db = evm.ctx.journaled_state.database;
429 (db, res)
430 };
431 *ctx.elapsed.lock().unwrap() += timer.elapsed();
432
433 let exec_result = exec_result;
434 check_evm_execution(
436 ctx.test,
437 ctx.unit.out.as_ref(),
438 ctx.name,
439 &exec_result,
440 db,
441 *ctx.cfg.spec(),
442 ctx.print_json_outcome,
443 )
444}
445
446fn debug_failed_test(ctx: DebugContext) {
447 println!("\nTraces:");
448
449 let mut cache = ctx.cache_state.clone();
451 cache.set_state_clear_flag(ctx.cfg.spec().is_enabled_in(SpecId::SPURIOUS_DRAGON));
452 let mut state = database::State::builder()
453 .with_cached_prestate(cache)
454 .with_bundle_update()
455 .build();
456
457 let mut evm = Context::mainnet()
458 .with_db(&mut state)
459 .with_block(ctx.block)
460 .with_tx(ctx.tx)
461 .with_cfg(ctx.cfg.clone())
462 .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary());
463
464 let exec_result = evm.inspect_tx_commit(ctx.tx);
465
466 println!("\nExecution result: {exec_result:#?}");
467 println!("\nExpected exception: {:?}", ctx.test.expect_exception);
468 println!("\nState before:\n{}", ctx.cache_state.pretty_print());
469 println!(
470 "\nState after:\n{}",
471 evm.ctx.journaled_state.database.cache.pretty_print()
472 );
473 println!("\nSpecification: {:?}", ctx.cfg.spec());
474 println!("\nTx: {:#?}", ctx.tx);
475 println!("Block: {:#?}", ctx.block);
476 println!("Cfg: {:#?}", ctx.cfg);
477 println!(
478 "\nTest name: {:?} (index: {}, path: {:?}) failed:\n{}",
479 ctx.name, ctx.index, ctx.path, ctx.error
480 );
481}
482
483#[derive(Clone, Copy)]
484struct TestRunnerConfig {
485 single_thread: bool,
486 trace: bool,
487 print_outcome: bool,
488 keep_going: bool,
489}
490
491impl TestRunnerConfig {
492 fn new(single_thread: bool, trace: bool, print_outcome: bool, keep_going: bool) -> Self {
493 let print_outcome = print_outcome || trace;
495 let single_thread = single_thread || print_outcome;
497
498 Self {
499 single_thread,
500 trace,
501 print_outcome,
502 keep_going,
503 }
504 }
505}
506
507#[derive(Clone)]
508struct TestRunnerState {
509 n_errors: Arc<AtomicUsize>,
510 console_bar: Arc<ProgressBar>,
511 queue: Arc<Mutex<(usize, Vec<PathBuf>)>>,
512 elapsed: Arc<Mutex<Duration>>,
513}
514
515impl TestRunnerState {
516 fn new(test_files: Vec<PathBuf>) -> Self {
517 let n_files = test_files.len();
518 Self {
519 n_errors: Arc::new(AtomicUsize::new(0)),
520 console_bar: Arc::new(ProgressBar::with_draw_target(
521 Some(n_files as u64),
522 ProgressDrawTarget::stdout(),
523 )),
524 queue: Arc::new(Mutex::new((0usize, test_files))),
525 elapsed: Arc::new(Mutex::new(Duration::ZERO)),
526 }
527 }
528
529 fn next_test(&self) -> Option<PathBuf> {
530 let (current_idx, queue) = &mut *self.queue.lock().unwrap();
531 let idx = *current_idx;
532 let test_path = queue.get(idx).cloned()?;
533 *current_idx = idx + 1;
534 Some(test_path)
535 }
536}
537
538fn run_test_worker(state: TestRunnerState, config: TestRunnerConfig) -> Result<(), TestError> {
539 loop {
540 if !config.keep_going && state.n_errors.load(Ordering::SeqCst) > 0 {
541 return Ok(());
542 }
543
544 let Some(test_path) = state.next_test() else {
545 return Ok(());
546 };
547
548 let result = execute_test_suite(
549 &test_path,
550 &state.elapsed,
551 config.trace,
552 config.print_outcome,
553 );
554
555 state.console_bar.inc(1);
556
557 if let Err(err) = result {
558 state.n_errors.fetch_add(1, Ordering::SeqCst);
559 if !config.keep_going {
560 return Err(err);
561 }
562 }
563 }
564}
565
566fn determine_thread_count(single_thread: bool, n_files: usize) -> usize {
567 match (single_thread, std::thread::available_parallelism()) {
568 (true, _) | (false, Err(_)) => 1,
569 (false, Ok(n)) => n.get().min(n_files),
570 }
571}
572
573pub fn run(
582 test_files: Vec<PathBuf>,
583 single_thread: bool,
584 trace: bool,
585 print_outcome: bool,
586 keep_going: bool,
587) -> Result<(), TestError> {
588 let config = TestRunnerConfig::new(single_thread, trace, print_outcome, keep_going);
589 let n_files = test_files.len();
590 let state = TestRunnerState::new(test_files);
591 let num_threads = determine_thread_count(config.single_thread, n_files);
592
593 let mut handles = Vec::with_capacity(num_threads);
595 for i in 0..num_threads {
596 let state = state.clone();
597
598 let thread = std::thread::Builder::new()
599 .name(format!("runner-{i}"))
600 .spawn(move || run_test_worker(state, config))
601 .unwrap();
602
603 handles.push(thread);
604 }
605
606 let mut thread_errors = Vec::new();
608 for (i, handle) in handles.into_iter().enumerate() {
609 match handle.join() {
610 Ok(Ok(())) => {}
611 Ok(Err(e)) => thread_errors.push(e),
612 Err(_) => thread_errors.push(TestError {
613 name: format!("thread {i} panicked"),
614 path: String::new(),
615 kind: TestErrorKind::Panic,
616 }),
617 }
618 }
619
620 state.console_bar.finish();
621
622 println!(
624 "Finished execution. Total CPU time: {:.6}s",
625 state.elapsed.lock().unwrap().as_secs_f64()
626 );
627
628 let n_errors = state.n_errors.load(Ordering::SeqCst);
629 let n_thread_errors = thread_errors.len();
630
631 if n_errors == 0 && n_thread_errors == 0 {
632 println!("All tests passed!");
633 Ok(())
634 } else {
635 println!("Encountered {n_errors} errors out of {n_files} total tests");
636
637 if n_thread_errors == 0 {
638 std::process::exit(1);
639 }
640
641 if n_thread_errors > 1 {
642 println!("{n_thread_errors} threads returned an error, out of {num_threads} total:");
643 for error in &thread_errors {
644 println!("{error}");
645 }
646 }
647 Err(thread_errors.swap_remove(0))
648 }
649}