1use crate::executors::{
2 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
3 corpus::{GlobalCorpusMetrics, ReplayTarget, StatelessReplayTarget, WorkerCorpus},
4};
5use alloy_dyn_abi::JsonAbiExt;
6use alloy_json_abi::Function;
7use alloy_primitives::{
8 Address, Bytes, Log, U256, keccak256,
9 map::{AddressHashMap, HashMap},
10};
11use eyre::Result;
12use foundry_common::sh_println;
13use foundry_config::FuzzConfig;
14use foundry_evm_core::{
15 Breakpoints,
16 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
17 decode::{RevertDecoder, SkipReason},
18 evm::FoundryEvmNetwork,
19};
20use foundry_evm_coverage::HitMaps;
21use foundry_evm_fuzz::{
22 BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError,
23 FuzzFixtures, FuzzRunMetadata, FuzzTestResult,
24 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state, fuzz_msg_value},
25};
26use foundry_evm_traces::SparsedTraceArena;
27use indicatif::ProgressBar;
28use proptest::{
29 strategy::{Just, Strategy},
30 test_runner::{RngAlgorithm, TestCaseError, TestRng, TestRunner},
31};
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: (Bytes, 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: (Bytes::new(), RawCallResult::default()),
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 workers = worker_ids
247 .into_par_iter()
248 .map(|worker_id| {
249 let _guard = tokio_handle.enter();
250 let _guard = info_span!("fuzz_worker", id = worker_id).entered();
251 let timer = Instant::now();
252 let r = self.run_worker(
253 worker_id,
254 func,
255 fuzz_fixtures,
256 address,
257 rd,
258 &shared_state,
259 progress,
260 );
261 debug!("finished in {:?}", timer.elapsed());
262 r
263 })
264 .collect::<Result<Vec<_>>>()?;
265
266 Ok(self.aggregate_results(workers, func, &shared_state))
267 }
268
269 pub fn replay_persisted_failure(
274 &mut self,
275 func: &Function,
276 address: Address,
277 rd: &RevertDecoder,
278 ) -> Result<FuzzTestResult> {
279 let Some(failure) = self.persisted_failure.as_ref() else {
280 return Ok(FuzzTestResult {
281 skipped: true,
282 reason: Some("no persisted fuzz failure to replay".to_string()),
283 ..Default::default()
284 });
285 };
286
287 let seed = failure.fuzz.seed.or(self.config.seed);
288 if let Some(cheats) = self.executor_f.inspector_mut().cheatcodes.as_mut()
289 && let Some(seed) = seed
290 {
291 let run = failure.fuzz.run.unwrap_or(1);
292 let worker = failure.fuzz.worker.unwrap_or(0) as usize;
293 cheats.set_seed(Self::fuzz_run_seed(seed, worker, run));
294 }
295
296 let calldata = failure.calldata.clone();
297 let mut call =
298 self.executor_f.call_raw(self.sender, address, calldata.clone(), U256::ZERO)?;
299 if call.result.as_ref() == MAGIC_ASSUME {
300 return Ok(FuzzTestResult {
301 skipped: true,
302 reason: Some("persisted fuzz failure rejected by `vm.assume`".to_string()),
303 ..Default::default()
304 });
305 }
306 if call.reverter == Some(CHEATCODE_ADDRESS)
307 && let Some(reason) = SkipReason::decode(&call.result)
308 {
309 return Ok(FuzzTestResult { skipped: true, reason: reason.0, ..Default::default() });
310 }
311
312 let (breakpoints, deprecated_cheatcodes) =
313 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
314 (cheats.breakpoints.clone(), cheats.deprecated.clone())
315 });
316 let success = if !self.config.fail_on_revert
317 && call
318 .reverter
319 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
320 {
321 true
322 } else {
323 self.executor_f.is_raw_call_mut_success(address, &mut call, false)
324 };
325
326 let mut result = FuzzTestResult {
327 success,
328 labels: call.labels.clone(),
329 traces: call.traces.clone(),
330 debug_bytecodes: call.debug_bytecodes.clone(),
331 breakpoints: Some(breakpoints),
332 deprecated_cheatcodes,
333 ..Default::default()
334 };
335
336 if success {
337 result.first_case = FuzzCase { gas: call.gas_used, stipend: call.stipend };
338 result.gas_by_case.push((call.gas_used, call.stipend));
339 result.line_coverage = call.line_coverage;
340 result.logs = call.logs;
341 result.gas_report_traces.extend(call.traces.into_iter().map(|trace| trace.arena));
342 } else {
343 let reason = if call.reverter == Some(CHEATCODE_ADDRESS) {
344 SkipReason::decode(&call.result)
345 .map(|reason| reason.to_string())
346 .or_else(|| rd.maybe_decode(&call.result, call.exit_reason))
347 } else {
348 rd.maybe_decode(&call.result, call.exit_reason)
349 };
350 result.reason = reason;
351 let args = calldata
352 .get(4..)
353 .map_or_else(Vec::new, |data| func.abi_decode_input(data).unwrap_or_default());
354 result.counterexample = Some(CounterExample::Single(
355 BaseCounterExample::from_fuzz_call(calldata, args, call.traces).with_fuzz_metadata(
356 FuzzRunMetadata::new(
357 seed,
358 failure.fuzz.run,
359 Some(failure.fuzz.worker.unwrap_or(0)),
360 ),
361 ),
362 ));
363 result.logs = call.logs;
364 }
365
366 Ok(result)
367 }
368
369 fn single_fuzz(
372 &self,
373 executor: &Executor<FEN>,
374 address: Address,
375 calldata: Bytes,
376 coverage_metrics: &mut WorkerCorpus,
377 frontier_recorder: &mut FuzzFrontierRecorder,
378 fuzz_run: Option<&FuzzRunMetadata>,
379 ) -> Result<FuzzOutcome<FEN>, TestCaseError> {
380 let mut call = executor
381 .call_raw(self.sender, address, calldata.clone(), U256::ZERO)
382 .map_err(|e| TestCaseError::fail(e.to_string()))?;
383 let cmp_values = call.evm_cmp_values.take().unwrap_or_default();
384 let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
385 let frontier_new_coverage =
388 self.config.corpus.collect_edge_coverage().then_some(new_coverage);
389 frontier_recorder.capture_stateless_call(
390 fuzz_run,
391 self.sender,
392 address,
393 &calldata,
394 &cmp_values,
395 frontier_new_coverage,
396 );
397 coverage_metrics.process_inputs(
398 &[BasicTxDetails {
399 warp: None,
400 roll: None,
401 sender: self.sender,
402 call_details: CallDetails {
403 target: address,
404 calldata: calldata.clone(),
405 value: None,
406 },
407 }],
408 &[cmp_values],
409 new_coverage,
410 None,
411 );
412
413 if call.result.as_ref() == MAGIC_ASSUME {
415 return Err(TestCaseError::reject(FuzzError::AssumeReject));
416 }
417
418 let (breakpoints, deprecated_cheatcodes) =
419 call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| {
420 (cheats.breakpoints.clone(), cheats.deprecated.clone())
421 });
422
423 let success = if !self.config.fail_on_revert
426 && call
427 .reverter
428 .is_some_and(|reverter| reverter != address && reverter != CHEATCODE_ADDRESS)
429 {
430 true
431 } else {
432 executor.is_raw_call_mut_success(address, &mut call, false)
433 };
434
435 if success {
436 Ok(FuzzOutcome::Case(CaseOutcome {
437 case: FuzzCase { gas: call.gas_used, stipend: call.stipend },
438 traces: call.traces,
439 debug_bytecodes: call.debug_bytecodes,
440 coverage: call.line_coverage,
441 breakpoints,
442 logs: call.logs,
443 deprecated_cheatcodes,
444 }))
445 } else {
446 Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
447 exit_reason: call.exit_reason,
448 counterexample: (calldata, call),
449 breakpoints,
450 }))
451 }
452 }
453
454 fn aggregate_results(
456 &self,
457 mut workers: Vec<WorkerState<FEN>>,
458 func: &Function,
459 shared_state: &SharedFuzzState,
460 ) -> FuzzTestResult {
461 self.write_branch_frontiers(&mut workers, func);
462
463 let mut result = FuzzTestResult::default();
464 if workers.is_empty() {
465 result.success = true;
466 return result;
467 }
468
469 let mut first_case_candidate = None;
471 let mut last_run_worker = None;
472 for (i, worker) in workers.iter().enumerate() {
473 if let Some((run, ref case)) = worker.first_case
474 && first_case_candidate.as_ref().is_none_or(|&(r, _)| run < r)
475 {
476 first_case_candidate = Some((run, case.clone()));
477 }
478
479 if last_run_worker.is_none_or(|(t, _)| worker.last_run_timestamp > t) {
480 last_run_worker = Some((worker.last_run_timestamp, i));
481 }
482
483 if worker.id == 0 {
485 result.failed_corpus_replays = worker.failed_corpus_replays;
486 }
487 }
488 result.first_case = first_case_candidate.map(|(_, case)| case).unwrap_or_default();
489 let (_, last_run_worker_idx) = last_run_worker.expect("at least one worker");
490
491 if let Some(&failed_worker_id) = shared_state.failed_worker_id.get() {
492 result.success = false;
493
494 let failed_worker_idx = workers.iter().position(|w| w.id == failed_worker_id).unwrap();
495 let failed_worker = &mut workers[failed_worker_idx];
496
497 let (calldata, call) = std::mem::take(&mut failed_worker.counterexample);
498 result.labels = call.labels;
499 result.traces = call.traces.clone();
500 result.debug_bytecodes = call.debug_bytecodes.clone();
501 result.breakpoints = call.cheatcodes.map(|c| c.breakpoints);
502
503 match &failed_worker.failure {
504 Some(TestCaseError::Fail(reason)) => {
505 let reason = reason.to_string();
506 result.reason = (!reason.is_empty()).then_some(reason);
507 let args = if let Some(data) = calldata.get(4..) {
508 func.abi_decode_input(data).unwrap_or_default()
509 } else {
510 vec![]
511 };
512 let fuzz = failed_worker.failure_run.unwrap_or_default();
513 result.counterexample = Some(CounterExample::Single(
514 BaseCounterExample::from_fuzz_call(calldata, args, call.traces)
515 .with_fuzz_metadata(FuzzRunMetadata::new(
516 fuzz.seed.or(self.config.seed),
517 fuzz.run,
518 fuzz.worker,
519 )),
520 ));
521 }
522 Some(TestCaseError::Reject(reason)) => {
523 let reason = reason.to_string();
524 result.reason = (!reason.is_empty()).then_some(reason);
525 }
526 None => {}
527 }
528 } else {
529 let last_run_worker = &workers[last_run_worker_idx];
530 result.success = true;
531 result.traces = last_run_worker.traces.last().cloned();
532 result.debug_bytecodes.clone_from(&last_run_worker.debug_bytecodes);
533 result.breakpoints = last_run_worker.breakpoints.clone();
534 }
535
536 if !self.config.show_logs {
537 result.logs = workers[last_run_worker_idx].logs.clone();
538 }
539
540 for mut worker in workers {
541 result.gas_by_case.append(&mut worker.gas_by_case);
542 if self.config.show_logs {
543 result.logs.append(&mut worker.logs);
544 }
545 result.gas_report_traces.extend(worker.traces.into_iter().map(|t| t.arena));
546 HitMaps::merge_opt(&mut result.line_coverage, worker.coverage);
547 result.deprecated_cheatcodes.extend(worker.deprecated_cheatcodes);
548 }
549
550 if let Some(reason) = &result.reason
551 && let Some(reason) = SkipReason::decode_self(reason)
552 {
553 result.skipped = true;
554 result.reason = reason.0;
555 }
556
557 result
558 }
559
560 fn write_branch_frontiers(&self, workers: &mut [WorkerState<FEN>], func: &Function) {
561 let Some(frontier_dir) = &self.config.corpus.frontier_dir else {
562 return;
563 };
564 let limit = self.config.corpus.frontier_limit;
565 if limit == 0 {
566 return;
567 }
568
569 let frontiers = frontier::merge_frontiers(
570 limit,
571 workers.iter_mut().flat_map(|worker| worker.frontiers.drain(..)),
572 );
573 if frontiers.is_empty() {
574 return;
575 }
576
577 let artifact = FuzzBranchFrontierArtifact::new(func, limit, frontiers);
578 if let Err(err) = frontier::write_frontier_artifact(frontier_dir, &artifact) {
579 warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
580 }
581 }
582
583 #[allow(clippy::too_many_arguments)]
585 fn run_worker(
586 &self,
587 worker_id: usize,
588 func: &Function,
589 fuzz_fixtures: &FuzzFixtures,
590 address: Address,
591 rd: &RevertDecoder,
592 shared_state: &SharedFuzzState,
593 progress: Option<&ProgressBar>,
594 ) -> Result<WorkerState<FEN>> {
595 let fuzz_state = shared_state.state.fork();
597 let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
598 let calldata_strategy = proptest::prop_oneof![
599 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
600 dictionary_weight => fuzz_calldata_from_state(func.clone(), &fuzz_state, fuzz_fixtures),
601 ];
602 let value_strategy = if func.state_mutability == alloy_json_abi::StateMutability::Payable {
603 fuzz_msg_value(self.config.corpus.payable_value_weight).boxed()
604 } else {
605 Just(None).boxed()
606 };
607 let strategy =
608 (calldata_strategy, value_strategy).prop_map(move |(calldata, value)| BasicTxDetails {
609 warp: None,
610 roll: None,
611 sender: Default::default(),
612 call_details: CallDetails { target: Default::default(), calldata, value },
613 });
614
615 let replay_target = ReplayTarget {
616 stateless: Some(StatelessReplayTarget { function: func, address }),
617 fuzzed_contracts: None,
618 dynamic: None,
619 };
620 let mut corpus = WorkerCorpus::new(
621 worker_id,
622 self.config.corpus.clone(),
623 strategy.boxed(),
624 (worker_id == 0).then_some(&self.executor_f),
626 replay_target,
627 )?;
628 let mut executor = self.executor_f.clone();
629 let frontier_limit = if self.config.corpus.capture_branch_frontiers() {
630 self.config.corpus.frontier_limit
631 } else {
632 0
633 };
634 let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
635
636 let mut worker = WorkerState::new(worker_id);
637 let max_traces_to_collect =
639 std::cmp::max(1, self.config.gas_report_samples / self.num_workers as u32);
640
641 let worker_runs = self.runs_per_worker(worker_id);
642 debug!(worker_runs);
643
644 let mut runner_config = self.runner.config().clone();
645 runner_config.cases = worker_runs;
646
647 let mut runner = if let Some(seed) = self.config.seed {
648 let worker_seed = Self::fuzz_worker_seed(seed, worker_id);
649 trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}");
650 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
651 TestRunner::new_with_rng(runner_config, rng)
652 } else {
653 TestRunner::new(runner_config)
654 };
655
656 if let Some(target_run) = self.config.run {
657 for _ in 1..target_run {
658 if let Err(err) = corpus.new_input(&mut runner, &fuzz_state, func) {
659 worker.failure = Some(TestCaseError::fail(format!(
660 "failed to generate fuzzed input in worker {}: {err}",
661 worker.id
662 )));
663 shared_state.try_claim_failure(worker_id);
664 return Ok(worker);
665 }
666 }
667 }
668
669 let mut persisted_failure =
670 self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none());
671
672 let sync_offset = (worker_id as u32).saturating_mul(100);
675 let sync_threshold = SYNC_INTERVAL + sync_offset;
676 let mut runs_since_sync = sync_threshold; let mut last_metrics_report = Instant::now();
678 'stop: while shared_state.should_continue() && worker.runs < worker_runs {
682 let (input, fuzz_run) = if worker_id == 0
684 && let Some(failure) = persisted_failure.take()
685 && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector)
686 {
687 let seed = failure.fuzz.seed.or(self.config.seed);
688 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
689 && let Some(seed) = seed
690 {
691 let run = failure.fuzz.run.unwrap_or(1);
692 let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize;
693 cheats.set_seed(Self::fuzz_run_seed(seed, worker, run));
694 }
695
696 (
697 failure.calldata.clone(),
698 Some(FuzzRunMetadata::new(
699 seed,
700 failure.fuzz.run,
701 Some(failure.fuzz.worker.unwrap_or(worker_id as u32)),
702 )),
703 )
704 } else {
705 runs_since_sync += 1;
706 if runs_since_sync >= sync_threshold {
707 let timer = Instant::now();
708 corpus.sync(
709 self.num_workers,
710 &executor,
711 replay_target,
712 &shared_state.global_corpus_metrics,
713 )?;
714 trace!("finished corpus sync in {:?}", timer.elapsed());
715 runs_since_sync = 0;
716 }
717
718 let fuzz_run = self.config.run.unwrap_or(worker.runs + 1);
719 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut()
720 && let Some(seed) = self.config.seed
721 {
722 cheats.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run));
723 }
724
725 let input = match corpus.new_input(&mut runner, &fuzz_state, func) {
726 Ok(input) => input,
727 Err(err) => {
728 worker.failure = Some(TestCaseError::fail(format!(
729 "failed to generate fuzzed input in worker {}: {err}",
730 worker.id
731 )));
732 shared_state.try_claim_failure(worker_id);
733 break 'stop;
734 }
735 };
736
737 (
738 input,
739 Some(FuzzRunMetadata::new(
740 self.config.seed,
741 Some(fuzz_run),
742 Some(worker_id as u32),
743 )),
744 )
745 };
746
747 let mut inc_runs = || {
748 let total_runs = shared_state.increment_runs();
749 debug_assert!(
750 shared_state.timer.is_enabled()
751 || total_runs
752 <= if self.config.run.is_some() { 1 } else { self.config.runs },
753 "worker runs were not distributed correctly"
754 );
755 worker.runs += 1;
756 if let Some(progress) = progress {
757 progress.inc(1);
758 }
759 total_runs
760 };
761
762 worker.last_run_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
763 match self.single_fuzz(
764 &executor,
765 address,
766 input,
767 &mut corpus,
768 &mut frontier_recorder,
769 fuzz_run.as_ref(),
770 ) {
771 Ok(fuzz_outcome) => match fuzz_outcome {
772 FuzzOutcome::Case(case) => {
773 let total_runs = inc_runs();
774
775 if worker_id == 0 && self.config.corpus.collect_edge_coverage() {
776 if let Some(progress) = progress {
777 corpus.sync_metrics(&shared_state.global_corpus_metrics);
778 progress
779 .set_message(format!("{}", shared_state.global_corpus_metrics));
780 } else if last_metrics_report.elapsed()
781 > DURATION_BETWEEN_METRICS_REPORT
782 {
783 corpus.sync_metrics(&shared_state.global_corpus_metrics);
784 let metrics = json!({
786 "timestamp": SystemTime::now()
787 .duration_since(UNIX_EPOCH)?
788 .as_secs(),
789 "test": func.name,
790 "metrics": shared_state.global_corpus_metrics.load(),
791 });
792 let _ = sh_println!("{metrics}");
793 last_metrics_report = Instant::now();
794 }
795 }
796
797 worker.gas_by_case.push((case.case.gas, case.case.stipend));
798
799 if worker.first_case.is_none() {
800 worker.first_case = Some((total_runs, case.case));
801 }
802
803 if let Some(call_traces) = case.traces {
804 if worker.traces.len() == max_traces_to_collect as usize {
805 worker.traces.pop();
806 }
807 worker.traces.push(call_traces);
808 worker.debug_bytecodes = case.debug_bytecodes;
809 worker.breakpoints = Some(case.breakpoints);
810 }
811
812 if self.config.show_logs {
816 worker.logs.extend(case.logs);
817 } else {
818 worker.logs = case.logs;
819 }
820
821 HitMaps::merge_opt(&mut worker.coverage, case.coverage);
822 worker.deprecated_cheatcodes = case.deprecated_cheatcodes;
823 }
824 FuzzOutcome::CounterExample(CounterExampleOutcome {
825 exit_reason: status,
826 counterexample: outcome,
827 ..
828 }) => {
829 inc_runs();
830 worker.failure_run = fuzz_run;
831
832 let reason = if outcome.1.reverter == Some(CHEATCODE_ADDRESS) {
835 SkipReason::decode(&outcome.1.result)
836 .map(|reason| reason.to_string())
837 .or_else(|| rd.maybe_decode(&outcome.1.result, status))
838 } else {
839 rd.maybe_decode(&outcome.1.result, status)
840 };
841 worker.logs.extend(outcome.1.logs.clone());
842 worker.counterexample = outcome;
843 worker.failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
844 shared_state.try_claim_failure(worker_id);
845 break 'stop;
846 }
847 },
848 Err(err) => match err {
849 TestCaseError::Fail(_) => {
850 worker.failure = Some(err);
851 shared_state.try_claim_failure(worker_id);
852 break 'stop;
853 }
854 TestCaseError::Reject(_) => {
855 let max = self.config.max_test_rejects;
856
857 let total = shared_state.increment_rejects();
858
859 if !self.config.corpus.collect_edge_coverage()
862 && let Some(progress) = progress
863 {
864 progress.set_message(format!("([{total}] rejected)"));
865 }
866
867 if max > 0 && total > max {
868 worker.failure =
869 Some(TestCaseError::reject(FuzzError::TooManyRejects(max)));
870 shared_state.try_claim_failure(worker_id);
871 break 'stop;
872 }
873 }
874 },
875 }
876 }
877
878 if worker_id == 0 {
879 worker.failed_corpus_replays = corpus.failed_replays;
880 }
881 worker.frontiers = frontier_recorder.into_frontiers();
882
883 trace!("worker {worker_id} fuzz stats");
885 fuzz_state.log_stats();
886
887 Ok(worker)
888 }
889
890 const fn runs_per_worker(&self, worker_id: usize) -> u32 {
892 let worker_id = worker_id as u32;
893 let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs };
894 let n = self.num_workers as u32;
895 let runs = total_runs / n;
896 let remainder = total_runs % n;
897 if worker_id < remainder { runs + 1 } else { runs }
900 }
901
902 fn worker_ids(&self) -> Vec<usize> {
904 if self.config.run.is_some() {
905 vec![self.config.worker.unwrap_or(0) as usize]
906 } else {
907 (0..self.num_workers).collect()
908 }
909 }
910
911 fn fuzz_worker_seed(seed: U256, worker_id: usize) -> U256 {
913 if worker_id == 0 {
914 seed
915 } else {
916 let worker_id = worker_id as u32;
917 let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat();
918 U256::from_be_bytes(keccak256(seed_data).0)
919 }
920 }
921
922 fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 {
924 Self::fuzz_worker_seed(seed, worker_id).wrapping_add(U256::from(run.saturating_sub(1)))
925 }
926}