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