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