1use crate::executors::{
2 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
3 campaign::{CampaignCallKind, CampaignControl, CampaignEvent, FuzzCampaign, FuzzCampaignMode},
4 corpus::{
5 CorpusSyncCoordinator, GlobalCorpusMetrics, ReplayTarget, StatelessReplayTarget,
6 WorkerCorpus,
7 },
8 should_ignore_revert, worker_seed,
9};
10use alloy_dyn_abi::JsonAbiExt;
11use alloy_json_abi::Function;
12use alloy_primitives::{
13 Address, Bytes, Log, U256,
14 map::{AddressHashMap, HashMap},
15};
16use eyre::Result;
17use foundry_common::{sh_println, sh_warn};
18use foundry_config::FuzzConfig;
19use foundry_evm_core::{
20 Breakpoints,
21 constants::MAGIC_ASSUME,
22 decode::{RevertDecoder, SkipReason},
23 evm::FoundryEvmNetwork,
24};
25use foundry_evm_coverage::HitMaps;
26use foundry_evm_fuzz::{
27 BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
28 FuzzFixtures, FuzzRunMetadata, FuzzTestResult,
29 strategies::{EvmFuzzState, TxGenerator},
30};
31use foundry_evm_traces::SparsedTraceArena;
32use indicatif::ProgressBar;
33use proptest::test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner};
34use rayon::iter::{IntoParallelIterator, ParallelIterator};
35use serde_json::json;
36use std::{
37 path::PathBuf,
38 sync::{
39 Arc, OnceLock,
40 atomic::{AtomicBool, AtomicU32, Ordering},
41 },
42 time::{Instant, SystemTime, UNIX_EPOCH},
43};
44
45mod frontier;
46mod types;
47use frontier::FuzzBranchFrontierArtifact;
48pub(super) use frontier::{
49 FuzzBranchFrontier, FuzzFrontierRecorder, StatefulFuzzBranchFrontierArtifact, merge_frontiers,
50 write_frontier_artifact,
51};
52pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome};
53
54const SYNC_INTERVAL: u32 = 1000;
56
57const MIN_RUNS_PER_WORKER: u32 = 64;
60
61struct WorkerState<FEN: FoundryEvmNetwork> {
62 id: usize,
64 first_case: Option<(u32, FuzzCase)>,
66 gas_by_case: Vec<(u64, u64)>,
68 counterexample: Option<(BasicTxDetails, RawCallResult<FEN>)>,
70 traces: Vec<SparsedTraceArena>,
74 debug_bytecodes: AddressHashMap<Bytes>,
76 breakpoints: Option<Breakpoints>,
78 last_case: Option<(BasicTxDetails, Option<FuzzRunMetadata>)>,
80 coverage: Option<HitMaps>,
82 logs: Vec<Log>,
84 deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
86 runs: u32,
88 failure: Option<TestCaseError>,
90 failure_run: Option<FuzzRunMetadata>,
92 last_run_timestamp: u128,
96 failed_corpus_replays: usize,
98 frontiers: Vec<FuzzBranchFrontier>,
100}
101
102impl<FEN: FoundryEvmNetwork> WorkerState<FEN> {
103 fn new(worker_id: usize) -> Self {
104 Self {
105 id: worker_id,
106 first_case: None,
107 gas_by_case: Vec::new(),
108 counterexample: None,
109 traces: Vec::new(),
110 debug_bytecodes: HashMap::default(),
111 breakpoints: None,
112 last_case: None,
113 coverage: None,
114 logs: Vec::new(),
115 deprecated_cheatcodes: HashMap::default(),
116 runs: 0,
117 failure: None,
118 failure_run: None,
119 last_run_timestamp: 0,
120 failed_corpus_replays: 0,
121 frontiers: Vec::new(),
122 }
123 }
124}
125
126struct SharedFuzzState {
128 state: EvmFuzzState,
129 total_runs: Arc<AtomicU32>,
131 failed_worker_id: OnceLock<usize>,
137 total_rejects: Arc<AtomicU32>,
139 timer: FuzzTestTimer,
141 timed_out: AtomicBool,
143 global_corpus_metrics: GlobalCorpusMetrics,
145
146 global_early_exit: EarlyExit,
148 local_early_exit: EarlyExit,
150}
151
152impl SharedFuzzState {
153 fn new(state: EvmFuzzState, timeout: Option<u32>, early_exit: EarlyExit) -> Self {
154 Self {
155 state,
156 total_runs: Arc::new(AtomicU32::new(0)),
157 failed_worker_id: OnceLock::new(),
158 total_rejects: Arc::new(AtomicU32::new(0)),
159 timer: FuzzTestTimer::new(timeout),
160 timed_out: AtomicBool::new(false),
161 global_corpus_metrics: GlobalCorpusMetrics::default(),
162 global_early_exit: early_exit,
163 local_early_exit: EarlyExit::new(true),
164 }
165 }
166
167 fn increment_runs(&self) -> u32 {
169 self.total_runs.fetch_add(1, Ordering::Relaxed) + 1
170 }
171
172 fn increment_rejects(&self) -> u32 {
174 self.total_rejects.fetch_add(1, Ordering::Relaxed) + 1
175 }
176
177 fn should_continue(&self) -> bool {
179 if self.global_early_exit.should_stop() || self.local_early_exit.should_stop() {
180 return false;
181 }
182 if self.timer.is_timed_out() {
183 self.timed_out.store(true, Ordering::Relaxed);
184 return false;
185 }
186 true
187 }
188
189 fn interrupted(&self, planned_runs: u32) -> bool {
192 self.global_early_exit.should_stop()
193 && !self.timed_out.load(Ordering::Relaxed)
194 && self.total_runs.load(Ordering::Relaxed) < planned_runs
195 }
196
197 fn try_claim_failure(&self, worker_id: usize) -> bool {
200 let mut claimed = false;
201 let _ = self.failed_worker_id.get_or_init(|| {
202 claimed = true;
203 self.local_early_exit.record_failure();
204 worker_id
205 });
206 claimed
207 }
208}
209
210pub struct FuzzedExecutor<FEN: FoundryEvmNetwork> {
216 executor_f: Executor<FEN>,
218 runner: TestRunner,
220 sender: Address,
222 config: FuzzConfig,
224 persisted_failure: Option<BaseCounterExample>,
226 corpus_replay_dir: Option<PathBuf>,
228 num_workers: usize,
230}
231
232impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
233 pub fn new(
235 executor: Executor<FEN>,
236 runner: TestRunner,
237 sender: Address,
238 config: FuzzConfig,
239 persisted_failure: Option<BaseCounterExample>,
240 corpus_replay_dir: Option<PathBuf>,
241 ) -> Self {
242 let run_limit = if config.run.is_some() { 1 } else { config.runs };
243 let max_workers = if run_limit == 0 {
244 0
245 } else if config.run.is_some() {
246 1
247 } else {
248 Ord::max(1, run_limit / MIN_RUNS_PER_WORKER)
249 };
250 let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize);
251 Self {
252 executor_f: executor,
253 runner,
254 sender,
255 config,
256 persisted_failure,
257 corpus_replay_dir,
258 num_workers,
259 }
260 }
261
262 #[allow(clippy::too_many_arguments)]
266 pub fn fuzz(
267 &mut self,
268 func: &Function,
269 fuzz_fixtures: &FuzzFixtures,
270 state: EvmFuzzState,
271 address: Address,
272 rd: &RevertDecoder,
273 progress: Option<&ProgressBar>,
274 early_exit: &EarlyExit,
275 tokio_handle: &tokio::runtime::Handle,
276 ) -> Result<FuzzTestResult> {
277 let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone());
278 let recorder = self.executor_f.inspector_mut().debug_recorder.take();
281
282 let worker_ids = self.worker_ids();
283 debug!(n = worker_ids.len(), "spawning workers");
284 let final_sync = (worker_ids.len() > 1 && self.config.corpus.corpus_dir.is_some())
285 .then(|| Arc::new(CorpusSyncCoordinator::new(worker_ids.len())));
286 let workers = worker_ids
287 .into_par_iter()
288 .map(|worker_id| {
289 let _guard = tokio_handle.enter();
290 let _guard = info_span!("fuzz_worker", id = worker_id).entered();
291 let timer = Instant::now();
292 let r = self.run_worker(
293 worker_id,
294 func,
295 fuzz_fixtures,
296 address,
297 rd,
298 &shared_state,
299 progress,
300 final_sync.as_deref(),
301 );
302 if r.is_err()
303 && let Some(final_sync) = &final_sync
304 {
305 final_sync.abort();
306 }
307 debug!("finished in {:?}", timer.elapsed());
308 r
309 })
310 .collect::<Result<Vec<_>>>();
311 self.executor_f.inspector_mut().debug_recorder = recorder;
312
313 let (mut result, shown_case) = self.aggregate_results(workers?, func, &shared_state);
314 if self.executor_f.inspector().debug_recorder.is_some()
315 && let Some(case) = shown_case
316 {
317 self.record_case(&mut result, address, case)?;
318 }
319 Ok(result)
320 }
321
322 fn record_case(
325 &mut self,
326 result: &mut FuzzTestResult,
327 address: Address,
328 (mut tx, run): (BasicTxDetails, Option<FuzzRunMetadata>),
329 ) -> Result<()> {
330 if let Some(cheats) = self.executor_f.inspector_mut().cheatcodes.as_mut()
331 && let Some(run) = run
332 && let Some(seed) = run.seed.or(self.config.seed)
333 {
334 let worker = run.worker.unwrap_or(0) as usize;
335 cheats.core.set_seed(Self::fuzz_run_seed(seed, worker, run.run.unwrap_or(1)));
336 }
337 tx.sender = self.sender;
338 tx.call_details.target = address;
339 tx.warp = None;
340 tx.roll = None;
341 self.resolve_stateless_tx_with_executor(&self.executor_f, &mut tx)?;
342 let mut call = self.executor_f.call_raw(
343 tx.sender,
344 tx.call_details.target,
345 tx.call_details.calldata,
346 tx.call_details.value.unwrap_or_default(),
347 )?;
348 let success = should_ignore_revert(
350 self.config.fail_on_revert,
351 address,
352 call.reverter,
353 self.executor_f.inspector().extra_cheatcode_addresses(),
354 ) || self.executor_f.is_raw_call_mut_success(address, &mut call);
355 if success != result.success {
356 let _ = sh_warn!(
357 "the debugged fuzz case {} when run again, unlike in the campaign, so the debugger \
358 may show a different path",
359 if success { "passed" } else { "failed" }
360 );
361 }
362 result.traces = call.traces;
363 result.debug_bytecodes = call.debug_bytecodes;
364 result.breakpoints = call.cheatcodes.map(|cheats| cheats.breakpoints.clone());
365 Ok(())
366 }
367
368 pub fn replay_persisted_failure(
373 &mut self,
374 func: &Function,
375 address: Address,
376 rd: &RevertDecoder,
377 ) -> Result<FuzzTestResult> {
378 let Some(failure) = self.persisted_failure.as_ref() else {
379 return Ok(FuzzTestResult {
380 skipped: true,
381 reason: Some("no persisted fuzz failure to replay".to_string()),
382 ..Default::default()
383 });
384 };
385
386 let seed = failure.fuzz.seed.or(self.config.seed);
387 if let Some(cheats) = self.executor_f.inspector_mut().cheatcodes.as_mut()
388 && let Some(seed) = seed
389 {
390 let run = failure.fuzz.run.unwrap_or(1);
391 let worker = failure.fuzz.worker.unwrap_or(0) as usize;
392 cheats.core.set_seed(Self::fuzz_run_seed(seed, worker, run));
393 }
394
395 let mut tx = BasicTxDetails {
396 warp: None,
397 roll: None,
398 sender: self.sender,
399 call_details: CallDetails {
400 target: address,
401 calldata: failure.calldata.clone(),
402 value: failure.value,
403 },
404 };
405 self.resolve_stateless_tx_with_executor(&self.executor_f, &mut tx)?;
406 let mut call = self.executor_f.call_raw(
407 tx.sender,
408 tx.call_details.target,
409 tx.call_details.calldata.clone(),
410 tx.call_details.value.unwrap_or_default(),
411 )?;
412 if call.result.as_ref() == MAGIC_ASSUME {
413 return Ok(FuzzTestResult {
414 skipped: true,
415 reason: Some("persisted fuzz failure rejected by `vm.assume`".to_string()),
416 ..Default::default()
417 });
418 }
419 if let Some(reason) = call.skip_reason() {
420 return Ok(FuzzTestResult { skipped: true, reason: reason.0, ..Default::default() });
421 }
422
423 let (breakpoints, deprecated_cheatcodes) =
424 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
425 (cheats.breakpoints.clone(), cheats.deprecated.clone())
426 });
427 let success = should_ignore_revert(
428 self.config.fail_on_revert,
429 address,
430 call.reverter,
431 self.executor_f.inspector().extra_cheatcode_addresses(),
432 ) || self.executor_f.is_raw_call_mut_success(address, &mut call);
433
434 let mut result = FuzzTestResult {
435 success,
436 labels: call.labels.clone(),
437 traces: call.traces.clone(),
438 debug_bytecodes: call.debug_bytecodes.clone(),
439 breakpoints: Some(breakpoints),
440 deprecated_cheatcodes,
441 fork_block_number: call.fork_block_number,
442 ..Default::default()
443 };
444
445 if success {
446 result.first_case = FuzzCase { gas: call.gas_used, stipend: call.stipend };
447 result.gas_by_case.push((call.gas_used, call.stipend));
448 result.line_coverage = call.line_coverage;
449 result.logs = call.logs;
450 result.gas_report_traces.extend(call.traces.into_iter().map(|trace| trace.arena));
451 } else {
452 let reason = call
453 .skip_reason()
454 .map(|reason| reason.to_string())
455 .or_else(|| rd.maybe_decode(&call.result, call.exit_reason));
456 result.reason = reason;
457 let args = tx
458 .call_details
459 .calldata
460 .get(4..)
461 .map_or_else(Vec::new, |data| func.abi_decode_input(data).unwrap_or_default());
462 result.counterexample = Some(CounterExample::Single(
463 BaseCounterExample::from_fuzz_tx(&tx, args, call.traces).with_fuzz_metadata(
464 FuzzRunMetadata::new(
465 seed,
466 failure.fuzz.run,
467 Some(failure.fuzz.worker.unwrap_or(0)),
468 ),
469 ),
470 ));
471 result.logs = call.logs;
472 }
473
474 Ok(result)
475 }
476
477 fn single_fuzz(
480 &self,
481 executor: &mut Executor<FEN>,
482 address: Address,
483 mut tx: BasicTxDetails,
484 coverage_metrics: &mut WorkerCorpus,
485 frontier_recorder: &mut FuzzFrontierRecorder,
486 fuzz_run: Option<&FuzzRunMetadata>,
487 ) -> Result<FuzzOutcome<FEN>, TestCaseError> {
488 tx.sender = self.sender;
489 tx.call_details.target = address;
490 tx.warp = None;
491 tx.roll = None;
492 self.resolve_stateless_tx_with_executor(executor, &mut tx)
493 .map_err(|e| TestCaseError::fail(e.to_string()))?;
494 let campaign = FuzzCampaign::new(FuzzCampaignMode::Stateless);
495 let mut state = (executor, tx);
496 let mut cmp_values = Vec::new();
497 let mut checked = None;
498 campaign
499 .run_sequence(
500 &mut state,
501 1,
502 |state| (state.0, &mut state.1),
503 |_| 0,
504 |_| false,
505 |_, event| {
506 match event {
507 CampaignEvent::Feedback(call) => {
508 cmp_values = call.evm_cmp_values.take().unwrap_or_default();
509 }
510 CampaignEvent::Check { result, kind, .. } => {
511 checked = Some((
512 result.take().expect("campaign check result is available"),
513 kind,
514 ));
515 return Ok(CampaignControl::Stop);
516 }
517 CampaignEvent::Advance | CampaignEvent::Next { .. } => {
518 unreachable!("depth-one campaigns cannot advance")
519 }
520 CampaignEvent::PostCheck => {}
521 }
522 Ok(CampaignControl::Continue)
523 },
524 )
525 .map_err(|e| TestCaseError::fail(e.to_string()))?;
526 let (mut call, kind) = checked.expect("depth-one campaign emits a check event");
527 let tx = state.1.clone();
528
529 if kind == CampaignCallKind::AssumptionRejected {
531 coverage_metrics.process_inputs(&[], &[], false, None);
533 return Err(TestCaseError::reject(FuzzError::AssumeReject));
534 }
535
536 if call.skip_reason().is_some() {
537 coverage_metrics.process_inputs(&[], &[], false, None);
539 } else {
540 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
541 let frontier_new_coverage =
544 self.config.corpus.collect_edge_coverage().then_some(new_coverage);
545 frontier_recorder.capture_call(
546 fuzz_run,
547 std::slice::from_ref(&tx),
548 0,
549 &cmp_values,
550 frontier_new_coverage,
551 );
552 coverage_metrics.process_inputs(
553 std::slice::from_ref(&tx),
554 &[cmp_values],
555 new_coverage,
556 None,
557 );
558 }
559
560 let (breakpoints, deprecated_cheatcodes) =
561 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
562 (cheats.breakpoints.clone(), cheats.deprecated.clone())
563 });
564
565 let success = should_ignore_revert(
568 self.config.fail_on_revert,
569 address,
570 call.reverter,
571 state.0.inspector().extra_cheatcode_addresses(),
572 ) || state.0.is_raw_call_mut_success(address, &mut call);
573
574 if success {
575 Ok(FuzzOutcome::Case(CaseOutcome {
576 case: FuzzCase { gas: call.gas_used, stipend: call.stipend },
577 traces: call.traces,
578 debug_bytecodes: call.debug_bytecodes,
579 coverage: call.line_coverage,
580 breakpoints,
581 logs: call.logs,
582 deprecated_cheatcodes,
583 }))
584 } else {
585 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
586 exit_reason: call.exit_reason,
587 counterexample: (tx, call),
588 breakpoints,
589 }))
590 }
591 }
592
593 fn resolve_stateless_tx_with_executor(
594 &self,
595 executor: &Executor<FEN>,
596 tx: &mut BasicTxDetails,
597 ) -> Result<()> {
598 tx.call_details.value = match tx.call_details.value {
599 Some(requested) if !requested.is_zero() => {
600 let value = requested.min(executor.get_balance(tx.sender)?);
601 (!value.is_zero()).then_some(value)
602 }
603 _ => None,
604 };
605 Ok(())
606 }
607
608 fn aggregate_results(
611 &self,
612 mut workers: Vec<WorkerState<FEN>>,
613 func: &Function,
614 shared_state: &SharedFuzzState,
615 ) -> (FuzzTestResult, Option<(BasicTxDetails, Option<FuzzRunMetadata>)>) {
616 self.write_branch_frontiers(&mut workers, func);
617
618 let mut result = FuzzTestResult::default();
619 if workers.is_empty() {
620 result.success = true;
621 return (result, None);
622 }
623
624 let mut first_case_candidate = None;
626 let mut last_run_worker = None;
627 for (i, worker) in workers.iter().enumerate() {
628 if let Some((run, ref case)) = worker.first_case
629 && first_case_candidate.as_ref().is_none_or(|&(r, _)| run < r)
630 {
631 first_case_candidate = Some((run, case.clone()));
632 }
633
634 if last_run_worker.is_none_or(|(t, _)| worker.last_run_timestamp > t) {
635 last_run_worker = Some((worker.last_run_timestamp, i));
636 }
637
638 if worker.id == 0 {
640 result.failed_corpus_replays = worker.failed_corpus_replays;
641 }
642 }
643 result.first_case = first_case_candidate.map(|(_, case)| case).unwrap_or_default();
644 let (_, last_run_worker_idx) = last_run_worker.expect("at least one worker");
645 let mut output_worker_idx = last_run_worker_idx;
646 let shown_case;
647
648 if let Some(&failed_worker_id) = shared_state.failed_worker_id.get() {
649 result.success = false;
650
651 let failed_worker_idx = workers.iter().position(|w| w.id == failed_worker_id).unwrap();
652 output_worker_idx = failed_worker_idx;
653 let failed_worker = &mut workers[failed_worker_idx];
654
655 let counterexample = failed_worker.counterexample.take();
656 shown_case =
657 counterexample.as_ref().map(|(tx, _)| (tx.clone(), failed_worker.failure_run));
658 if let Some((_, call)) = &counterexample {
659 result.labels.clone_from(&call.labels);
660 result.traces.clone_from(&call.traces);
661 result.debug_bytecodes.clone_from(&call.debug_bytecodes);
662 result.breakpoints = call.cheatcodes.as_ref().map(|c| c.breakpoints.clone());
663 result.fork_block_number = call.fork_block_number;
664 }
665
666 match &failed_worker.failure {
667 Some(TestCaseError::Fail(reason)) => {
668 let reason = reason.to_string();
669 result.reason = (!reason.is_empty()).then_some(reason);
670 if let Some((tx, call)) = counterexample {
671 let args =
672 tx.call_details.calldata.get(4..).map_or_else(Vec::new, |data| {
673 func.abi_decode_input(data).unwrap_or_default()
674 });
675 let fuzz = failed_worker.failure_run.unwrap_or_default();
676 result.counterexample = Some(CounterExample::Single(
677 BaseCounterExample::from_fuzz_tx(&tx, args, call.traces)
678 .with_fuzz_metadata(FuzzRunMetadata::new(
679 fuzz.seed.or(self.config.seed),
680 fuzz.run,
681 fuzz.worker,
682 )),
683 ));
684 }
685 }
686 Some(TestCaseError::Reject(reason)) => {
687 let reason = reason.to_string();
688 result.reason = (!reason.is_empty()).then_some(reason);
689 }
690 None => {}
691 }
692 } else {
693 let last_run_worker = &workers[last_run_worker_idx];
694 result.success = true;
695 let planned_runs = if self.config.run.is_some() { 1 } else { self.config.runs };
696 if shared_state.interrupted(planned_runs) {
697 result.skipped = true;
698 result.reason = Some("interrupted".to_string());
699 }
700 result.traces = last_run_worker.traces.last().cloned();
701 result.debug_bytecodes.clone_from(&last_run_worker.debug_bytecodes);
702 result.breakpoints = last_run_worker.breakpoints.clone();
703 shown_case = last_run_worker.last_case.clone();
704 }
705
706 if !self.config.show_logs {
707 result.logs = workers[output_worker_idx].logs.clone();
708 }
709
710 for mut worker in workers {
711 result.gas_by_case.append(&mut worker.gas_by_case);
712 if self.config.show_logs {
713 result.logs.append(&mut worker.logs);
714 }
715 result.gas_report_traces.extend(worker.traces.into_iter().map(|t| t.arena));
716 HitMaps::merge_opt(&mut result.line_coverage, worker.coverage);
717 result.deprecated_cheatcodes.extend(worker.deprecated_cheatcodes);
718 }
719
720 if let Some(reason) = &result.reason
721 && let Some(reason) = SkipReason::decode_self(reason)
722 {
723 result.skipped = true;
724 result.reason = reason.0;
725 }
726
727 (result, shown_case)
728 }
729
730 fn write_branch_frontiers(&self, workers: &mut [WorkerState<FEN>], func: &Function) {
731 let Some(frontier_dir) = &self.config.corpus.frontier_dir else {
732 return;
733 };
734 let limit = self.config.corpus.frontier_limit;
735 if limit == 0 {
736 return;
737 }
738
739 let frontiers = frontier::merge_frontiers(
740 limit,
741 workers.iter_mut().flat_map(|worker| worker.frontiers.drain(..)),
742 );
743 if frontiers.is_empty() {
744 return;
745 }
746
747 let artifact = FuzzBranchFrontierArtifact::new(func, limit, frontiers);
748 if let Err(err) = frontier::write_frontier_artifact(frontier_dir, &artifact) {
749 warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
750 }
751 }
752
753 #[allow(clippy::too_many_arguments)]
755 fn run_worker(
756 &self,
757 worker_id: usize,
758 func: &Function,
759 fuzz_fixtures: &FuzzFixtures,
760 address: Address,
761 rd: &RevertDecoder,
762 shared_state: &SharedFuzzState,
763 progress: Option<&ProgressBar>,
764 final_sync: Option<&CorpusSyncCoordinator>,
765 ) -> Result<WorkerState<FEN>> {
766 let fuzz_seed = shared_state.state.fork();
768 let generator = TxGenerator::stateless(
769 fuzz_seed.clone(),
770 fuzz_fixtures.clone(),
771 address,
772 self.sender,
773 func.clone(),
774 self.config.dictionary.dictionary_weight,
775 self.config.corpus.payable_value_weight,
776 );
777 let fuzz_state = fuzz_seed.stateless_worker();
778 let generator = foundry_evm_fuzz::sequence::SequenceGenerator::stateless_with_fixtures(
779 generator,
780 fuzz_state.clone(),
781 fuzz_fixtures.clone(),
782 func.clone(),
783 &self.config.corpus,
784 )?;
785
786 let replay_target = ReplayTarget {
787 stateless: Some(StatelessReplayTarget { function: func, address }),
788 fuzzed_contracts: None,
789 dynamic: None,
790 senders: None,
791 };
792 let mut corpus = WorkerCorpus::new(
793 worker_id,
794 self.config.corpus.clone(),
795 generator,
796 self.corpus_replay_dir.as_deref(),
797 (worker_id == 0).then_some(&self.executor_f),
799 replay_target,
800 )?;
801 let mut executor = self.executor_f.clone();
802 let frontier_limit = if self.config.corpus.capture_branch_frontiers() {
803 self.config.corpus.frontier_limit
804 } else {
805 0
806 };
807 let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
808
809 let mut worker = WorkerState::new(worker_id);
810 let max_traces_to_collect =
812 std::cmp::max(1, self.config.gas_report_samples / self.num_workers as u32);
813
814 let worker_runs = self.runs_per_worker(worker_id);
815 debug!(worker_runs);
816
817 let mut runner_config = self.runner.config().clone();
818 runner_config.cases = worker_runs;
819
820 let mut runner = if let Some(seed) = self.config.seed {
821 let worker_seed = worker_seed(seed, worker_id as u32);
822 trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}");
823 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
824 TestRunner::new_with_rng(runner_config, rng)
825 } else {
826 TestRunner::new(runner_config)
827 };
828
829 if let Some(target_run) = self.config.run {
830 for _ in 1..target_run {
831 if let Err(err) = corpus.new_sequence(&mut runner) {
832 worker.failure = Some(TestCaseError::fail(format!(
833 "failed to generate fuzzed input in worker {}: {err}",
834 worker.id
835 )));
836 shared_state.try_claim_failure(worker_id);
837 return Ok(worker);
838 }
839 }
840 }
841 let mut generated_inputs = 0;
842
843 let mut persisted_failure =
844 self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none());
845
846 let sync_offset = (worker_id as u32).saturating_mul(100);
849 let sync_threshold = SYNC_INTERVAL + sync_offset;
850 let mut runs_since_sync = sync_threshold; let mut last_metrics_report = Instant::now();
852 'stop: while shared_state.should_continue() && worker.runs < worker_runs {
856 let (input, fuzz_run, is_persisted_replay) = if worker_id == 0
858 && let Some(failure) = persisted_failure.take()
859 && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector)
860 {
861 let seed = failure.fuzz.seed.or(self.config.seed);
862 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
863 && let Some(seed) = seed
864 {
865 let run = failure.fuzz.run.unwrap_or(1);
866 let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize;
867 cheats.core.set_seed(Self::fuzz_run_seed(seed, worker, run));
868 }
869
870 (
871 BasicTxDetails {
872 warp: None,
873 roll: None,
874 sender: self.sender,
875 call_details: CallDetails {
876 target: address,
877 calldata: failure.calldata.clone(),
878 value: failure.value,
879 },
880 },
881 Some(FuzzRunMetadata::new(
882 seed,
883 failure.fuzz.run,
884 Some(failure.fuzz.worker.unwrap_or(worker_id as u32)),
885 )),
886 true,
887 )
888 } else {
889 runs_since_sync += 1;
890 if runs_since_sync >= sync_threshold {
891 let timer = Instant::now();
892 corpus.sync(
893 self.num_workers,
894 &executor,
895 replay_target,
896 &shared_state.global_corpus_metrics,
897 )?;
898 trace!("finished corpus sync in {:?}", timer.elapsed());
899 runs_since_sync = 0;
900 }
901
902 let fuzz_run = self.config.run.unwrap_or_else(|| {
903 generated_inputs += 1;
904 generated_inputs
905 });
906 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
907 && let Some(seed) = self.config.seed
908 {
909 cheats.core.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run));
910 }
911
912 let input = match corpus.new_sequence(&mut runner) {
913 Ok(plan) => plan.into_first(),
914 Err(err) => {
915 worker.failure = Some(TestCaseError::fail(format!(
916 "failed to generate fuzzed input in worker {}: {err}",
917 worker.id
918 )));
919 shared_state.try_claim_failure(worker_id);
920 break 'stop;
921 }
922 };
923
924 (
925 input,
926 Some(FuzzRunMetadata::new(
927 self.config.seed,
928 Some(fuzz_run),
929 Some(worker_id as u32),
930 )),
931 false,
932 )
933 };
934
935 let mut inc_runs = || {
936 let total_runs = shared_state.increment_runs();
937 debug_assert!(
938 shared_state.timer.is_enabled()
939 || total_runs
940 <= if self.config.run.is_some() { 1 } else { self.config.runs },
941 "worker runs were not distributed correctly"
942 );
943 worker.runs += 1;
944 if let Some(progress) = progress {
945 progress.inc(1);
946 }
947 total_runs
948 };
949
950 worker.last_run_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
951 let case_input = input.clone();
952 match self.single_fuzz(
953 &mut executor,
954 address,
955 input,
956 &mut corpus,
957 &mut frontier_recorder,
958 fuzz_run.as_ref(),
959 ) {
960 Ok(fuzz_outcome) => match fuzz_outcome {
961 FuzzOutcome::Case(case) => {
962 if is_persisted_replay {
963 continue 'stop;
964 }
965 let total_runs = inc_runs();
966
967 if worker_id == 0 && self.config.corpus.collect_edge_coverage() {
968 if let Some(progress) = progress {
969 corpus.sync_metrics(&shared_state.global_corpus_metrics);
970 progress
971 .set_message(format!("{}", shared_state.global_corpus_metrics));
972 } else if last_metrics_report.elapsed()
973 > DURATION_BETWEEN_METRICS_REPORT
974 {
975 corpus.sync_metrics(&shared_state.global_corpus_metrics);
976 let metrics = json!({
978 "timestamp": SystemTime::now()
979 .duration_since(UNIX_EPOCH)?
980 .as_secs(),
981 "test": func.name,
982 "metrics": shared_state.global_corpus_metrics.load(),
983 });
984 let _ = sh_println!("{metrics}");
985 last_metrics_report = Instant::now();
986 }
987 }
988
989 worker.gas_by_case.push((case.case.gas, case.case.stipend));
990
991 if worker.first_case.is_none() {
992 worker.first_case = Some((total_runs, case.case));
993 }
994
995 if let Some(call_traces) = case.traces {
996 if worker.traces.len() == max_traces_to_collect as usize {
997 worker.traces.pop();
998 }
999 worker.traces.push(call_traces);
1000 worker.debug_bytecodes = case.debug_bytecodes;
1001 worker.breakpoints = Some(case.breakpoints);
1002 worker.last_case = Some((case_input, fuzz_run));
1003 }
1004
1005 if self.config.show_logs {
1009 worker.logs.extend(case.logs);
1010 } else {
1011 worker.logs = case.logs;
1012 }
1013
1014 HitMaps::merge_opt(&mut worker.coverage, case.coverage);
1015 worker.deprecated_cheatcodes = case.deprecated_cheatcodes;
1016 }
1017 FuzzOutcome::CounterExample(CounterExampleOutcome {
1018 exit_reason: status,
1019 counterexample: outcome,
1020 ..
1021 }) => {
1022 if !is_persisted_replay {
1023 inc_runs();
1024 }
1025 worker.failure_run = fuzz_run;
1026
1027 let reason = outcome
1029 .1
1030 .skip_reason()
1031 .map(|reason| reason.to_string())
1032 .or_else(|| rd.maybe_decode(&outcome.1.result, status));
1033 if self.config.show_logs {
1034 worker.logs.extend(outcome.1.logs.clone());
1035 } else {
1036 worker.logs.clone_from(&outcome.1.logs);
1037 }
1038 worker.counterexample = Some(outcome);
1039 worker.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
1040 shared_state.try_claim_failure(worker_id);
1041 break 'stop;
1042 }
1043 },
1044 Err(err) => match err {
1045 TestCaseError::Fail(_) => {
1046 worker.failure = Some(err);
1047 shared_state.try_claim_failure(worker_id);
1048 break 'stop;
1049 }
1050 TestCaseError::Reject(_) => {
1051 let max = self.config.max_test_rejects;
1052
1053 let total = shared_state.increment_rejects();
1054
1055 if !self.config.corpus.collect_edge_coverage()
1058 && let Some(progress) = progress
1059 {
1060 progress.set_message(format!("([{total}] rejected)"));
1061 }
1062
1063 if max > 0 && total > max {
1064 worker.failure =
1065 Some(TestCaseError::reject(FuzzError::TooManyRejects(max)));
1066 shared_state.try_claim_failure(worker_id);
1067 break 'stop;
1068 }
1069 }
1070 },
1071 }
1072 }
1073
1074 if let Some(final_sync) = final_sync {
1075 corpus.finalize_sync(&executor, replay_target, final_sync)?;
1076 }
1077
1078 if worker_id == 0 {
1079 worker.failed_corpus_replays = corpus.failed_replays;
1080 }
1081 worker.frontiers = frontier_recorder.into_frontiers();
1082
1083 trace!("worker {worker_id} fuzz stats");
1085 fuzz_state.log_stats();
1086
1087 Ok(worker)
1088 }
1089
1090 const fn runs_per_worker(&self, worker_id: usize) -> u32 {
1092 let worker_id = worker_id as u32;
1093 let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs };
1094 let n = self.num_workers as u32;
1095 let runs = total_runs / n;
1096 let remainder = total_runs % n;
1097 if worker_id < remainder { runs + 1 } else { runs }
1100 }
1101
1102 fn worker_ids(&self) -> Vec<usize> {
1104 if self.config.run.is_some() {
1105 vec![self.config.worker.unwrap_or(0) as usize]
1106 } else {
1107 (0..self.num_workers).collect()
1108 }
1109 }
1110
1111 fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 {
1113 worker_seed(seed, worker_id as u32).wrapping_add(U256::from(run.saturating_sub(1)))
1114 }
1115}