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