Skip to main content

foundry_evm/executors/
corpus.rs

1//! Corpus management for parallel fuzzing with coverage-guided mutation.
2//!
3//! This module implements a corpus-based fuzzing system that stores, mutates, and shares
4//! transaction sequences across multiple fuzzing workers. Each corpus entry represents a
5//! sequence of transactions that has produced interesting coverage, and can be mutated to
6//! discover new execution paths.
7//!
8//! ## File System Structure
9//!
10//! The corpus is organized on disk as follows:
11//!
12//! ```text
13//! <corpus_dir>/
14//! ├── worker0/                  # Master (worker 0) directory
15//! │   ├── corpus/               # Master's corpus entries
16//! │   │   ├── <uuid>-<timestamp>.json          # Corpus entry (if small)
17//! │   │   ├── <uuid>-<timestamp>.json.gz       # Corpus entry (if large, compressed)
18//! │   └── sync/                 # Directory where other workers export new findings
19//! │       └── <uuid>-<timestamp>.json          # New entries from other workers
20//! └── workerN/                  # Worker N's directory
21//!     ├── corpus/               # Worker N's local corpus
22//!     │   └── ...
23//!     └── sync/                 # Worker 2's sync directory
24//!         └── ...
25//! ```
26//!
27//! ## Workflow
28//!
29//! - Each worker maintains its own local corpus with entries stored as JSON files
30//! - Workers export new interesting entries to the master's sync directory via hard links
31//! - The master (worker0) imports new entries from its sync directory and exports them to all the
32//!   other workers
33//! - Workers sync with the master to receive new corpus entries from other workers
34//! - This all happens periodically, there is no clear order in which workers export or import
35//!   entries since it doesn't matter as long as the corpus eventually syncs across all workers
36
37use super::corpus_io::{CorpusDirEntry, canonical_replay_dirs, read_corpus_dir};
38use crate::{
39    executors::{Executor, RawCallResult, invariant::execute_tx},
40    inspectors::{CmpOperands, EdgeIndexMap, MAX_EDGE_COUNT},
41};
42use alloy_dyn_abi::JsonAbiExt;
43use alloy_json_abi::Function;
44use alloy_primitives::{Address, Bytes, I256};
45use eyre::{Result, eyre};
46use foundry_common::{ContractsByAddress, ContractsByArtifact, sh_warn};
47use foundry_config::FuzzCorpusConfig;
48use foundry_evm_core::{evm::FoundryEvmNetwork, utils::StateChangeset};
49use foundry_evm_fuzz::{
50    BasicTxDetails,
51    invariant::{ArtifactFilters, FuzzRunIdentifiedContracts},
52    strategies::{
53        EvmFuzzState, FuzzStateReader, InvariantFuzzState, generate_msg_value, mutate_param_value,
54    },
55};
56use proptest::{
57    prelude::{Just, Rng, Strategy},
58    prop_oneof,
59    strategy::{BoxedStrategy, ValueTree},
60    test_runner::TestRunner,
61};
62use serde::{Deserialize, Serialize};
63use std::{
64    collections::HashSet,
65    fmt,
66    path::{Path, PathBuf},
67    sync::{
68        Arc,
69        atomic::{AtomicUsize, Ordering},
70    },
71    time::{SystemTime, UNIX_EPOCH},
72};
73use uuid::Uuid;
74
75const WORKER: &str = "worker";
76const CORPUS_DIR: &str = "corpus";
77const SYNC_DIR: &str = "sync";
78const OPTIMIZATION_BEST_FILE: &str = "optimization_best.json";
79
80const FAVORABILITY_THRESHOLD: f64 = 0.3;
81
82/// Threshold for compressing corpus entries.
83/// 4KiB is usually the minimum file size on popular file systems.
84const GZIP_THRESHOLD: usize = 4 * 1024;
85
86/// Possible mutation strategies to apply on a call sequence.
87#[derive(Debug, Clone)]
88enum MutationType {
89    /// Splice original call sequence.
90    Splice,
91    /// Repeat selected call several times.
92    Repeat,
93    /// Interleave calls from two random call sequences.
94    Interleave,
95    /// Replace prefix of the original call sequence with new calls.
96    Prefix,
97    /// Replace suffix of the original call sequence with new calls.
98    Suffix,
99    /// ABI mutate random args of selected call in sequence.
100    Abi,
101    /// Replace input bytes using comparison operands observed for a corpus entry
102    /// (input-to-state, LibAFL-style).
103    Cmp,
104}
105
106/// Persisted optimization state: the best value found and the sequence that produced it.
107#[derive(Clone, Serialize, Deserialize)]
108struct OptimizationState {
109    best_value: I256,
110    best_sequence: Vec<BasicTxDetails>,
111}
112
113/// Holds Corpus information.
114#[derive(Clone, Serialize)]
115struct CorpusEntry {
116    // Unique corpus identifier.
117    uuid: Uuid,
118    // Total mutations of corpus as primary source.
119    total_mutations: usize,
120    // New coverage found as a result of mutating this corpus.
121    new_finds_produced: usize,
122    // Corpus call sequence.
123    #[serde(skip_serializing)]
124    tx_seq: Vec<BasicTxDetails>,
125    // Per-call EVM comparison operands observed while executing this corpus entry.
126    // Parallel to `tx_seq`. Empty inner vec means "no cmp data for this call".
127    #[serde(skip_serializing)]
128    cmp_seq: Vec<Vec<CmpOperands>>,
129    // Whether this corpus is favored, i.e. producing new finds more often than
130    // `FAVORABILITY_THRESHOLD`.
131    is_favored: bool,
132    /// Timestamp of when this entry was written to disk in seconds.
133    #[serde(skip_serializing)]
134    timestamp: u64,
135}
136
137impl CorpusEntry {
138    /// Creates a corpus entry with a new UUID.
139    pub fn new(tx_seq: Vec<BasicTxDetails>) -> Self {
140        Self::new_with_cmp(tx_seq, Vec::new(), Uuid::new_v4())
141    }
142
143    /// Creates a corpus entry with the given UUID and per-call cmp operand log.
144    pub fn new_with_cmp(
145        tx_seq: Vec<BasicTxDetails>,
146        cmp_seq: Vec<Vec<CmpOperands>>,
147        uuid: Uuid,
148    ) -> Self {
149        Self {
150            uuid,
151            total_mutations: 0,
152            new_finds_produced: 0,
153            tx_seq,
154            cmp_seq,
155            is_favored: false,
156            timestamp: SystemTime::now()
157                .duration_since(UNIX_EPOCH)
158                .expect("time went backwards")
159                .as_secs(),
160        }
161    }
162
163    fn write_to_disk_in(&self, dir: &Path, can_gzip: bool) -> foundry_common::fs::Result<()> {
164        let file_name = self.file_name(can_gzip);
165        let path = dir.join(file_name);
166        if self.should_gzip(can_gzip) {
167            foundry_common::fs::write_json_gzip_file(&path, &self.tx_seq)
168        } else {
169            foundry_common::fs::write_json_file(&path, &self.tx_seq)
170        }
171    }
172
173    fn file_name(&self, can_gzip: bool) -> String {
174        let ext = if self.should_gzip(can_gzip) { ".json.gz" } else { ".json" };
175        format!("{}-{}{ext}", self.uuid, self.timestamp)
176    }
177
178    fn should_gzip(&self, can_gzip: bool) -> bool {
179        if !can_gzip {
180            return false;
181        }
182        let size: usize = self.tx_seq.iter().map(|tx| tx.estimate_serialized_size()).sum();
183        size > GZIP_THRESHOLD
184    }
185}
186
187/// Corpus entry selected by a worker and returned for logical-campaign persistence.
188#[derive(Debug, Clone)]
189pub(crate) struct CampaignCorpusEntry {
190    tx_seq: Vec<BasicTxDetails>,
191    dedupe_by_coverage: bool,
192}
193
194struct ReplayOutcome {
195    keep_entry: bool,
196    new_coverage: bool,
197    cmp_seq: Vec<Vec<CmpOperands>>,
198    failed_replays: usize,
199}
200
201#[derive(Clone, Copy)]
202pub(crate) struct ReplayTarget<'a> {
203    pub(crate) fuzzed_function: Option<&'a Function>,
204    pub(crate) fuzzed_contracts: Option<&'a FuzzRunIdentifiedContracts>,
205    pub(crate) dynamic: Option<&'a DynamicTargetCtx<'a>>,
206}
207
208struct ReplayCoverage<'a> {
209    history_map: &'a mut Vec<u8>,
210    edge_indices: &'a mut EdgeIndexMap,
211    sancov_history_map: &'a mut Vec<u8>,
212    metrics: Option<&'a mut CorpusMetrics>,
213}
214
215/// Campaign-level corpus state produced by replaying persisted corpus entries once.
216///
217/// Parallel invariant workers clone this seed so every worker starts with the same warmed corpus
218/// and coverage maps. That avoids each worker rediscovering persisted coverage relative to an empty
219/// local map.
220#[derive(Clone, Default)]
221pub(crate) struct WorkerCorpusSeed {
222    in_memory_corpus: Vec<CorpusEntry>,
223    history_map: Vec<u8>,
224    edge_indices: EdgeIndexMap,
225    sancov_history_map: Vec<u8>,
226    metrics: CorpusMetrics,
227    failed_replays: usize,
228    optimization_best_value: Option<I256>,
229    optimization_best_sequence: Vec<BasicTxDetails>,
230}
231
232impl WorkerCorpusSeed {
233    fn empty(config: &FuzzCorpusConfig) -> Self {
234        // Hash mode always merges a fixed `MAX_EDGE_COUNT` bitmap, so preallocate to avoid moving
235        // the one-time 64 KiB resize into the first merge. Collision-free and sancov maps grow on
236        // demand and start empty.
237        let history_map =
238            if config.collect_evm_edge_coverage() && !config.evm_edge_coverage_collision_free() {
239                vec![0u8; MAX_EDGE_COUNT]
240            } else {
241                Vec::new()
242            };
243        Self { history_map, ..Default::default() }
244    }
245
246    fn with_optimization_state(mut self, config: &FuzzCorpusConfig) -> Self {
247        if let Some((value, sequence)) = load_optimization_state(config) {
248            self.optimization_best_value = Some(value);
249            self.optimization_best_sequence = sequence;
250        }
251        self
252    }
253
254    pub(crate) fn clone_for_worker(&self, worker_id: usize, worker_count: usize) -> Self {
255        let in_memory_corpus = self
256            .in_memory_corpus
257            .iter()
258            .enumerate()
259            .filter(|(idx, _)| idx % worker_count == worker_id)
260            .map(|(_, entry)| entry.clone())
261            .collect::<Vec<_>>();
262
263        let mut metrics = self.metrics.clone();
264        metrics.corpus_count = in_memory_corpus.len();
265        metrics.favored_items = in_memory_corpus.iter().filter(|entry| entry.is_favored).count();
266
267        Self {
268            in_memory_corpus,
269            history_map: self.history_map.clone(),
270            edge_indices: self.edge_indices.clone(),
271            sancov_history_map: self.sancov_history_map.clone(),
272            metrics,
273            failed_replays: self.failed_replays,
274            optimization_best_value: self.optimization_best_value,
275            optimization_best_sequence: self.optimization_best_sequence.clone(),
276        }
277    }
278
279    pub(crate) fn load_from_disk<FEN: FoundryEvmNetwork>(
280        config: &FuzzCorpusConfig,
281        executor: Option<&Executor<FEN>>,
282        fuzzed_function: Option<&Function>,
283        fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
284        dynamic: Option<DynamicTargetCtx<'_>>,
285    ) -> Result<Self> {
286        let mut seed = Self::empty(config).with_optimization_state(config);
287        let Some(corpus_dir) = &config.corpus_dir else {
288            return Ok(seed);
289        };
290
291        // Seed in-memory corpus with the persisted optimization best sequence so the mutation
292        // engine can build on it in future runs.
293        if !seed.optimization_best_sequence.is_empty() {
294            seed.in_memory_corpus.push(CorpusEntry::new(seed.optimization_best_sequence.clone()));
295            seed.metrics.corpus_count += 1;
296        }
297
298        if fuzzed_contracts.is_some() && has_legacy_invariant_corpus_dirs(corpus_dir) {
299            let _ = sh_warn!(
300                "Ignoring legacy invariant corpus directories under {}; new corpus entries are persisted under the contract-level corpus directory.",
301                corpus_dir.display(),
302            );
303        }
304
305        let Some(executor) = executor else {
306            return Ok(seed);
307        };
308        let mut seen_entries =
309            seed.in_memory_corpus.iter().map(|entry| entry.uuid).collect::<HashSet<_>>();
310        let target = ReplayTarget { fuzzed_function, fuzzed_contracts, dynamic: dynamic.as_ref() };
311        for entry in unique_corpus_entries(&canonical_replay_dirs(corpus_dir), &mut seen_entries) {
312            let tx_seq = entry.read_tx_seq()?;
313            if tx_seq.is_empty() {
314                continue;
315            }
316
317            let coverage = ReplayCoverage {
318                history_map: &mut seed.history_map,
319                edge_indices: &mut seed.edge_indices,
320                sancov_history_map: &mut seed.sancov_history_map,
321                metrics: Some(&mut seed.metrics),
322            };
323            let ReplayOutcome { keep_entry, cmp_seq, failed_replays, .. } =
324                replay_corpus_sequence(&tx_seq, executor, target, coverage)?;
325            seed.failed_replays += failed_replays;
326            if !keep_entry {
327                continue;
328            }
329
330            seed.metrics.corpus_count += 1;
331            debug!(
332                target: "corpus",
333                "load sequence with len {} from corpus file {}",
334                tx_seq.len(),
335                entry.path.display()
336            );
337            seed.in_memory_corpus.push(CorpusEntry::new_with_cmp(tx_seq, cmp_seq, entry.uuid));
338        }
339
340        Ok(seed)
341    }
342
343    /// Filters and persists logical-campaign corpus entries after worker results have merged.
344    ///
345    /// This consumes the deferred entries and writes each retained entry as soon as replay proves
346    /// it contributes new coverage. Keeping this path streaming avoids building a second filtered
347    /// copy of every campaign entry during invariant finalization.
348    pub(crate) fn persist_filtered_campaign_outputs<FEN: FoundryEvmNetwork>(
349        &self,
350        config: &FuzzCorpusConfig,
351        entries: impl IntoIterator<Item = CampaignCorpusEntry>,
352        executor: &Executor<FEN>,
353        target: ReplayTarget<'_>,
354        optimization_best: Option<(I256, &[BasicTxDetails])>,
355    ) -> Result<()> {
356        let mut history_map = self.history_map.clone();
357        let mut edge_indices = self.edge_indices.clone();
358        let mut sancov_history_map = self.sancov_history_map.clone();
359
360        let mut output_dir_ready = false;
361        for entry in entries {
362            if entry.dedupe_by_coverage {
363                let coverage = ReplayCoverage {
364                    history_map: &mut history_map,
365                    edge_indices: &mut edge_indices,
366                    sancov_history_map: &mut sancov_history_map,
367                    metrics: None,
368                };
369                let ReplayOutcome { keep_entry, new_coverage, .. } =
370                    replay_corpus_sequence(&entry.tx_seq, executor, target, coverage)?;
371                if !keep_entry || !new_coverage {
372                    continue;
373                }
374            }
375
376            if !output_dir_ready {
377                prepare_campaign_output_dir(config);
378                output_dir_ready = true;
379            }
380            persist_campaign_entry(config, entry);
381        }
382
383        persist_optimization_output(config, optimization_best);
384        Ok(())
385    }
386}
387
388#[derive(Default)]
389pub(crate) struct GlobalCorpusMetrics {
390    // Number of edges seen during the invariant run.
391    cumulative_edges_seen: AtomicUsize,
392    // Number of features (new hitcount bin of previously hit edge) seen during the invariant run.
393    cumulative_features_seen: AtomicUsize,
394    // Number of corpus entries.
395    corpus_count: AtomicUsize,
396    // Number of corpus entries that are favored.
397    favored_items: AtomicUsize,
398}
399
400impl fmt::Display for GlobalCorpusMetrics {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        self.load().fmt(f)
403    }
404}
405
406impl GlobalCorpusMetrics {
407    pub(crate) fn load(&self) -> CorpusMetrics {
408        CorpusMetrics {
409            cumulative_edges_seen: self.cumulative_edges_seen.load(Ordering::Relaxed),
410            cumulative_features_seen: self.cumulative_features_seen.load(Ordering::Relaxed),
411            corpus_count: self.corpus_count.load(Ordering::Relaxed),
412            favored_items: self.favored_items.load(Ordering::Relaxed),
413        }
414    }
415}
416
417#[derive(Serialize, Default, Clone)]
418pub(crate) struct CorpusMetrics {
419    // Number of edges seen during the invariant run.
420    cumulative_edges_seen: usize,
421    // Number of features (new hitcount bin of previously hit edge) seen during the invariant run.
422    cumulative_features_seen: usize,
423    // Number of corpus entries.
424    corpus_count: usize,
425    // Number of corpus entries that are favored.
426    favored_items: usize,
427}
428
429impl fmt::Display for CorpusMetrics {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        writeln!(f)?;
432        writeln!(f, "      Edge coverage metrics:")?;
433        writeln!(f, "        - cumulative edges seen: {}", self.cumulative_edges_seen)?;
434        writeln!(f, "        - cumulative features seen: {}", self.cumulative_features_seen)?;
435        writeln!(f, "        - corpus count: {}", self.corpus_count)?;
436        write!(f, "        - favored items: {}", self.favored_items)?;
437        Ok(())
438    }
439}
440
441impl CorpusMetrics {
442    /// Records number of new edges or features explored during the campaign.
443    pub const fn update_seen(&mut self, is_edge: bool) {
444        if is_edge {
445            self.cumulative_edges_seen += 1;
446        } else {
447            self.cumulative_features_seen += 1;
448        }
449    }
450
451    /// Updates campaign favored items.
452    pub const fn update_favored(&mut self, is_favored: bool, corpus_favored: bool) {
453        if is_favored && !corpus_favored {
454            self.favored_items += 1;
455        } else if !is_favored && corpus_favored {
456            self.favored_items -= 1;
457        }
458    }
459}
460
461/// Per-worker corpus manager.
462pub struct WorkerCorpus {
463    /// Worker Id
464    id: usize,
465    /// In-memory corpus entries populated from the persisted files and
466    /// runs administered by this worker.
467    in_memory_corpus: Vec<CorpusEntry>,
468    /// History of binned hitcount of edges seen during fuzzing
469    history_map: Vec<u8>,
470    /// Stable dense EVM edge IDs for this worker's history map.
471    edge_indices: EdgeIndexMap,
472    /// History of binned hitcount of sancov (native Rust) edges seen during fuzzing
473    sancov_history_map: Vec<u8>,
474    /// Number of failed replays from initial corpus
475    pub(crate) failed_replays: usize,
476    /// Worker Metrics
477    pub(crate) metrics: CorpusMetrics,
478    /// Fuzzed calls generator.
479    tx_generator: BoxedStrategy<BasicTxDetails>,
480    /// Call sequence mutation strategy type generator used by stateful fuzzing.
481    mutation_generator: BoxedStrategy<MutationType>,
482    /// Identifier of current mutated entry for this worker.
483    current_mutated: Option<Uuid>,
484    /// Config
485    config: Arc<FuzzCorpusConfig>,
486    /// Indices of new entries added to [`WorkerCorpus::in_memory_corpus`] since last sync.
487    new_entry_indices: Vec<usize>,
488    /// Last sync timestamp in seconds.
489    last_sync_timestamp: u64,
490    /// Worker Dir
491    /// corpus_dir/worker1/
492    worker_dir: Option<PathBuf>,
493    /// Metrics at last sync - used to calculate deltas while syncing with global metrics
494    last_sync_metrics: CorpusMetrics,
495    /// Optimization mode: the best value found so far (loaded from disk or discovered in-run).
496    optimization_best_value: Option<I256>,
497    /// Optimization mode: the call sequence that produced the best value.
498    optimization_best_sequence: Vec<BasicTxDetails>,
499}
500
501/// Refs used during corpus replay to register contracts deployed mid-sequence as fuzz targets,
502/// mirroring the campaign loop so follow-up calls into them aren't dropped by `can_replay_tx`.
503#[derive(Clone, Copy)]
504pub struct DynamicTargetCtx<'a> {
505    pub project_contracts: &'a ContractsByArtifact,
506    pub setup_contracts: &'a ContractsByAddress,
507    pub artifact_filters: &'a ArtifactFilters,
508}
509
510/// Registers contracts created by the last tx so subsequent txs in the same replayed sequence
511/// can target them.
512pub(crate) fn register_replay_created(
513    state_changeset: &StateChangeset,
514    dynamic: Option<&DynamicTargetCtx<'_>>,
515    fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
516    created: &mut Vec<Address>,
517) {
518    let (Some(dynamic), Some(fuzzed_contracts)) = (dynamic, fuzzed_contracts) else {
519        return;
520    };
521    if let Err(error) = fuzzed_contracts.collect_created_contracts(
522        state_changeset,
523        dynamic.project_contracts,
524        dynamic.setup_contracts,
525        dynamic.artifact_filters,
526        created,
527    ) {
528        warn!(target: "corpus", "{error}");
529    }
530}
531
532/// Clears dynamic targets added during a replayed entry so they don't leak into the next one.
533pub(crate) fn rollback_replay_created(
534    fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
535    created: Vec<Address>,
536) {
537    if !created.is_empty()
538        && let Some(fuzzed_contracts) = fuzzed_contracts
539    {
540        fuzzed_contracts.clear_created_contracts(created);
541    }
542}
543
544fn load_optimization_state(config: &FuzzCorpusConfig) -> Option<(I256, Vec<BasicTxDetails>)> {
545    let corpus_dir = config.corpus_dir.as_ref()?;
546    let opt_path = corpus_dir.join(OPTIMIZATION_BEST_FILE);
547    if !opt_path.is_file() {
548        return None;
549    }
550
551    match foundry_common::fs::read_json_file::<OptimizationState>(&opt_path) {
552        Ok(state) => {
553            debug!(
554                target: "corpus",
555                "loaded optimization best value {} with sequence len {}",
556                state.best_value,
557                state.best_sequence.len()
558            );
559            Some((state.best_value, state.best_sequence))
560        }
561        Err(err) => {
562            let _ = sh_warn!(
563                "failed to load optimization state from {}: {err}; starting without persisted optimization seed",
564                opt_path.display()
565            );
566            None
567        }
568    }
569}
570
571fn replay_corpus_sequence<FEN: FoundryEvmNetwork>(
572    tx_seq: &[BasicTxDetails],
573    executor: &Executor<FEN>,
574    target: ReplayTarget<'_>,
575    coverage: ReplayCoverage<'_>,
576) -> Result<ReplayOutcome> {
577    let mut executor = executor.clone();
578    replay_corpus_sequence_with_executor(tx_seq, &mut executor, target, coverage, false, true)
579}
580
581fn replay_corpus_sequence_with_executor<FEN: FoundryEvmNetwork>(
582    tx_seq: &[BasicTxDetails],
583    executor: &mut Executor<FEN>,
584    target: ReplayTarget<'_>,
585    mut coverage: ReplayCoverage<'_>,
586    trace_sync: bool,
587    reject_unmatched_function: bool,
588) -> Result<ReplayOutcome> {
589    let mut cmp_seq = Vec::with_capacity(tx_seq.len());
590    let mut failed_replays = 0;
591    let mut new_coverage_for_entry = false;
592    let mut created: Vec<Address> = Vec::new();
593
594    for tx in tx_seq {
595        if WorkerCorpus::can_replay_tx(tx, target.fuzzed_function, target.fuzzed_contracts) {
596            let mut call_result = execute_tx(executor, tx)?;
597            cmp_seq.push(call_result.evm_cmp_values.take().unwrap_or_default());
598            let (new_coverage, is_edge) = call_result.merge_all_coverage(
599                coverage.history_map,
600                coverage.edge_indices,
601                coverage.sancov_history_map,
602            );
603            if new_coverage {
604                new_coverage_for_entry = true;
605                if let Some(metrics) = coverage.metrics.as_deref_mut() {
606                    metrics.update_seen(is_edge);
607                }
608            }
609
610            register_replay_created(
611                &call_result.state_changeset,
612                target.dynamic,
613                target.fuzzed_contracts,
614                &mut created,
615            );
616
617            // Commit only when running invariant / stateful tests.
618            if target.fuzzed_contracts.is_some() {
619                executor.commit(&mut call_result);
620            }
621
622            if trace_sync {
623                trace!(
624                    target: "corpus",
625                    %new_coverage,
626                    ?tx,
627                    "replayed tx for syncing",
628                );
629            }
630        } else {
631            cmp_seq.push(Vec::new());
632            failed_replays += 1;
633
634            if reject_unmatched_function && target.fuzzed_function.is_some() {
635                rollback_replay_created(target.fuzzed_contracts, created);
636                return Ok(ReplayOutcome {
637                    keep_entry: false,
638                    new_coverage: new_coverage_for_entry,
639                    cmp_seq,
640                    failed_replays,
641                });
642            }
643        }
644    }
645    rollback_replay_created(target.fuzzed_contracts, created);
646
647    Ok(ReplayOutcome {
648        keep_entry: true,
649        new_coverage: new_coverage_for_entry,
650        cmp_seq,
651        failed_replays,
652    })
653}
654
655impl WorkerCorpus {
656    pub fn new<FEN: FoundryEvmNetwork>(
657        id: usize,
658        config: FuzzCorpusConfig,
659        tx_generator: BoxedStrategy<BasicTxDetails>,
660        // Only required by master worker (id = 0) to replay existing corpus.
661        executor: Option<&Executor<FEN>>,
662        fuzzed_function: Option<&Function>,
663        fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
664        dynamic: Option<DynamicTargetCtx<'_>>,
665    ) -> Result<Self> {
666        let seed = if id == 0 {
667            WorkerCorpusSeed::load_from_disk(
668                &config,
669                executor,
670                fuzzed_function,
671                fuzzed_contracts,
672                dynamic,
673            )?
674        } else {
675            WorkerCorpusSeed::empty(&config).with_optimization_state(&config)
676        };
677        Ok(Self::from_seed(id, config, tx_generator, seed))
678    }
679
680    pub(crate) fn from_seed(
681        id: usize,
682        config: FuzzCorpusConfig,
683        tx_generator: BoxedStrategy<BasicTxDetails>,
684        seed: WorkerCorpusSeed,
685    ) -> Self {
686        let mutation_generator = prop_oneof![
687            Just(MutationType::Splice),
688            Just(MutationType::Repeat),
689            Just(MutationType::Interleave),
690            Just(MutationType::Prefix),
691            Just(MutationType::Suffix),
692            Just(MutationType::Abi),
693            Just(MutationType::Cmp),
694        ]
695        .boxed();
696
697        let worker_dir = config.corpus_dir.as_ref().map(|corpus_dir| {
698            let worker_dir = corpus_dir.join(format!("{WORKER}{id}"));
699            let worker_corpus = worker_dir.join(CORPUS_DIR);
700            let sync_dir = worker_dir.join(SYNC_DIR);
701
702            // Create the necessary directories for the worker.
703            let _ = foundry_common::fs::create_dir_all(&worker_corpus);
704            let _ = foundry_common::fs::create_dir_all(&sync_dir);
705
706            worker_dir
707        });
708
709        Self {
710            id,
711            in_memory_corpus: seed.in_memory_corpus,
712            history_map: seed.history_map,
713            edge_indices: seed.edge_indices,
714            sancov_history_map: seed.sancov_history_map,
715            failed_replays: seed.failed_replays,
716            metrics: seed.metrics,
717            tx_generator,
718            mutation_generator,
719            current_mutated: None,
720            config: config.into(),
721            new_entry_indices: Default::default(),
722            last_sync_timestamp: 0,
723            worker_dir,
724            last_sync_metrics: Default::default(),
725            optimization_best_value: seed.optimization_best_value,
726            optimization_best_sequence: seed.optimization_best_sequence,
727        }
728    }
729
730    /// Updates stats for the given call sequence, if new coverage produced.
731    /// Persists the call sequence (if corpus directory is configured and new coverage or
732    /// improved optimization value) and updates in-memory corpus.
733    #[instrument(skip_all)]
734    pub fn process_inputs(
735        &mut self,
736        inputs: &[BasicTxDetails],
737        cmp_seq: &[Vec<CmpOperands>],
738        new_coverage: bool,
739        optimization: Option<(I256, Vec<BasicTxDetails>)>,
740    ) {
741        let _ = self.process_inputs_inner(inputs, cmp_seq, new_coverage, optimization, true);
742    }
743
744    /// Updates worker-local corpus state and returns any corpus entry to persist after the
745    /// logical campaign has merged worker outputs.
746    #[instrument(skip_all)]
747    pub fn process_inputs_for_campaign(
748        &mut self,
749        inputs: &[BasicTxDetails],
750        cmp_seq: &[Vec<CmpOperands>],
751        new_coverage: bool,
752        optimization: Option<(I256, Vec<BasicTxDetails>)>,
753    ) -> Option<CampaignCorpusEntry> {
754        self.process_inputs_inner(inputs, cmp_seq, new_coverage, optimization, false)
755    }
756
757    fn process_inputs_inner(
758        &mut self,
759        inputs: &[BasicTxDetails],
760        cmp_seq: &[Vec<CmpOperands>],
761        new_coverage: bool,
762        optimization: Option<(I256, Vec<BasicTxDetails>)>,
763        persist_now: bool,
764    ) -> Option<CampaignCorpusEntry> {
765        // Check if this run improved the optimization value.
766        let improved_optimization = optimization.as_ref().is_some_and(|(value, _)| {
767            self.optimization_best_value.is_none_or(|best| *value > best)
768        });
769
770        // Update stats of current mutated primary corpus.
771        if let Some(uuid) = &self.current_mutated {
772            let should_credit = new_coverage || improved_optimization;
773            if let Some(corpus) =
774                self.in_memory_corpus.iter_mut().find(|corpus| corpus.uuid == *uuid)
775            {
776                corpus.total_mutations += 1;
777                if should_credit {
778                    corpus.new_finds_produced += 1
779                }
780                let is_favored = (corpus.new_finds_produced as f64 / corpus.total_mutations as f64)
781                    > FAVORABILITY_THRESHOLD;
782                self.metrics.update_favored(is_favored, corpus.is_favored);
783                corpus.is_favored = is_favored;
784
785                trace!(
786                    target: "corpus",
787                    "updated corpus {}, total mutations: {}, new finds: {}",
788                    corpus.uuid, corpus.total_mutations, corpus.new_finds_produced
789                );
790            }
791
792            self.current_mutated = None;
793        }
794
795        if let Some((value, best_seq)) = optimization
796            && improved_optimization
797        {
798            self.optimization_best_value = Some(value);
799            self.optimization_best_sequence = best_seq;
800            if persist_now {
801                self.persist_optimization_state();
802            }
803        }
804
805        if !self.config.is_coverage_guided() {
806            return None;
807        }
808
809        // Collect inputs if current run produced new coverage or improved optimization.
810        if !new_coverage && !improved_optimization {
811            return None;
812        }
813
814        // When the run is interesting only because of optimization (no new coverage),
815        // add the best prefix to the corpus instead of the full run — the prefix is
816        // the sequence that actually achieved the best value.
817        assert!(!inputs.is_empty());
818        let corpus_inputs = if improved_optimization && !new_coverage {
819            self.optimization_best_sequence.clone()
820        } else {
821            inputs.to_vec()
822        };
823        let corpus_cmp_seq: Vec<Vec<CmpOperands>> =
824            cmp_seq.iter().take(corpus_inputs.len()).cloned().collect();
825        let campaign_entry = (!persist_now).then(|| CampaignCorpusEntry {
826            tx_seq: corpus_inputs.clone(),
827            dedupe_by_coverage: new_coverage,
828        });
829        let corpus = CorpusEntry::new_with_cmp(corpus_inputs, corpus_cmp_seq, Uuid::new_v4());
830
831        if persist_now && let Some(worker_corpus) = &self.worker_dir {
832            let worker_corpus = worker_corpus.join(CORPUS_DIR);
833            let write_result = corpus.write_to_disk_in(&worker_corpus, self.config.corpus_gzip);
834            if let Err(err) = write_result {
835                debug!(target: "corpus", %err, "failed to record call sequence {:?}", corpus.tx_seq);
836            } else {
837                trace!(
838                    target: "corpus",
839                    "persisted {} inputs for new coverage for {} corpus",
840                    corpus.tx_seq.len(),
841                    corpus.uuid,
842                );
843            }
844        }
845
846        // Track in-memory corpus changes to update MasterWorker on sync.
847        let new_index = self.in_memory_corpus.len();
848        self.new_entry_indices.push(new_index);
849
850        // This includes reverting txs in the corpus and `can_continue` removes
851        // them. We want this as it is new coverage and may help reach the other branch.
852        self.metrics.corpus_count += 1;
853        self.in_memory_corpus.push(corpus);
854
855        campaign_entry
856    }
857
858    /// Returns the previously persisted optimization best value and sequence (if any).
859    pub fn optimization_initial_state(&self) -> (Option<I256>, Vec<BasicTxDetails>) {
860        (self.optimization_best_value, self.optimization_best_sequence.clone())
861    }
862
863    /// Persists the current optimization best value and sequence to disk.
864    fn persist_optimization_state(&self) {
865        let optimization_best = self
866            .optimization_best_value
867            .map(|value| (value, self.optimization_best_sequence.as_slice()));
868        Self::persist_campaign_outputs(&self.config, Vec::new(), optimization_best);
869    }
870
871    /// Persists logical-campaign corpus and optimization outputs after worker results have merged.
872    pub(crate) fn persist_campaign_outputs(
873        config: &FuzzCorpusConfig,
874        entries: impl IntoIterator<Item = CampaignCorpusEntry>,
875        optimization_best: Option<(I256, &[BasicTxDetails])>,
876    ) {
877        let mut output_dir_ready = false;
878        for entry in entries {
879            if !output_dir_ready {
880                prepare_campaign_output_dir(config);
881                output_dir_ready = true;
882            }
883            persist_campaign_entry(config, entry);
884        }
885
886        persist_optimization_output(config, optimization_best);
887    }
888
889    /// Collects EVM and sancov coverage from call result and updates metrics.
890    pub fn merge_edge_coverage<FEN: FoundryEvmNetwork>(
891        &mut self,
892        call_result: &mut RawCallResult<FEN>,
893    ) -> bool {
894        if !self.config.collect_edge_coverage() {
895            return false;
896        }
897
898        let (new_coverage, is_edge) = call_result.merge_all_coverage(
899            &mut self.history_map,
900            &mut self.edge_indices,
901            &mut self.sancov_history_map,
902        );
903        if new_coverage {
904            self.metrics.update_seen(is_edge);
905        }
906        new_coverage
907    }
908
909    /// Generates new call sequence from in memory corpus. Evicts oldest corpus mutated more than
910    /// configured max mutations value. Used by invariant test campaigns.
911    #[instrument(skip_all)]
912    pub fn new_inputs(
913        &mut self,
914        test_runner: &mut TestRunner,
915        fuzz_state: &InvariantFuzzState,
916        targeted_contracts: &FuzzRunIdentifiedContracts,
917    ) -> Result<Vec<BasicTxDetails>> {
918        let mut new_seq = vec![];
919
920        // Early return with first_input only if corpus dir / coverage guided fuzzing not
921        // configured.
922        if !self.config.is_coverage_guided() {
923            new_seq.push(self.new_tx(test_runner)?);
924            return Ok(new_seq);
925        };
926
927        if !self.in_memory_corpus.is_empty() {
928            self.evict_oldest_corpus()?;
929
930            let mutation_type = self
931                .mutation_generator
932                .new_tree(test_runner)
933                .map_err(|err| eyre!("Could not generate mutation type {err}"))?
934                .current();
935
936            let rng = test_runner.rng();
937            let corpus_len = self.in_memory_corpus.len();
938            let primary = &self.in_memory_corpus[rng.random_range(0..corpus_len)];
939            let secondary = &self.in_memory_corpus[rng.random_range(0..corpus_len)];
940
941            match mutation_type {
942                MutationType::Splice => {
943                    trace!(target: "corpus", "splice {} and {}", primary.uuid, secondary.uuid);
944
945                    self.current_mutated = Some(primary.uuid);
946
947                    let start1 = rng.random_range(0..primary.tx_seq.len());
948                    let end1 = rng.random_range(start1..primary.tx_seq.len());
949
950                    let start2 = rng.random_range(0..secondary.tx_seq.len());
951                    let end2 = rng.random_range(start2..secondary.tx_seq.len());
952
953                    for tx in primary.tx_seq.iter().take(end1).skip(start1) {
954                        new_seq.push(tx.clone());
955                    }
956                    for tx in secondary.tx_seq.iter().take(end2).skip(start2) {
957                        new_seq.push(tx.clone());
958                    }
959                }
960                MutationType::Repeat => {
961                    let corpus = if rng.random::<bool>() { primary } else { secondary };
962                    trace!(target: "corpus", "repeat {}", corpus.uuid);
963
964                    self.current_mutated = Some(corpus.uuid);
965
966                    new_seq = corpus.tx_seq.clone();
967                    let start = rng.random_range(0..corpus.tx_seq.len());
968                    let end = rng.random_range(start..corpus.tx_seq.len());
969                    let item_idx = rng.random_range(0..corpus.tx_seq.len());
970                    let repeated = vec![new_seq[item_idx].clone(); end - start];
971                    new_seq.splice(start..end, repeated);
972                }
973                MutationType::Interleave => {
974                    trace!(target: "corpus", "interleave {} with {}", primary.uuid, secondary.uuid);
975
976                    self.current_mutated = Some(primary.uuid);
977
978                    for (tx1, tx2) in primary.tx_seq.iter().zip(secondary.tx_seq.iter()) {
979                        // TODO: chunks?
980                        let tx = if rng.random::<bool>() { tx1.clone() } else { tx2.clone() };
981                        new_seq.push(tx);
982                    }
983                }
984                MutationType::Prefix => {
985                    let corpus = if rng.random::<bool>() { primary } else { secondary };
986                    trace!(target: "corpus", "overwrite prefix of {}", corpus.uuid);
987
988                    self.current_mutated = Some(corpus.uuid);
989
990                    new_seq = corpus.tx_seq.clone();
991                    for i in 0..rng.random_range(0..=new_seq.len()) {
992                        new_seq[i] = self.new_tx(test_runner)?;
993                    }
994                }
995                MutationType::Suffix => {
996                    let corpus = if rng.random::<bool>() { primary } else { secondary };
997                    trace!(target: "corpus", "overwrite suffix of {}", corpus.uuid);
998
999                    self.current_mutated = Some(corpus.uuid);
1000
1001                    new_seq = corpus.tx_seq.clone();
1002                    for i in new_seq.len() - rng.random_range(0..new_seq.len())..corpus.tx_seq.len()
1003                    {
1004                        new_seq[i] = self.new_tx(test_runner)?;
1005                    }
1006                }
1007                MutationType::Abi => {
1008                    let targets = targeted_contracts.targets();
1009                    let corpus = if rng.random::<bool>() { primary } else { secondary };
1010                    trace!(target: "corpus", "ABI mutate args of {}", corpus.uuid);
1011
1012                    self.current_mutated = Some(corpus.uuid);
1013
1014                    new_seq = corpus.tx_seq.clone();
1015
1016                    let idx = rng.random_range(0..new_seq.len());
1017                    let tx = new_seq.get_mut(idx).unwrap();
1018                    if let (_, Some(function)) = targets.fuzzed_artifacts(tx) {
1019                        // TODO: add call_value to call details and mutate it as well as sender some
1020                        // of the time.
1021                        if !function.inputs.is_empty() {
1022                            self.abi_mutate(tx, function, test_runner, fuzz_state)?;
1023                        }
1024                    }
1025                }
1026                MutationType::Cmp => {
1027                    let targets = targeted_contracts.targets();
1028                    let corpus = if rng.random::<bool>() { primary } else { secondary };
1029                    trace!(target: "corpus", "cmp mutate args of {}", corpus.uuid);
1030
1031                    self.current_mutated = Some(corpus.uuid);
1032
1033                    new_seq = corpus.tx_seq.clone();
1034                    let candidates = corpus
1035                        .cmp_seq
1036                        .iter()
1037                        .enumerate()
1038                        .filter_map(|(idx, cmp_values)| (!cmp_values.is_empty()).then_some(idx))
1039                        .collect::<Vec<_>>();
1040
1041                    let mut mutated = false;
1042                    let fallback_idx = rng.random_range(0..new_seq.len());
1043                    if !candidates.is_empty() {
1044                        let start = rng.random_range(0..candidates.len());
1045                        for offset in 0..candidates.len() {
1046                            let idx = candidates[(start + offset) % candidates.len()];
1047                            let tx = new_seq.get_mut(idx).unwrap();
1048                            if let (_, Some(function)) = targets.fuzzed_artifacts(tx) {
1049                                mutated = Self::cmp_mutate(
1050                                    tx,
1051                                    function,
1052                                    corpus.cmp_seq[idx].as_slice(),
1053                                    test_runner,
1054                                )?;
1055                                if mutated {
1056                                    break;
1057                                }
1058                            }
1059                        }
1060                    }
1061
1062                    if !mutated {
1063                        let tx = new_seq.get_mut(fallback_idx).unwrap();
1064                        if let (_, Some(function)) = targets.fuzzed_artifacts(tx)
1065                            && !function.inputs.is_empty()
1066                        {
1067                            self.abi_mutate(tx, function, test_runner, fuzz_state)?;
1068                        }
1069                    }
1070                }
1071            }
1072        }
1073
1074        // Make sure the new sequence contains at least one tx to start fuzzing from.
1075        if new_seq.is_empty() {
1076            new_seq.push(self.new_tx(test_runner)?);
1077        }
1078        trace!(target: "corpus", "new sequence of {} calls generated", new_seq.len());
1079
1080        Ok(new_seq)
1081    }
1082
1083    /// Generates a new input from the shared in memory corpus.  Evicts oldest corpus mutated more
1084    /// than configured max mutations value. Used by fuzz (stateless) test campaigns.
1085    #[instrument(skip_all)]
1086    pub fn new_input(
1087        &mut self,
1088        test_runner: &mut TestRunner,
1089        fuzz_state: &EvmFuzzState,
1090        function: &Function,
1091    ) -> Result<Bytes> {
1092        // Early return if not running with coverage guided fuzzing.
1093        if !self.config.is_coverage_guided() {
1094            return Ok(self.new_tx(test_runner)?.call_details.calldata);
1095        }
1096
1097        self.evict_oldest_corpus()?;
1098
1099        let tx = if self.in_memory_corpus.is_empty() {
1100            self.new_tx(test_runner)?
1101        } else {
1102            let corpus = &self.in_memory_corpus
1103                [test_runner.rng().random_range(0..self.in_memory_corpus.len())];
1104            self.current_mutated = Some(corpus.uuid);
1105            let mut tx = corpus.tx_seq.first().unwrap().clone();
1106            let cmp_values = corpus.cmp_seq.first().map_or(&[][..], Vec::as_slice);
1107            if !Self::cmp_mutate(&mut tx, function, cmp_values, test_runner)?
1108                && !function.inputs.is_empty()
1109            {
1110                self.abi_mutate(&mut tx, function, test_runner, fuzz_state)?;
1111            }
1112            tx
1113        };
1114
1115        Ok(tx.call_details.calldata)
1116    }
1117
1118    /// Generates single call from corpus strategy.
1119    pub fn new_tx(&self, test_runner: &mut TestRunner) -> Result<BasicTxDetails> {
1120        Ok(self
1121            .tx_generator
1122            .new_tree(test_runner)
1123            .map_err(|_| eyre!("Could not generate case"))?
1124            .current())
1125    }
1126
1127    /// Returns the next call to be used in call sequence.
1128    /// If coverage guided fuzzing is not configured or if previous input was discarded then this is
1129    /// a new tx from strategy.
1130    /// If running with coverage guided fuzzing it returns a new call only when sequence
1131    /// does not have enough entries, or randomly. Otherwise, returns the next call from initial
1132    /// sequence.
1133    pub fn generate_next_input(
1134        &mut self,
1135        test_runner: &mut TestRunner,
1136        sequence: &[BasicTxDetails],
1137        discarded: bool,
1138        depth: usize,
1139    ) -> Result<BasicTxDetails> {
1140        // Early return with new input if corpus dir / coverage guided fuzzing not configured or if
1141        // call was discarded.
1142        if self.config.corpus_dir.is_none() || discarded {
1143            return self.new_tx(test_runner);
1144        }
1145
1146        // When running with coverage guided fuzzing enabled then generate new sequence if initial
1147        // sequence's length is less than depth or randomly, to occasionally intermix new txs.
1148        if depth > sequence.len().saturating_sub(1) || test_runner.rng().random_ratio(1, 10) {
1149            return self.new_tx(test_runner);
1150        }
1151
1152        // Continue with the next call initial sequence.
1153        Ok(sequence[depth].clone())
1154    }
1155
1156    /// Flush the oldest corpus mutated more than configured max mutations unless they are
1157    /// favored.
1158    fn evict_oldest_corpus(&mut self) -> Result<()> {
1159        if self.in_memory_corpus.len() > self.config.corpus_min_size.max(1)
1160            && let Some(index) = self.in_memory_corpus.iter().position(|corpus| {
1161                corpus.total_mutations > self.config.corpus_min_mutations && !corpus.is_favored
1162            })
1163        {
1164            let corpus = &self.in_memory_corpus[index];
1165
1166            trace!(target: "corpus", corpus=%serde_json::to_string(&corpus).unwrap(), "evict corpus");
1167
1168            // Remove corpus from memory.
1169            self.in_memory_corpus.remove(index);
1170
1171            // Adjust the tracked indices.
1172            self.new_entry_indices.retain_mut(|i| {
1173                if *i > index {
1174                    *i -= 1; // Shift indices down.
1175                    true // Keep this index.
1176                } else {
1177                    *i != index // Remove if it's the deleted index, keep otherwise.
1178                }
1179            });
1180        }
1181        Ok(())
1182    }
1183
1184    /// Mutates calldata of provided tx by abi decoding current values and randomly selecting the
1185    /// inputs to change.
1186    fn abi_mutate(
1187        &self,
1188        tx: &mut BasicTxDetails,
1189        function: &Function,
1190        test_runner: &mut TestRunner,
1191        fuzz_state: &impl FuzzStateReader,
1192    ) -> Result<()> {
1193        // Mutate value with 15% probability for payable functions.
1194        if function.state_mutability == alloy_json_abi::StateMutability::Payable
1195            && test_runner.rng().random_ratio(15, 100)
1196        {
1197            tx.call_details.value = Some(generate_msg_value(test_runner));
1198        }
1199
1200        // Mutate calldata.
1201        let mut arg_mutation_rounds =
1202            test_runner.rng().random_range(0..=function.inputs.len()).max(1);
1203        let round_arg_idx: Vec<usize> = if function.inputs.len() <= 1 {
1204            vec![0]
1205        } else {
1206            (0..arg_mutation_rounds)
1207                .map(|_| test_runner.rng().random_range(0..function.inputs.len()))
1208                .collect()
1209        };
1210        let mut prev_inputs = function
1211            .abi_decode_input(&tx.call_details.calldata[4..])
1212            .map_err(|err| eyre!("failed to load previous inputs: {err}"))?;
1213
1214        while arg_mutation_rounds > 0 {
1215            let idx = round_arg_idx[arg_mutation_rounds - 1];
1216            prev_inputs[idx] = mutate_param_value(
1217                &function
1218                    .inputs
1219                    .get(idx)
1220                    .expect("Could not get input to mutate")
1221                    .selector_type()
1222                    .parse()?,
1223                prev_inputs[idx].clone(),
1224                test_runner,
1225                fuzz_state,
1226            );
1227            arg_mutation_rounds -= 1;
1228        }
1229
1230        tx.call_details.calldata =
1231            function.abi_encode_input(&prev_inputs).map_err(|e| eyre!(e.to_string()))?.into();
1232        Ok(())
1233    }
1234
1235    /// Mutates calldata by replacing bytes matching one side of an observed EVM comparison with
1236    /// the other side, following LibAFL's input-to-state replacement strategy.
1237    fn cmp_mutate(
1238        tx: &mut BasicTxDetails,
1239        function: &Function,
1240        cmp_values: &[CmpOperands],
1241        test_runner: &mut TestRunner,
1242    ) -> Result<bool> {
1243        if cmp_values.is_empty() || tx.call_details.calldata.len() <= 4 {
1244            return Ok(false);
1245        }
1246
1247        let start = test_runner.rng().random_range(0..cmp_values.len());
1248        for offset in 0..cmp_values.len() {
1249            let cmp = &cmp_values[(start + offset) % cmp_values.len()];
1250            if let Some(mutated) =
1251                Self::cmp_mutated_calldata(tx.call_details.calldata.as_ref(), cmp, test_runner)
1252                && function.abi_decode_input(&mutated[4..]).is_ok()
1253            {
1254                tx.call_details.calldata = mutated.into();
1255                return Ok(true);
1256            }
1257        }
1258
1259        Ok(false)
1260    }
1261
1262    fn cmp_mutated_calldata(
1263        calldata: &[u8],
1264        cmp: &CmpOperands,
1265        test_runner: &mut TestRunner,
1266    ) -> Option<Vec<u8>> {
1267        const WIDTHS: [usize; 6] = [32, 16, 8, 4, 2, 1];
1268
1269        let lhs_full = cmp.op1.to_be_bytes::<32>();
1270        let rhs_full = cmp.op2.to_be_bytes::<32>();
1271        let width_start = test_runner.rng().random_range(0..WIDTHS.len());
1272        for offset in 0..WIDTHS.len() {
1273            let width = WIDTHS[(width_start + offset) % WIDTHS.len()];
1274            let lhs = &lhs_full[32 - width..];
1275            let rhs = &rhs_full[32 - width..];
1276            if lhs == rhs {
1277                continue;
1278            }
1279
1280            let lhs_first = test_runner.rng().random::<bool>();
1281            let first = if lhs_first { (lhs, rhs) } else { (rhs, lhs) };
1282            let second = if lhs_first { (rhs, lhs) } else { (lhs, rhs) };
1283
1284            if let Some(mutated) =
1285                Self::replace_cmp_operand(calldata, first.0, first.1, test_runner).or_else(|| {
1286                    Self::replace_cmp_operand(calldata, second.0, second.1, test_runner)
1287                })
1288            {
1289                return Some(mutated);
1290            }
1291        }
1292
1293        None
1294    }
1295
1296    fn replace_cmp_operand(
1297        calldata: &[u8],
1298        pattern: &[u8],
1299        replacement: &[u8],
1300        test_runner: &mut TestRunner,
1301    ) -> Option<Vec<u8>> {
1302        const SELECTOR_LEN: usize = 4;
1303
1304        if pattern.is_empty()
1305            || pattern.len() != replacement.len()
1306            || calldata.len() < SELECTOR_LEN + pattern.len()
1307            || (pattern.len() < 32 && pattern.iter().all(|&b| b == 0))
1308        {
1309            return None;
1310        }
1311
1312        let search_len = calldata.len() - SELECTOR_LEN - pattern.len() + 1;
1313        let start = test_runner.rng().random_range(0..search_len);
1314        for offset in 0..search_len {
1315            let idx = SELECTOR_LEN + ((start + offset) % search_len);
1316            if &calldata[idx..idx + pattern.len()] == pattern {
1317                let mut mutated = calldata.to_vec();
1318                mutated[idx..idx + replacement.len()].copy_from_slice(replacement);
1319                return Some(mutated);
1320            }
1321        }
1322
1323        None
1324    }
1325
1326    // Sync Methods.
1327
1328    /// Imports the new corpus entries from the `sync` directory.
1329    /// These contain tx sequences which are replayed and used to update the history map.
1330    fn load_sync_corpus(&self) -> Result<Vec<(CorpusDirEntry, Vec<BasicTxDetails>)>> {
1331        let Some(worker_dir) = &self.worker_dir else {
1332            return Ok(vec![]);
1333        };
1334
1335        let sync_dir = worker_dir.join(SYNC_DIR);
1336        if !sync_dir.is_dir() {
1337            return Ok(vec![]);
1338        }
1339
1340        let mut imports = vec![];
1341        for entry in read_corpus_dir(&sync_dir) {
1342            if entry.timestamp <= self.last_sync_timestamp {
1343                continue;
1344            }
1345            let tx_seq = entry.read_tx_seq()?;
1346            if tx_seq.is_empty() {
1347                warn!(target: "corpus", "skipping empty corpus entry: {}", entry.path.display());
1348                continue;
1349            }
1350            imports.push((entry, tx_seq));
1351        }
1352
1353        if !imports.is_empty() {
1354            debug!(target: "corpus", "imported {} new corpus entries", imports.len());
1355        }
1356
1357        Ok(imports)
1358    }
1359
1360    /// Syncs and calibrates the in memory corpus and updates the history_map if new coverage is
1361    /// found from the corpus findings of other workers.
1362    #[instrument(skip_all)]
1363    fn calibrate<FEN: FoundryEvmNetwork>(
1364        &mut self,
1365        executor: &Executor<FEN>,
1366        fuzzed_function: Option<&Function>,
1367        fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
1368        dynamic: Option<&DynamicTargetCtx<'_>>,
1369    ) -> Result<()> {
1370        let Some(worker_dir) = &self.worker_dir else {
1371            return Ok(());
1372        };
1373        let corpus_dir = worker_dir.join(CORPUS_DIR);
1374
1375        let mut executor = executor.clone();
1376        for (entry, tx_seq) in self.load_sync_corpus()? {
1377            let target = ReplayTarget { fuzzed_function, fuzzed_contracts, dynamic };
1378            let coverage = ReplayCoverage {
1379                history_map: &mut self.history_map,
1380                edge_indices: &mut self.edge_indices,
1381                sancov_history_map: &mut self.sancov_history_map,
1382                metrics: Some(&mut self.metrics),
1383            };
1384            let ReplayOutcome { keep_entry, new_coverage, cmp_seq, .. } =
1385                replay_corpus_sequence_with_executor(
1386                    &tx_seq,
1387                    &mut executor,
1388                    target,
1389                    coverage,
1390                    true,
1391                    false,
1392                )?;
1393
1394            let sync_path = &entry.path;
1395            if keep_entry && new_coverage {
1396                // Move file from sync/ to corpus/ directory.
1397                let corpus_path = corpus_dir.join(sync_path.components().next_back().unwrap());
1398                if let Err(err) = std::fs::rename(sync_path, &corpus_path) {
1399                    debug!(target: "corpus", %err, "failed to move synced corpus from {sync_path:?} to {corpus_path:?} dir");
1400                    continue;
1401                }
1402
1403                debug!(
1404                    target: "corpus",
1405                    name=%entry.name(),
1406                    "moved synced corpus to corpus dir",
1407                );
1408
1409                let corpus_entry = CorpusEntry::new_with_cmp(tx_seq.clone(), cmp_seq, entry.uuid);
1410                self.in_memory_corpus.push(corpus_entry);
1411            } else {
1412                // Remove the file as it did not generate new coverage.
1413                if let Err(err) = std::fs::remove_file(&entry.path) {
1414                    debug!(target: "corpus", %err, "failed to remove synced corpus from {sync_path:?}");
1415                    continue;
1416                }
1417                trace!(target: "corpus", "removed synced corpus from {sync_path:?}");
1418            }
1419        }
1420
1421        Ok(())
1422    }
1423
1424    /// Exports the new corpus entries to the master worker's sync dir.
1425    #[instrument(skip_all)]
1426    fn export_to_master(&self) -> Result<()> {
1427        // Master doesn't export (it only receives from others).
1428        assert_ne!(self.id, 0, "non-master only");
1429
1430        // Early return if no new entries or corpus dir not configured.
1431        if self.new_entry_indices.is_empty() || self.worker_dir.is_none() {
1432            return Ok(());
1433        }
1434
1435        let worker_dir = self.worker_dir.as_ref().unwrap();
1436        let Some(master_sync_dir) = self
1437            .config
1438            .corpus_dir
1439            .as_ref()
1440            .map(|dir| dir.join(format!("{WORKER}0")).join(SYNC_DIR))
1441        else {
1442            return Ok(());
1443        };
1444
1445        let mut exported = 0;
1446        let corpus_dir = worker_dir.join(CORPUS_DIR);
1447
1448        for &index in &self.new_entry_indices {
1449            let Some(corpus) = self.in_memory_corpus.get(index) else { continue };
1450            let file_name = corpus.file_name(self.config.corpus_gzip);
1451            let file_path = corpus_dir.join(&file_name);
1452            let sync_path = master_sync_dir.join(&file_name);
1453            if let Err(err) = std::fs::hard_link(&file_path, &sync_path) {
1454                debug!(target: "corpus", %err, "failed to export corpus {}", corpus.uuid);
1455                continue;
1456            }
1457            exported += 1;
1458        }
1459
1460        debug!(target: "corpus", "exported {exported} new corpus entries");
1461
1462        Ok(())
1463    }
1464
1465    /// Exports the global corpus to the `sync/` directories of all the non-master workers.
1466    #[instrument(skip_all)]
1467    fn export_to_workers(&mut self, num_workers: usize) -> Result<()> {
1468        assert_eq!(self.id, 0, "master worker only");
1469        if self.worker_dir.is_none() {
1470            return Ok(());
1471        }
1472
1473        let worker_dir = self.worker_dir.as_ref().unwrap();
1474        let master_corpus_dir = worker_dir.join(CORPUS_DIR);
1475        let filtered_master_corpus = read_corpus_dir(&master_corpus_dir)
1476            .filter(|entry| entry.timestamp > self.last_sync_timestamp)
1477            .collect::<Vec<_>>();
1478        let mut any_distributed = false;
1479        for target_worker in 1..num_workers {
1480            let target_dir = self
1481                .config
1482                .corpus_dir
1483                .as_ref()
1484                .unwrap()
1485                .join(format!("{WORKER}{target_worker}"))
1486                .join(SYNC_DIR);
1487
1488            if !target_dir.is_dir() {
1489                foundry_common::fs::create_dir_all(&target_dir)?;
1490            }
1491
1492            for entry in &filtered_master_corpus {
1493                let name = entry.name();
1494                let sync_path = target_dir.join(name);
1495                if let Err(err) = std::fs::hard_link(&entry.path, &sync_path) {
1496                    debug!(target: "corpus", %err, from=?entry.path, to=?sync_path, "failed to distribute corpus");
1497                    continue;
1498                }
1499                any_distributed = true;
1500                trace!(target: "corpus", %name, ?target_dir, "distributed corpus");
1501            }
1502        }
1503
1504        debug!(target: "corpus", %any_distributed, "distributed master corpus to all workers");
1505
1506        Ok(())
1507    }
1508
1509    // TODO(dani): currently only master syncs metrics?
1510    /// Syncs local metrics with global corpus metrics by calculating and applying deltas.
1511    pub(crate) fn sync_metrics(&mut self, global_corpus_metrics: &GlobalCorpusMetrics) {
1512        // Calculate delta metrics since last sync.
1513        let edges_delta = self
1514            .metrics
1515            .cumulative_edges_seen
1516            .saturating_sub(self.last_sync_metrics.cumulative_edges_seen);
1517        let features_delta = self
1518            .metrics
1519            .cumulative_features_seen
1520            .saturating_sub(self.last_sync_metrics.cumulative_features_seen);
1521        // For corpus count and favored items, calculate deltas.
1522        let corpus_count_delta =
1523            self.metrics.corpus_count as isize - self.last_sync_metrics.corpus_count as isize;
1524        let favored_delta =
1525            self.metrics.favored_items as isize - self.last_sync_metrics.favored_items as isize;
1526
1527        // Add delta values to global metrics.
1528
1529        if edges_delta > 0 {
1530            global_corpus_metrics.cumulative_edges_seen.fetch_add(edges_delta, Ordering::Relaxed);
1531        }
1532        if features_delta > 0 {
1533            global_corpus_metrics
1534                .cumulative_features_seen
1535                .fetch_add(features_delta, Ordering::Relaxed);
1536        }
1537
1538        if corpus_count_delta > 0 {
1539            global_corpus_metrics
1540                .corpus_count
1541                .fetch_add(corpus_count_delta as usize, Ordering::Relaxed);
1542        } else if corpus_count_delta < 0 {
1543            global_corpus_metrics
1544                .corpus_count
1545                .fetch_sub((-corpus_count_delta) as usize, Ordering::Relaxed);
1546        }
1547
1548        if favored_delta > 0 {
1549            global_corpus_metrics
1550                .favored_items
1551                .fetch_add(favored_delta as usize, Ordering::Relaxed);
1552        } else if favored_delta < 0 {
1553            global_corpus_metrics
1554                .favored_items
1555                .fetch_sub((-favored_delta) as usize, Ordering::Relaxed);
1556        }
1557
1558        // Store current metrics as last sync metrics for next delta calculation.
1559        self.last_sync_metrics = self.metrics.clone();
1560    }
1561
1562    /// Syncs the workers in_memory_corpus and history_map with the findings from other workers.
1563    #[instrument(skip_all)]
1564    pub fn sync<FEN: FoundryEvmNetwork>(
1565        &mut self,
1566        num_workers: usize,
1567        executor: &Executor<FEN>,
1568        fuzzed_function: Option<&Function>,
1569        fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
1570        dynamic: Option<&DynamicTargetCtx<'_>>,
1571        global_corpus_metrics: &GlobalCorpusMetrics,
1572    ) -> Result<()> {
1573        trace!(target: "corpus", "syncing");
1574
1575        self.sync_metrics(global_corpus_metrics);
1576
1577        self.calibrate(executor, fuzzed_function, fuzzed_contracts, dynamic)?;
1578        if self.id == 0 {
1579            self.export_to_workers(num_workers)?;
1580        } else {
1581            self.export_to_master()?;
1582        }
1583
1584        let last_sync = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
1585        self.last_sync_timestamp = last_sync;
1586
1587        self.new_entry_indices.clear();
1588
1589        debug!(target: "corpus", last_sync, "synced");
1590
1591        Ok(())
1592    }
1593
1594    /// Helper to check if a tx can be replayed.
1595    pub(crate) fn can_replay_tx(
1596        tx: &BasicTxDetails,
1597        fuzzed_function: Option<&Function>,
1598        fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
1599    ) -> bool {
1600        fuzzed_contracts.is_some_and(|contracts| contracts.targets().can_replay(tx))
1601            || fuzzed_function.is_some_and(|function| {
1602                tx.call_details
1603                    .calldata
1604                    .get(..4)
1605                    .is_some_and(|selector| function.selector() == selector)
1606            })
1607    }
1608}
1609
1610fn prepare_campaign_output_dir(config: &FuzzCorpusConfig) {
1611    let Some(root) = &config.corpus_dir else {
1612        return;
1613    };
1614    let corpus_dir = root.join(format!("{WORKER}0")).join(CORPUS_DIR);
1615    if let Err(err) = foundry_common::fs::create_dir_all(&corpus_dir) {
1616        debug!(target: "corpus", %err, "failed to create campaign corpus dir");
1617    }
1618}
1619
1620fn persist_campaign_entry(config: &FuzzCorpusConfig, entry: CampaignCorpusEntry) {
1621    let Some(root) = &config.corpus_dir else {
1622        return;
1623    };
1624    let corpus_dir = root.join(format!("{WORKER}0")).join(CORPUS_DIR);
1625    let corpus = CorpusEntry::new(entry.tx_seq);
1626    let write_result = corpus.write_to_disk_in(&corpus_dir, config.corpus_gzip);
1627    if let Err(err) = write_result {
1628        debug!(target: "corpus", %err, "failed to record call sequence {:?}", corpus.tx_seq);
1629    } else {
1630        trace!(
1631            target: "corpus",
1632            "persisted {} inputs for new coverage for {} corpus",
1633            corpus.tx_seq.len(),
1634            corpus.uuid,
1635        );
1636    }
1637}
1638
1639fn persist_optimization_output(
1640    config: &FuzzCorpusConfig,
1641    optimization_best: Option<(I256, &[BasicTxDetails])>,
1642) {
1643    let Some(root) = &config.corpus_dir else {
1644        return;
1645    };
1646    let Some((value, sequence)) = optimization_best else {
1647        return;
1648    };
1649    let state = OptimizationState { best_value: value, best_sequence: sequence.to_vec() };
1650    let path = root.join(OPTIMIZATION_BEST_FILE);
1651    if let Err(err) = foundry_common::fs::write_json_file(&path, &state) {
1652        debug!(target: "corpus", %err, "failed to persist optimization state");
1653    } else {
1654        trace!(
1655            target: "corpus",
1656            "persisted optimization best value {} with sequence len {}",
1657            value,
1658            sequence.len()
1659        );
1660    }
1661}
1662
1663fn has_legacy_invariant_corpus_dirs(path: &Path) -> bool {
1664    std::fs::read_dir(path).is_ok_and(|entries| {
1665        entries.flatten().any(|entry| {
1666            let path = entry.path();
1667            path.is_dir()
1668                && entry.file_name().to_str().is_some_and(|name| !name.starts_with(WORKER))
1669                && !path.join(OPTIMIZATION_BEST_FILE).is_file()
1670        })
1671    })
1672}
1673
1674fn unique_corpus_entries<'a>(
1675    replay_dirs: &'a [PathBuf],
1676    seen_entries: &'a mut HashSet<Uuid>,
1677) -> impl Iterator<Item = CorpusDirEntry> + 'a {
1678    replay_dirs.iter().flat_map(|replay_dir| read_corpus_dir(replay_dir)).filter(|entry| {
1679        let is_new = seen_entries.insert(entry.uuid);
1680        if !is_new {
1681            trace!(target: "corpus", "skipping duplicate corpus entry {}", entry.uuid);
1682        }
1683        is_new
1684    })
1685}
1686
1687#[cfg(test)]
1688mod tests {
1689    use super::*;
1690    use alloy_dyn_abi::DynSolValue;
1691    use alloy_primitives::U256;
1692    use std::fs;
1693
1694    fn basic_tx() -> BasicTxDetails {
1695        BasicTxDetails {
1696            warp: None,
1697            roll: None,
1698            sender: Address::ZERO,
1699            call_details: foundry_evm_fuzz::CallDetails {
1700                target: Address::ZERO,
1701                calldata: Bytes::new(),
1702                value: None,
1703            },
1704        }
1705    }
1706
1707    fn temp_corpus_dir() -> PathBuf {
1708        let dir = std::env::temp_dir().join(format!("foundry-corpus-tests-{}", Uuid::new_v4()));
1709        let _ = fs::create_dir_all(&dir);
1710        dir
1711    }
1712
1713    fn corpus_config(corpus_dir: PathBuf) -> FuzzCorpusConfig {
1714        FuzzCorpusConfig {
1715            corpus_dir: Some(corpus_dir),
1716            corpus_gzip: false,
1717            corpus_min_mutations: 0,
1718            corpus_min_size: 0,
1719            ..Default::default()
1720        }
1721    }
1722
1723    fn worker_corpus(id: usize, corpus_root: PathBuf, seed: WorkerCorpusSeed) -> WorkerCorpus {
1724        WorkerCorpus::from_seed(id, corpus_config(corpus_root), Just(basic_tx()).boxed(), seed)
1725    }
1726
1727    fn empty_worker_corpus(id: usize, corpus_root: PathBuf) -> WorkerCorpus {
1728        worker_corpus(id, corpus_root, WorkerCorpusSeed::default())
1729    }
1730
1731    fn seeded_worker_corpus(
1732        id: usize,
1733        corpus_root: PathBuf,
1734        entries: Vec<CorpusEntry>,
1735    ) -> WorkerCorpus {
1736        worker_corpus(
1737            id,
1738            corpus_root,
1739            WorkerCorpusSeed { in_memory_corpus: entries, ..Default::default() },
1740        )
1741    }
1742
1743    #[test]
1744    fn cmp_mutate_replaces_matching_calldata_operand() {
1745        let function = Function::parse("testCmp(uint256)").unwrap();
1746        let original = U256::from(7u64);
1747        let replacement = U256::from(42u64);
1748        let calldata: Bytes =
1749            function.abi_encode_input(&[DynSolValue::Uint(original, 256)]).unwrap().into();
1750        let mut tx = BasicTxDetails {
1751            warp: None,
1752            roll: None,
1753            sender: Address::ZERO,
1754            call_details: foundry_evm_fuzz::CallDetails {
1755                target: Address::ZERO,
1756                calldata,
1757                value: None,
1758            },
1759        };
1760        let cmp = CmpOperands {
1761            op1: original,
1762            op2: replacement,
1763            pc: 0,
1764            address: Address::ZERO,
1765            opcode: 0,
1766        };
1767        let config =
1768            proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
1769        let mut runner = TestRunner::new(config);
1770
1771        let mutated = WorkerCorpus::cmp_mutate(&mut tx, &function, &[cmp], &mut runner).unwrap();
1772
1773        assert!(mutated);
1774        let decoded = function.abi_decode_input(&tx.call_details.calldata[4..]).unwrap();
1775        assert_eq!(decoded[0].as_uint().unwrap().0, replacement);
1776    }
1777
1778    fn new_manager_with_single_corpus() -> (WorkerCorpus, Uuid) {
1779        let corpus = CorpusEntry::new(vec![basic_tx()]);
1780        let seed_uuid = corpus.uuid;
1781        let mut manager = seeded_worker_corpus(0, temp_corpus_dir(), vec![corpus]);
1782        manager.current_mutated = Some(seed_uuid);
1783
1784        (manager, seed_uuid)
1785    }
1786
1787    #[test]
1788    fn campaign_processing_returns_corpus_without_writing_worker_file() {
1789        let corpus_root = temp_corpus_dir();
1790        let worker_subdir = corpus_root.join("worker1");
1791        let mut manager = empty_worker_corpus(1, corpus_root);
1792
1793        let record = manager.process_inputs_for_campaign(&[basic_tx()], &[], true, None);
1794
1795        let record = record.unwrap();
1796        assert!(record.dedupe_by_coverage);
1797        assert_eq!(manager.in_memory_corpus.len(), 1);
1798        assert_eq!(manager.metrics.corpus_count, 1);
1799        assert_eq!(read_corpus_dir(&worker_subdir.join(CORPUS_DIR)).count(), 0);
1800    }
1801
1802    #[test]
1803    fn merged_campaign_outputs_write_corpus_and_optimization_to_master_dir() {
1804        let corpus_root = temp_corpus_dir();
1805        let mut manager = empty_worker_corpus(1, corpus_root.clone());
1806        let sequence = vec![basic_tx()];
1807        let record = manager
1808            .process_inputs_for_campaign(
1809                &sequence,
1810                &[],
1811                false,
1812                Some((I256::try_from(7).unwrap(), sequence.clone())),
1813            )
1814            .unwrap();
1815        let inputs = vec![record];
1816        WorkerCorpus::persist_campaign_outputs(
1817            &corpus_config(corpus_root.clone()),
1818            inputs,
1819            Some((I256::try_from(7).unwrap(), &sequence)),
1820        );
1821
1822        let master_corpus_dir = corpus_root.join("worker0").join(CORPUS_DIR);
1823        let entries = read_corpus_dir(&master_corpus_dir).collect::<Vec<_>>();
1824        assert_eq!(entries.len(), 1);
1825        let persisted_sequence = entries[0].read_tx_seq().unwrap();
1826        assert_eq!(persisted_sequence.len(), sequence.len());
1827        assert_eq!(persisted_sequence[0].sender, sequence[0].sender);
1828        assert_eq!(persisted_sequence[0].call_details.target, sequence[0].call_details.target);
1829        assert_eq!(persisted_sequence[0].call_details.calldata, sequence[0].call_details.calldata);
1830
1831        let state: OptimizationState =
1832            foundry_common::fs::read_json_file(&corpus_root.join(OPTIMIZATION_BEST_FILE)).unwrap();
1833        assert_eq!(state.best_value, I256::try_from(7).unwrap());
1834        assert_eq!(state.best_sequence.len(), sequence.len());
1835        assert_eq!(state.best_sequence[0].sender, sequence[0].sender);
1836        assert_eq!(state.best_sequence[0].call_details.target, sequence[0].call_details.target);
1837        assert_eq!(state.best_sequence[0].call_details.calldata, sequence[0].call_details.calldata);
1838    }
1839
1840    #[test]
1841    fn persisted_worker_corpus_entries_are_deduped_by_uuid() {
1842        let corpus_root = temp_corpus_dir();
1843        let corpus = CorpusEntry::new(vec![basic_tx()]);
1844        let duplicate = corpus.clone();
1845
1846        let worker0_corpus = corpus_root.join("worker0").join(CORPUS_DIR);
1847        let worker1_corpus = corpus_root.join("worker1").join(CORPUS_DIR);
1848        fs::create_dir_all(&worker0_corpus).unwrap();
1849        fs::create_dir_all(&worker1_corpus).unwrap();
1850        corpus.write_to_disk_in(&worker0_corpus, false).unwrap();
1851        duplicate.write_to_disk_in(&worker1_corpus, false).unwrap();
1852
1853        let mut seen = HashSet::new();
1854        let entries = unique_corpus_entries(&canonical_replay_dirs(&corpus_root), &mut seen)
1855            .collect::<Vec<_>>();
1856
1857        assert_eq!(entries.len(), 1);
1858        assert_eq!(entries[0].uuid, corpus.uuid);
1859    }
1860
1861    #[test]
1862    fn non_master_campaign_worker_uses_persisted_optimization_baseline() {
1863        let corpus_root = temp_corpus_dir();
1864        let persisted_sequence = vec![basic_tx()];
1865        let persisted_state = OptimizationState {
1866            best_value: I256::try_from(100).unwrap(),
1867            best_sequence: persisted_sequence,
1868        };
1869        foundry_common::fs::write_json_file(
1870            &corpus_root.join(OPTIMIZATION_BEST_FILE),
1871            &persisted_state,
1872        )
1873        .unwrap();
1874        let mut manager = WorkerCorpus::new::<foundry_evm_core::evm::EthEvmNetwork>(
1875            1,
1876            corpus_config(corpus_root),
1877            Just(basic_tx()).boxed(),
1878            None,
1879            None,
1880            None,
1881            None,
1882        )
1883        .unwrap();
1884
1885        let worse_sequence = vec![basic_tx()];
1886        let worse = manager.process_inputs_for_campaign(
1887            &worse_sequence,
1888            &[],
1889            false,
1890            Some((I256::try_from(50).unwrap(), worse_sequence.clone())),
1891        );
1892        assert!(worse.is_none());
1893
1894        let better_sequence = vec![basic_tx()];
1895        let better = manager.process_inputs_for_campaign(
1896            &better_sequence,
1897            &[],
1898            false,
1899            Some((I256::try_from(150).unwrap(), better_sequence.clone())),
1900        );
1901        assert!(better.is_some());
1902    }
1903
1904    #[test]
1905    fn worker_can_initialize_from_warmed_seed() {
1906        let corpus_root = temp_corpus_dir();
1907        let tx_seq = vec![basic_tx()];
1908        let seed = WorkerCorpusSeed {
1909            in_memory_corpus: vec![CorpusEntry::new(tx_seq.clone())],
1910            history_map: vec![1, 2, 3],
1911            edge_indices: EdgeIndexMap::default(),
1912            sancov_history_map: vec![4, 5],
1913            metrics: CorpusMetrics {
1914                cumulative_edges_seen: 7,
1915                cumulative_features_seen: 11,
1916                corpus_count: 1,
1917                favored_items: 0,
1918            },
1919            failed_replays: 13,
1920            optimization_best_value: Some(I256::try_from(17).unwrap()),
1921            optimization_best_sequence: tx_seq,
1922        };
1923
1924        let manager =
1925            WorkerCorpus::from_seed(1, corpus_config(corpus_root), Just(basic_tx()).boxed(), seed);
1926
1927        assert_eq!(manager.in_memory_corpus.len(), 1);
1928        assert_eq!(manager.history_map, vec![1, 2, 3]);
1929        assert_eq!(manager.sancov_history_map, vec![4, 5]);
1930        assert_eq!(manager.metrics.cumulative_edges_seen, 7);
1931        assert_eq!(manager.metrics.cumulative_features_seen, 11);
1932        assert_eq!(manager.metrics.corpus_count, 1);
1933        assert_eq!(manager.failed_replays, 13);
1934        let (value, sequence) = manager.optimization_initial_state();
1935        assert_eq!(value, Some(I256::try_from(17).unwrap()));
1936        assert_eq!(sequence.len(), 1);
1937    }
1938
1939    #[test]
1940    fn clone_for_worker_shards_warmed_corpus_and_recomputes_metrics() {
1941        let entries = (0..10)
1942            .map(|idx| {
1943                let mut entry = CorpusEntry::new(vec![basic_tx()]);
1944                entry.is_favored = idx % 2 == 0;
1945                entry
1946            })
1947            .collect::<Vec<_>>();
1948        let entry_ids = entries.iter().map(|entry| entry.uuid).collect::<Vec<_>>();
1949        let seed = WorkerCorpusSeed {
1950            in_memory_corpus: entries,
1951            history_map: vec![1, 2, 3],
1952            edge_indices: EdgeIndexMap::default(),
1953            sancov_history_map: vec![4, 5],
1954            metrics: CorpusMetrics {
1955                cumulative_edges_seen: 7,
1956                cumulative_features_seen: 11,
1957                corpus_count: 10,
1958                favored_items: 5,
1959            },
1960            failed_replays: 13,
1961            optimization_best_value: Some(I256::try_from(17).unwrap()),
1962            optimization_best_sequence: vec![basic_tx()],
1963        };
1964
1965        let worker_count = 3;
1966        let shards = (0..worker_count)
1967            .map(|worker_id| seed.clone_for_worker(worker_id, worker_count))
1968            .collect::<Vec<_>>();
1969        let mut sharded_ids = shards
1970            .iter()
1971            .flat_map(|shard| shard.in_memory_corpus.iter().map(|entry| entry.uuid))
1972            .collect::<Vec<_>>();
1973        let mut expected_ids = entry_ids.clone();
1974        sharded_ids.sort_unstable();
1975        expected_ids.sort_unstable();
1976
1977        assert_eq!(sharded_ids, expected_ids);
1978        assert_eq!(
1979            shards[0].in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
1980            [entry_ids[0], entry_ids[3], entry_ids[6], entry_ids[9]]
1981        );
1982        assert_eq!(
1983            shards[1].in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
1984            [entry_ids[1], entry_ids[4], entry_ids[7]]
1985        );
1986        assert_eq!(
1987            shards[2].in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
1988            [entry_ids[2], entry_ids[5], entry_ids[8]]
1989        );
1990        assert_eq!(
1991            shards.iter().map(|shard| shard.in_memory_corpus.len()).collect::<Vec<_>>(),
1992            [4, 3, 3]
1993        );
1994        assert_eq!(
1995            shards.iter().map(|shard| shard.metrics.corpus_count).collect::<Vec<_>>(),
1996            [4, 3, 3]
1997        );
1998        assert_eq!(
1999            shards.iter().map(|shard| shard.metrics.favored_items).collect::<Vec<_>>(),
2000            [2, 1, 2]
2001        );
2002        assert!(shards.iter().all(|shard| shard.history_map == seed.history_map));
2003        assert!(shards.iter().all(|shard| shard.sancov_history_map == seed.sancov_history_map));
2004        assert!(shards.iter().all(|shard| shard.metrics.cumulative_edges_seen == 7));
2005        assert!(shards.iter().all(|shard| shard.metrics.cumulative_features_seen == 11));
2006    }
2007
2008    #[test]
2009    fn detects_legacy_invariant_corpus_dirs_without_matching_worker_dirs() {
2010        let corpus_root = temp_corpus_dir();
2011        fs::create_dir_all(corpus_root.join("worker0")).unwrap();
2012        assert!(!has_legacy_invariant_corpus_dirs(&corpus_root));
2013
2014        fs::create_dir_all(corpus_root.join("invariant_a")).unwrap();
2015        assert!(has_legacy_invariant_corpus_dirs(&corpus_root));
2016    }
2017
2018    #[test]
2019    fn ignores_optimization_invariant_corpus_dirs_when_detecting_legacy_dirs() {
2020        let corpus_root = temp_corpus_dir();
2021        fs::create_dir_all(corpus_root.join("worker0")).unwrap();
2022        let optimization_dir = corpus_root.join("invariant_optimize");
2023        fs::create_dir_all(optimization_dir.join("worker0")).unwrap();
2024        fs::write(optimization_dir.join(OPTIMIZATION_BEST_FILE), "{}").unwrap();
2025
2026        assert!(!has_legacy_invariant_corpus_dirs(&corpus_root));
2027
2028        fs::create_dir_all(corpus_root.join("invariant_legacy").join("worker0")).unwrap();
2029        assert!(has_legacy_invariant_corpus_dirs(&corpus_root));
2030    }
2031
2032    #[test]
2033    fn favored_sets_true_and_metrics_increment_when_ratio_gt_threshold() {
2034        let (mut manager, uuid) = new_manager_with_single_corpus();
2035        let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
2036        corpus.total_mutations = 4;
2037        corpus.new_finds_produced = 2; // ratio currently 0.5 if both increment → 3/5 = 0.6 > 0.3.
2038        corpus.is_favored = false;
2039
2040        // Ensure metrics start at 0.
2041        assert_eq!(manager.metrics.favored_items, 0);
2042
2043        // Mark this as the currently mutated corpus and process a run with new coverage.
2044        manager.current_mutated = Some(uuid);
2045        manager.process_inputs(&[basic_tx()], &[], true, None);
2046
2047        let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
2048        assert!(corpus.is_favored, "expected favored to be true when ratio > threshold");
2049        assert_eq!(
2050            manager.metrics.favored_items, 1,
2051            "favored_items should increment on false→true"
2052        );
2053    }
2054
2055    #[test]
2056    fn favored_sets_false_and_metrics_decrement_when_ratio_lt_threshold() {
2057        let (mut manager, uuid) = new_manager_with_single_corpus();
2058        let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
2059        corpus.total_mutations = 9;
2060        corpus.new_finds_produced = 3; // 3/9 = 0.333.. > 0.3; after +1: 3/10 = 0.3 => not favored.
2061        corpus.is_favored = true; // Start as favored.
2062
2063        manager.metrics.favored_items = 1;
2064
2065        // Next run does NOT produce coverage → only total_mutations increments, ratio drops.
2066        manager.current_mutated = Some(uuid);
2067        manager.process_inputs(&[basic_tx()], &[], false, None);
2068
2069        let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
2070        assert!(!corpus.is_favored, "expected favored to be false when ratio < threshold");
2071        assert_eq!(
2072            manager.metrics.favored_items, 0,
2073            "favored_items should decrement on true→false"
2074        );
2075    }
2076
2077    #[test]
2078    fn favored_is_false_on_ratio_equal_threshold() {
2079        let (mut manager, uuid) = new_manager_with_single_corpus();
2080        let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
2081        // After this call with new_coverage=true, totals become 10 and 3 → 0.3.
2082        corpus.total_mutations = 9;
2083        corpus.new_finds_produced = 2;
2084        corpus.is_favored = false;
2085
2086        manager.current_mutated = Some(uuid);
2087        manager.process_inputs(&[basic_tx()], &[], true, None);
2088
2089        let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
2090        assert!(
2091            !(corpus.is_favored),
2092            "with strict '>' comparison, favored must be false when ratio == threshold"
2093        );
2094    }
2095
2096    #[test]
2097    fn eviction_skips_favored_and_evicts_non_favored() {
2098        // Manager with two corpora.
2099        let mut favored = CorpusEntry::new(vec![basic_tx()]);
2100        favored.total_mutations = 2;
2101        favored.is_favored = true;
2102
2103        let mut non_favored = CorpusEntry::new(vec![basic_tx()]);
2104        non_favored.total_mutations = 2;
2105        non_favored.is_favored = false;
2106        let non_favored_uuid = non_favored.uuid;
2107
2108        let mut manager = seeded_worker_corpus(0, temp_corpus_dir(), vec![favored, non_favored]);
2109
2110        // First eviction should remove the non-favored one.
2111        manager.evict_oldest_corpus().unwrap();
2112        assert_eq!(manager.in_memory_corpus.len(), 1);
2113        assert!(manager.in_memory_corpus.iter().all(|c| c.is_favored));
2114
2115        // Attempt eviction again: only favored remains → should not remove.
2116        manager.evict_oldest_corpus().unwrap();
2117        assert_eq!(manager.in_memory_corpus.len(), 1, "favored corpus must not be evicted");
2118
2119        // Ensure the evicted one was the non-favored uuid.
2120        assert!(manager.in_memory_corpus.iter().all(|c| c.uuid != non_favored_uuid));
2121    }
2122}