1use crate::executors::{
2 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
3 corpus::{GlobalCorpusMetrics, WorkerCorpus},
4};
5use alloy_dyn_abi::JsonAbiExt;
6use alloy_json_abi::Function;
7use alloy_primitives::{Address, Bytes, Log, U256, keccak256, map::HashMap};
8use eyre::Result;
9use foundry_common::sh_println;
10use foundry_config::FuzzConfig;
11use foundry_evm_core::{
12 Breakpoints,
13 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
14 decode::{RevertDecoder, SkipReason},
15};
16use foundry_evm_coverage::HitMaps;
17use foundry_evm_fuzz::{
18 BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
19 FuzzFixtures, FuzzTestResult,
20 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
21};
22use foundry_evm_traces::SparsedTraceArena;
23use indicatif::ProgressBar;
24use proptest::{
25 strategy::Strategy,
26 test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner},
27};
28use rayon::iter::{IntoParallelIterator, ParallelIterator};
29use serde_json::json;
30use std::{
31 sync::{
32 Arc, OnceLock,
33 atomic::{AtomicU32, Ordering},
34 },
35 time::{Instant, SystemTime, UNIX_EPOCH},
36};
37
38mod types;
39pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
40
41const SYNC_INTERVAL: u32 = 1000;
43
44const MIN_RUNS_PER_WORKER: u32 = 64;
47
48#[derive(Default)]
49struct WorkerState {
50 id: usize,
52 first_case: Option<(u32, FuzzCase)>,
54 gas_by_case: Vec<(u64, u64)>,
56 counterexample: (Bytes, RawCallResult),
58 traces: Vec<SparsedTraceArena>,
62 breakpoints: Option<Breakpoints>,
64 coverage: Option<HitMaps>,
66 logs: Vec<Log>,
68 deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
70 runs: u32,
72 failure: Option<TestCaseError>,
74 last_run_timestamp: u128,
78 failed_corpus_replays: usize,
80}
81
82impl WorkerState {
83 fn new(worker_id: usize) -> Self {
84 Self { id: worker_id, ..Default::default() }
85 }
86}
87
88struct SharedFuzzState {
90 state: EvmFuzzState,
91 total_runs: Arc<AtomicU32>,
93 failed_worker_id: OnceLock<usize>,
99 total_rejects: Arc<AtomicU32>,
101 timer: FuzzTestTimer,
103 global_corpus_metrics: GlobalCorpusMetrics,
105
106 global_early_exit: EarlyExit,
108 local_early_exit: EarlyExit,
110}
111
112impl SharedFuzzState {
113 fn new(state: EvmFuzzState, timeout: Option<u32>, early_exit: EarlyExit) -> Self {
114 Self {
115 state,
116 total_runs: Arc::new(AtomicU32::new(0)),
117 failed_worker_id: OnceLock::new(),
118 total_rejects: Arc::new(AtomicU32::new(0)),
119 timer: FuzzTestTimer::new(timeout),
120 global_corpus_metrics: GlobalCorpusMetrics::default(),
121 global_early_exit: early_exit,
122 local_early_exit: EarlyExit::new(true),
123 }
124 }
125
126 fn increment_runs(&self) -> u32 {
128 self.total_runs.fetch_add(1, Ordering::Relaxed) + 1
129 }
130
131 fn increment_rejects(&self) -> u32 {
133 self.total_rejects.fetch_add(1, Ordering::Relaxed) + 1
134 }
135
136 fn should_continue(&self) -> bool {
138 !(self.global_early_exit.should_stop()
139 || self.local_early_exit.should_stop()
140 || self.timer.is_timed_out())
141 }
142
143 fn try_claim_failure(&self, worker_id: usize) -> bool {
146 let mut claimed = false;
147 let _ = self.failed_worker_id.get_or_init(|| {
148 claimed = true;
149 self.local_early_exit.record_failure();
150 worker_id
151 });
152 claimed
153 }
154}
155
156pub struct FuzzedExecutor {
162 executor_f: Executor,
164 runner: TestRunner,
166 sender: Address,
168 config: FuzzConfig,
170 persisted_failure: Option<BaseCounterExample>,
172 num_workers: usize,
174}
175
176impl FuzzedExecutor {
177 pub fn new(
179 executor: Executor,
180 runner: TestRunner,
181 sender: Address,
182 config: FuzzConfig,
183 persisted_failure: Option<BaseCounterExample>,
184 ) -> Self {
185 let mut max_workers = Ord::max(1, config.runs / MIN_RUNS_PER_WORKER);
186 if config.runs == 0 {
187 max_workers = 0;
188 }
189 let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize);
190 Self { executor_f: executor, runner, sender, config, persisted_failure, num_workers }
191 }
192
193 #[allow(clippy::too_many_arguments)]
199 pub fn fuzz(
200 &mut self,
201 func: &Function,
202 fuzz_fixtures: &FuzzFixtures,
203 state: EvmFuzzState,
204 address: Address,
205 rd: &RevertDecoder,
206 progress: Option<&ProgressBar>,
207 early_exit: &EarlyExit,
208 tokio_handle: &tokio::runtime::Handle,
209 ) -> Result<FuzzTestResult> {
210 let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone());
211
212 debug!(n = self.num_workers, "spawning workers");
213 let workers = (0..self.num_workers)
214 .into_par_iter()
215 .map(|worker_id| {
216 let _guard = tokio_handle.enter();
217 let _guard = info_span!("fuzz_worker", id = worker_id).entered();
218 let timer = Instant::now();
219 let r = self.run_worker(
220 worker_id,
221 func,
222 fuzz_fixtures,
223 address,
224 rd,
225 &shared_state,
226 progress,
227 );
228 debug!("finished in {:?}", timer.elapsed());
229 r
230 })
231 .collect::<Result<Vec<_>>>()?;
232
233 Ok(self.aggregate_results(workers, func, &shared_state))
234 }
235
236 fn single_fuzz(
239 &self,
240 executor: &Executor,
241 address: Address,
242 calldata: Bytes,
243 coverage_metrics: &mut WorkerCorpus,
244 ) -> Result<FuzzOutcome, TestCaseError> {
245 let mut call = executor
246 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
247 .map_err(|e| TestCaseError::fail(e.to_string()))?;
248 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
249 coverage_metrics.process_inputs(
250 &[BasicTxDetails {
251 warp: None,
252 roll: None,
253 sender: self.sender,
254 call_details: CallDetails { target: address, calldata: calldata.clone() },
255 }],
256 new_coverage,
257 );
258
259 if call.result.as_ref() == MAGIC_ASSUME {
261 return Err(TestCaseError::reject(FuzzError::AssumeReject));
262 }
263
264 let (breakpoints, deprecated_cheatcodes) =
265 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
266 (cheats.breakpoints.clone(), cheats.deprecated.clone())
267 });
268
269 let success = if !self.config.fail_on_revert
272 && call
273 .reverter
274 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
275 {
276 true
277 } else {
278 executor.is_raw_call_mut_success(address, &mut call, false)
279 };
280
281 if success {
282 Ok(FuzzOutcome::Case(CaseOutcome {
283 case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
284 traces: call.traces,
285 coverage: call.line_coverage,
286 breakpoints,
287 logs: call.logs,
288 deprecated_cheatcodes,
289 }))
290 } else {
291 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
292 exit_reason: call.exit_reason,
293 counterexample: (calldata, call),
294 breakpoints,
295 }))
296 }
297 }
298
299 fn aggregate_results(
301 &self,
302 mut workers: Vec<WorkerState>,
303 func: &Function,
304 shared_state: &SharedFuzzState,
305 ) -> FuzzTestResult {
306 let mut result = FuzzTestResult::default();
307 if workers.is_empty() {
308 result.success = true;
309 return result;
310 }
311
312 let mut first_case_candidate = None;
314 let mut last_run_worker = None;
315 for (i, worker) in workers.iter().enumerate() {
316 if let Some((run, ref case)) = worker.first_case
317 && first_case_candidate.as_ref().is_none_or(|&(r, _)| run < r)
318 {
319 first_case_candidate = Some((run, case.clone()));
320 }
321
322 if last_run_worker.is_none_or(|(t, _)| worker.last_run_timestamp > t) {
323 last_run_worker = Some((worker.last_run_timestamp, i));
324 }
325
326 if worker.id == 0 {
328 result.failed_corpus_replays = worker.failed_corpus_replays;
329 }
330 }
331 result.first_case = first_case_candidate.map(|(_, case)| case).unwrap_or_default();
332 let (_, last_run_worker_idx) = last_run_worker.expect("at least one worker");
333
334 if let Some(&failed_worker_id) = shared_state.failed_worker_id.get() {
335 result.success = false;
336
337 let failed_worker_idx = workers.iter().position(|w| w.id == failed_worker_id).unwrap();
338 let failed_worker = &mut workers[failed_worker_idx];
339
340 let (calldata, call) = std::mem::take(&mut failed_worker.counterexample);
341 result.labels = call.labels;
342 result.traces = call.traces.clone();
343 result.breakpoints = call.cheatcodes.map(|c| c.breakpoints);
344
345 match &failed_worker.failure {
346 Some(TestCaseError::Fail(reason)) => {
347 let reason = reason.to_string();
348 result.reason = (!reason.is_empty()).then_some(reason);
349 let args = if let Some(data) = calldata.get(4..) {
350 func.abi_decode_input(data).unwrap_or_default()
351 } else {
352 vec![]
353 };
354 result.counterexample = Some(CounterExample::Single(
355 BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
356 ));
357 }
358 Some(TestCaseError::Reject(reason)) => {
359 let reason = reason.to_string();
360 result.reason = (!reason.is_empty()).then_some(reason);
361 }
362 None => {}
363 }
364 } else {
365 let last_run_worker = &workers[last_run_worker_idx];
366 result.success = true;
367 result.traces = last_run_worker.traces.last().cloned();
368 result.breakpoints = last_run_worker.breakpoints.clone();
369 }
370
371 if !self.config.show_logs {
372 result.logs = workers[last_run_worker_idx].logs.clone();
373 }
374
375 for mut worker in workers {
376 result.gas_by_case.append(&mut worker.gas_by_case);
377 if self.config.show_logs {
378 result.logs.append(&mut worker.logs);
379 }
380 result.gas_report_traces.extend(worker.traces.into_iter().map(|t| t.arena));
381 HitMaps::merge_opt(&mut result.line_coverage, worker.coverage);
382 result.deprecated_cheatcodes.extend(worker.deprecated_cheatcodes);
383 }
384
385 if let Some(reason) = &result.reason
386 && let Some(reason) = SkipReason::decode_self(reason)
387 {
388 result.skipped = true;
389 result.reason = reason.0;
390 }
391
392 result
393 }
394
395 #[allow(clippy::too_many_arguments)]
397 fn run_worker(
398 &self,
399 worker_id: usize,
400 func: &Function,
401 fuzz_fixtures: &FuzzFixtures,
402 address: Address,
403 rd: &RevertDecoder,
404 shared_state: &SharedFuzzState,
405 progress: Option<&ProgressBar>,
406 ) -> Result<WorkerState> {
407 let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
409 let strategy = proptest::prop_oneof![
410 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
411 dictionary_weight => fuzz_calldata_from_state(func.clone(), &shared_state.state),
412 ]
413 .prop_map(move |calldata| BasicTxDetails {
414 warp: None,
415 roll: None,
416 sender: Default::default(),
417 call_details: CallDetails { target: Default::default(), calldata },
418 });
419
420 let mut corpus = WorkerCorpus::new(
421 worker_id,
422 self.config.corpus.clone(),
423 strategy.boxed(),
424 if worker_id == 0 { Some(&self.executor_f) } else { None },
426 Some(func),
427 None, )?;
429 let mut executor = self.executor_f.clone();
430
431 let mut worker = WorkerState::new(worker_id);
432 let max_traces_to_collect =
434 std::cmp::max(1, self.config.gas_report_samples / self.num_workers as u32);
435
436 let worker_runs = self.runs_per_worker(worker_id);
437 debug!(worker_runs);
438
439 let mut runner_config = self.runner.config().clone();
440 runner_config.cases = worker_runs;
441
442 let mut runner = if let Some(seed) = self.config.seed {
443 let worker_seed = if worker_id == 0 {
445 seed
447 } else {
448 let seed_data =
450 [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat();
451 U256::from_be_bytes(keccak256(seed_data).0)
452 };
453 trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}");
454 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
455 TestRunner::new_with_rng(runner_config, rng)
456 } else {
457 TestRunner::new(runner_config)
458 };
459
460 let mut persisted_failure = self.persisted_failure.as_ref().filter(|_| worker_id == 0);
461
462 let sync_offset = worker_id as u32 * 100;
465 let sync_threshold = SYNC_INTERVAL + sync_offset;
466 let mut runs_since_sync = sync_threshold; let mut last_metrics_report = Instant::now();
468 'stop: while shared_state.should_continue() && worker.runs < worker_runs {
472 let input = if worker_id == 0
474 && let Some(failure) = persisted_failure.take()
475 && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector)
476 {
477 failure.calldata.clone()
478 } else {
479 runs_since_sync += 1;
480 if runs_since_sync >= sync_threshold {
481 let timer = Instant::now();
482 corpus.sync(
483 self.num_workers,
484 &executor,
485 Some(func),
486 None,
487 &shared_state.global_corpus_metrics,
488 )?;
489 trace!("finished corpus sync in {:?}", timer.elapsed());
490 runs_since_sync = 0;
491 }
492
493 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
494 && let Some(seed) = self.config.seed
495 {
496 cheats.set_seed(seed.wrapping_add(U256::from(worker.runs)));
497 }
498
499 match corpus.new_input(&mut runner, &shared_state.state, func) {
500 Ok(input) => input,
501 Err(err) => {
502 worker.failure = Some(TestCaseError::fail(format!(
503 "failed to generate fuzzed input in worker {}: {err}",
504 worker.id
505 )));
506 shared_state.try_claim_failure(worker_id);
507 break 'stop;
508 }
509 }
510 };
511
512 let mut inc_runs = || {
513 let total_runs = shared_state.increment_runs();
514 debug_assert!(
515 shared_state.timer.is_enabled() || total_runs <= self.config.runs,
516 "worker runs were not distributed correctly"
517 );
518 worker.runs += 1;
519 if let Some(progress) = progress {
520 progress.inc(1);
521 }
522 total_runs
523 };
524
525 worker.last_run_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
526 match self.single_fuzz(&executor, address, input, &mut corpus) {
527 Ok(fuzz_outcome) => match fuzz_outcome {
528 FuzzOutcome::Case(case) => {
529 let total_runs = inc_runs();
530
531 if worker_id == 0 && self.config.corpus.collect_edge_coverage() {
532 if let Some(progress) = progress {
533 corpus.sync_metrics(&shared_state.global_corpus_metrics);
534 progress
535 .set_message(format!("{}", shared_state.global_corpus_metrics));
536 } else if last_metrics_report.elapsed()
537 > DURATION_BETWEEN_METRICS_REPORT
538 {
539 corpus.sync_metrics(&shared_state.global_corpus_metrics);
540 let metrics = json!({
542 "timestamp": SystemTime::now()
543 .duration_since(UNIX_EPOCH)?
544 .as_secs(),
545 "test": func.name,
546 "metrics": shared_state.global_corpus_metrics.load(),
547 });
548 let _ = sh_println!("{metrics}");
549 last_metrics_report = Instant::now();
550 }
551 }
552
553 worker.gas_by_case.push((case.case.gas, case.case.stipend));
554
555 if worker.first_case.is_none() {
556 worker.first_case = Some((total_runs, case.case));
557 }
558
559 if let Some(call_traces) = case.traces {
560 if worker.traces.len() == max_traces_to_collect as usize {
561 worker.traces.pop();
562 }
563 worker.traces.push(call_traces);
564 worker.breakpoints = Some(case.breakpoints);
565 }
566
567 if self.config.show_logs {
571 worker.logs.extend(case.logs);
572 } else {
573 worker.logs = case.logs;
574 }
575
576 HitMaps::merge_opt(&mut worker.coverage, case.coverage);
577 worker.deprecated_cheatcodes = case.deprecated_cheatcodes;
578 }
579 FuzzOutcome::CounterExample(CounterExampleOutcome {
580 exit_reason: status,
581 counterexample: outcome,
582 ..
583 }) => {
584 inc_runs();
585
586 let reason = rd.maybe_decode(&outcome.1.result, status);
587 worker.logs.extend(outcome.1.logs.clone());
588 worker.counterexample = outcome;
589 worker.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
590 shared_state.try_claim_failure(worker_id);
591 break 'stop;
592 }
593 },
594 Err(err) => match err {
595 TestCaseError::Fail(_) => {
596 worker.failure = Some(err);
597 shared_state.try_claim_failure(worker_id);
598 break 'stop;
599 }
600 TestCaseError::Reject(_) => {
601 let max = self.config.max_test_rejects;
602
603 let total = shared_state.increment_rejects();
604
605 if !self.config.corpus.collect_edge_coverage()
608 && let Some(progress) = progress
609 {
610 progress.set_message(format!("([{total}] rejected)"));
611 }
612
613 if max > 0 && total > max {
614 worker.failure =
615 Some(TestCaseError::reject(FuzzError::TooManyRejects(max)));
616 shared_state.try_claim_failure(worker_id);
617 break 'stop;
618 }
619 }
620 },
621 }
622 }
623
624 if worker_id == 0 {
625 worker.failed_corpus_replays = corpus.failed_replays;
626 }
627
628 trace!("worker {worker_id} fuzz stats");
630 shared_state.state.log_stats();
631
632 Ok(worker)
633 }
634
635 fn runs_per_worker(&self, worker_id: usize) -> u32 {
637 let worker_id = worker_id as u32;
638 let total_runs = self.config.runs;
639 let n = self.num_workers as u32;
640 let runs = total_runs / n;
641 let remainder = total_runs % n;
642 if worker_id < remainder { runs + 1 } else { runs }
645 }
646}