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