Skip to main content

foundry_evm/executors/fuzz/
mod.rs

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
54/// Corpus syncs across workers every `SYNC_INTERVAL` runs.
55const SYNC_INTERVAL: u32 = 1000;
56
57/// Minimum number of runs per worker.
58/// This is mainly to reduce the overall number of rayon jobs.
59const MIN_RUNS_PER_WORKER: u32 = 64;
60
61struct WorkerState<FEN: FoundryEvmNetwork> {
62    /// Worker identifier
63    id: usize,
64    /// First fuzz case this worker encountered (with global run number)
65    first_case: Option<(u32, FuzzCase)>,
66    /// Gas usage for all cases this worker ran
67    gas_by_case: Vec<(u64, u64)>,
68    /// Counterexample if this worker found one
69    counterexample: Option<(BasicTxDetails, RawCallResult<FEN>)>,
70    /// Traces collected by this worker
71    ///
72    /// Stores up to `max_traces_to_collect` which is `config.gas_report_samples / num_workers`
73    traces: Vec<SparsedTraceArena>,
74    /// Runtime bytecodes for the last collected trace.
75    debug_bytecodes: AddressHashMap<Bytes>,
76    /// Last breakpoints from this worker
77    breakpoints: Option<Breakpoints>,
78    /// Coverage collected by this worker
79    coverage: Option<HitMaps>,
80    /// Logs from all cases this worker ran
81    logs: Vec<Log>,
82    /// Deprecated cheatcodes seen by this worker
83    deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
84    /// Number of runs this worker completed
85    runs: u32,
86    /// Failure reason if this worker failed
87    failure: Option<TestCaseError>,
88    /// Fuzz run metadata that produced the failure.
89    failure_run: Option<FuzzRunMetadata>,
90    /// Last run timestamp in milliseconds
91    ///
92    /// Used to identify which worker ran last and collect its traces and call breakpoints
93    last_run_timestamp: u128,
94    /// Failed corpus replays
95    failed_corpus_replays: usize,
96    /// Branch frontiers captured for symbolic follow-up.
97    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
123/// Shared state for coordinating parallel fuzz workers
124struct SharedFuzzState {
125    state: EvmFuzzState,
126    /// Total runs across workers
127    total_runs: Arc<AtomicU32>,
128    /// Found failure
129    ///
130    /// The worker that found the failure sets it's ID.
131    ///
132    /// This ID is then used to correctly extract the failure reason and counterexample.
133    failed_worker_id: OnceLock<usize>,
134    /// Total rejects across workers
135    total_rejects: Arc<AtomicU32>,
136    /// Fuzz timer
137    timer: FuzzTestTimer,
138    /// Whether a worker stopped because the timer expired.
139    timed_out: AtomicBool,
140    /// Global corpus metrics
141    global_corpus_metrics: GlobalCorpusMetrics,
142
143    /// Global test suite early exit.
144    global_early_exit: EarlyExit,
145    /// Local fuzz early exit.
146    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    /// Increments the number of runs and returns the new value.
165    fn increment_runs(&self) -> u32 {
166        self.total_runs.fetch_add(1, Ordering::Relaxed) + 1
167    }
168
169    /// Increments and returns the new value of the number of rejected tests.
170    fn increment_rejects(&self) -> u32 {
171        self.total_rejects.fetch_add(1, Ordering::Relaxed) + 1
172    }
173
174    /// Returns `true` if the worker should continue running.
175    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    /// Returns `true` if fail-fast or Ctrl-C stopped the campaign before its timeout or
187    /// `planned_runs`.
188    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    /// Returns true if the worker was able to claim the failure, false if failure was set by
195    /// another worker
196    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
207/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`].
208///
209/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with
210/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the
211/// configuration which can be overridden via [environment variables](proptest::test_runner::Config)
212pub struct FuzzedExecutor<FEN: FoundryEvmNetwork> {
213    /// The EVM executor.
214    executor_f: Executor<FEN>,
215    /// The fuzzer
216    runner: TestRunner,
217    /// The account that calls tests.
218    sender: Address,
219    /// The fuzz configuration.
220    config: FuzzConfig,
221    /// The persisted counterexample to be replayed, if any.
222    persisted_failure: Option<BaseCounterExample>,
223    /// An existing corpus to replay before persisting into the configured corpus directory.
224    corpus_replay_dir: Option<PathBuf>,
225    /// The number of parallel workers.
226    num_workers: usize,
227}
228
229impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
230    /// Instantiates a fuzzed executor given a testrunner
231    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    /// Fuzzes the provided function, assuming it is available at the contract at `address`
260    ///
261    /// Returns a list of all the consumed gas and calldata of every fuzz case.
262    #[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    /// Replays the persisted single-call counterexample exactly once.
310    ///
311    /// Unlike [`Self::fuzz`], this never falls through to generated inputs if the persisted
312    /// calldata now succeeds or is rejected by `vm.assume`.
313    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    /// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
419    /// or a `CounterExampleOutcome`
420    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        // Handle `vm.assume` before recording coverage or persisting the input.
471        if kind == CampaignCallKind::AssumptionRejected {
472            // Account for the attempted corpus mutation without retaining or crediting the input.
473            coverage_metrics.process_inputs(&[], &[], false, None);
474            return Err(TestCaseError::reject(FuzzError::AssumeReject));
475        }
476
477        if call.skip_reason().is_some() {
478            // Account for the attempted corpus mutation without retaining or crediting the input.
479            coverage_metrics.process_inputs(&[], &[], false, None);
480        } else {
481            let new_coverage = coverage_metrics.merge_edge_coverage(&mut call);
482            // `new_coverage` is only meaningful when edge coverage is collected; otherwise
483            // `merge_edge_coverage` always returns `false`, so record it as unknown for frontiers.
484            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        // Consider call success if test should not fail on reverts and reverter is not the test
507        // address or one of the network's cheatcode contracts.
508        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    /// Aggregates the results from all workers
550    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        // Find first case and last run worker. Set `failed_corpus_replays`.
565        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            // Only set replays from master which is responsible for replaying persisted corpus.
579            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    /// Runs a single fuzz worker
690    #[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        // Prepare
703        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            // Master worker replays the persisted corpus using the executor
734            (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        // We want to collect at least one trace which will be displayed to user.
747        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        // Offset to stagger corpus syncs across workers; so that workers don't sync at the same
783        // time.
784        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; // Always sync at the start.
787        let mut last_metrics_report = Instant::now();
788        // Continue while:
789        // 1. Global state allows (not timed out, not at global limit, no failure found)
790        // 2. Worker hasn't reached its specific run limit
791        'stop: while shared_state.should_continue() && worker.runs < worker_runs {
792            // If counterexample recorded, replay it first, without incrementing runs.
793            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                                // Display metrics inline.
912                                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                        // Always store logs from the last run in test_data.logs for display at
940                        // verbosity >= 2. When show_logs is true,
941                        // accumulate all logs. When false, only keep the last run's logs.
942                        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                        // Only classify magic skip payloads minted by the skip cheatcode.
962                        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                        // Update progress bar to reflect rejected runs.
990                        // TODO(dani): (pre-existing) conflicts with corpus metrics `set_message`
991                        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        // Logs stats
1018        trace!("worker {worker_id} fuzz stats");
1019        fuzz_state.log_stats();
1020
1021        Ok(worker)
1022    }
1023
1024    /// Determines the number of runs per worker.
1025    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        // Distribute the remainder evenly among the first `remainder` workers,
1032        // assuming `worker_id` is in `0..n`.
1033        if worker_id < remainder { runs + 1 } else { runs }
1034    }
1035
1036    /// Returns the worker IDs to execute.
1037    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    /// Derives the deterministic RNG seed for a fuzz worker.
1046    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    /// Derives the deterministic RNG seed for cheatcode randomness in a worker-local run.
1057    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}