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::{
38    CorpusDirEntry, canonical_replay_dirs, read_corpus_dir, read_corpus_dir_strict,
39};
40use crate::{
41    executors::{
42        Executor, RawCallResult,
43        campaign::{CampaignCallKind, execute_invariant_replay_tx},
44        invariant::execute_tx,
45    },
46    inspectors::{CmpOperands, EdgeIndexMap, MAX_EDGE_COUNT},
47};
48use alloy_dyn_abi::JsonAbiExt;
49use alloy_json_abi::Function;
50use alloy_primitives::{Address, Bytes, I256, U256};
51use alloy_sol_types::SolCall;
52use eyre::{Result, eyre};
53use foundry_cheatcodes::Vm::{
54    revertToAndDeleteCall, revertToCall, revertToStateAndDeleteCall, revertToStateCall, rollCall,
55    setEvmVersionCall, warpCall,
56};
57use foundry_common::{ContractsByAddress, ContractsByArtifact, TestFunctionExt, sh_warn};
58use foundry_config::FuzzCorpusConfig;
59use foundry_evm_core::{
60    constants::{CALLER, CHEATCODE_ADDRESS},
61    evm::FoundryEvmNetwork,
62    utils::StateChangeset,
63};
64use foundry_evm_fuzz::{
65    BasicTxDetails, CallDetails, ObservedCall,
66    invariant::{
67        ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, SenderFilters,
68        TargetedContracts,
69    },
70    sequence::{ComparisonHint, CorpusEntryView, SequenceGenerator, SequencePlan},
71};
72use proptest::test_runner::TestRunner;
73use revm::{context::Block, primitives::hardfork::SpecId};
74use serde::{Deserialize, Serialize};
75use std::{
76    collections::HashSet,
77    fmt,
78    path::{Path, PathBuf},
79    sync::{
80        Arc,
81        atomic::{AtomicBool, AtomicUsize, Ordering},
82    },
83    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
84};
85use uuid::Uuid;
86
87const WORKER: &str = "worker";
88const CORPUS_DIR: &str = "corpus";
89const SYNC_DIR: &str = "sync";
90const OPTIMIZATION_BEST_FILE: &str = "optimization_best.json";
91
92const FAVORABILITY_THRESHOLD: f64 = 0.3;
93
94/// Threshold for compressing corpus entries.
95/// 4KiB is usually the minimum file size on popular file systems.
96const GZIP_THRESHOLD: usize = 4 * 1024;
97
98/// Persisted optimization state: the best value found and the sequence that produced it.
99#[derive(Clone, Serialize, Deserialize)]
100struct OptimizationState {
101    best_value: I256,
102    best_sequence: Vec<BasicTxDetails>,
103}
104
105/// Holds Corpus information.
106#[derive(Clone, Serialize)]
107struct CorpusEntry {
108    // Unique corpus identifier.
109    uuid: Uuid,
110    // Total mutations of corpus as primary source.
111    total_mutations: usize,
112    // New coverage found as a result of mutating this corpus.
113    new_finds_produced: usize,
114    // Corpus call sequence.
115    #[serde(skip_serializing)]
116    tx_seq: Vec<BasicTxDetails>,
117    // Per-call EVM comparison operands observed while executing this corpus entry.
118    // Parallel to `tx_seq`. Empty inner vec means "no cmp data for this call".
119    #[serde(skip_serializing)]
120    cmp_seq: Vec<Vec<ComparisonHint>>,
121    // Whether this corpus is favored, i.e. producing new finds more often than
122    // `FAVORABILITY_THRESHOLD`.
123    is_favored: bool,
124    /// Timestamp of when this entry was written to disk in seconds.
125    #[serde(skip_serializing)]
126    timestamp: u64,
127    /// Original filename for an entry imported from another worker.
128    #[serde(skip_serializing)]
129    persisted_file_name: Option<String>,
130}
131
132impl CorpusEntry {
133    /// Creates a corpus entry with a new UUID.
134    pub fn new(tx_seq: Vec<BasicTxDetails>) -> Self {
135        Self::new_with_cmp(tx_seq, Vec::new(), Uuid::new_v4())
136    }
137
138    /// Creates a corpus entry with the given UUID and per-call cmp operand log.
139    pub fn new_with_cmp(
140        tx_seq: Vec<BasicTxDetails>,
141        cmp_seq: Vec<Vec<ComparisonHint>>,
142        uuid: Uuid,
143    ) -> Self {
144        Self {
145            uuid,
146            total_mutations: 0,
147            new_finds_produced: 0,
148            tx_seq,
149            cmp_seq,
150            is_favored: false,
151            timestamp: SystemTime::now()
152                .duration_since(UNIX_EPOCH)
153                .expect("time went backwards")
154                .as_secs(),
155            persisted_file_name: None,
156        }
157    }
158
159    fn write_to_disk_in(&self, dir: &Path, can_gzip: bool) -> foundry_common::fs::Result<PathBuf> {
160        let should_gzip = self.should_gzip(can_gzip);
161        let file_name = self.file_name(should_gzip);
162        let path = dir.join(&file_name);
163        let temp_path = dir.join(format!(".{file_name}.{}.tmp", Uuid::new_v4()));
164
165        let write_result = if should_gzip {
166            foundry_common::fs::write_json_gzip_file(&temp_path, &self.tx_seq)
167        } else {
168            foundry_common::fs::write_json_file(&temp_path, &self.tx_seq)
169        };
170        if let Err(err) = write_result {
171            let _ = foundry_common::fs::remove_file(&temp_path);
172            return Err(err);
173        }
174
175        if let Err(err) = std::fs::rename(&temp_path, &path) {
176            let _ = foundry_common::fs::remove_file(&temp_path);
177            return Err(foundry_common::errors::FsPathError::write(err, &path));
178        }
179
180        Ok(path)
181    }
182
183    fn file_name(&self, gzip: bool) -> String {
184        if let Some(name) = &self.persisted_file_name {
185            return name.clone();
186        }
187        let ext = if gzip { ".json.gz" } else { ".json" };
188        format!("{}-{}{ext}", self.uuid, self.timestamp)
189    }
190
191    fn should_gzip(&self, can_gzip: bool) -> bool {
192        if !can_gzip {
193            return false;
194        }
195        let size: usize = self.tx_seq.iter().map(|tx| tx.estimate_serialized_size()).sum();
196        size > GZIP_THRESHOLD
197    }
198}
199
200/// Persists one call sequence as a corpus seed in the canonical worker0 corpus directory.
201pub fn persist_corpus_seed(
202    config: &FuzzCorpusConfig,
203    tx_seq: Vec<BasicTxDetails>,
204) -> foundry_common::fs::Result<Option<PathBuf>> {
205    let Some(root) = &config.corpus_dir else {
206        return Ok(None);
207    };
208    for dir in canonical_replay_dirs(root) {
209        for entry in read_corpus_dir(&dir) {
210            match entry.read_tx_seq() {
211                Ok(existing) if same_tx_sequence(&existing, &tx_seq) => {
212                    return Ok(Some(entry.path));
213                }
214                Ok(_) => {}
215                Err(err) => debug!(%err, path = ?entry.path, "failed to read corpus seed"),
216            }
217        }
218    }
219    let corpus_dir = root.join(format!("{WORKER}0")).join(CORPUS_DIR);
220    foundry_common::fs::create_dir_all(&corpus_dir)?;
221    CorpusEntry::new(tx_seq).write_to_disk_in(&corpus_dir, config.corpus_gzip).map(Some)
222}
223
224fn same_tx_sequence(left: &[BasicTxDetails], right: &[BasicTxDetails]) -> bool {
225    left.len() == right.len()
226        && left.iter().zip(right).all(|(left, right)| {
227            left.warp == right.warp
228                && left.roll == right.roll
229                && left.sender == right.sender
230                && left.call_details.target == right.call_details.target
231                && left.call_details.calldata == right.call_details.calldata
232                && left.call_details.value == right.call_details.value
233        })
234}
235
236fn link_corpus_file(from: &Path, to: &Path) -> bool {
237    match std::fs::hard_link(from, to) {
238        Ok(()) => true,
239        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
240            if !std::fs::symlink_metadata(to).is_ok_and(|metadata| metadata.file_type().is_file()) {
241                debug!(target: "corpus", from=?from, to=?to, "corpus destination is not a regular file");
242                return false;
243            }
244            let entry = |path: &Path| CorpusDirEntry {
245                path: path.to_path_buf(),
246                uuid: Uuid::nil(),
247                timestamp: 0,
248            };
249            let matches = entry(from)
250                .read_tx_seq()
251                .ok()
252                .zip(entry(to).read_tx_seq().ok())
253                .is_some_and(|(from, to)| same_tx_sequence(&from, &to));
254            if !matches {
255                debug!(target: "corpus", from=?from, to=?to, "conflicting corpus file already exists");
256            }
257            matches
258        }
259        Err(err) => {
260            debug!(target: "corpus", %err, from=?from, to=?to, "failed to link corpus file");
261            false
262        }
263    }
264}
265
266fn accept_synced_corpus_file(
267    entry: &CorpusDirEntry,
268    tx_seq: &[BasicTxDetails],
269    corpus_path: &Path,
270) -> bool {
271    if corpus_path.is_file() {
272        let existing = CorpusDirEntry {
273            path: corpus_path.to_path_buf(),
274            uuid: entry.uuid,
275            timestamp: entry.timestamp,
276        };
277        if existing.read_tx_seq().is_ok_and(|existing| same_tx_sequence(&existing, tx_seq)) {
278            if let Err(err) = std::fs::remove_file(&entry.path) {
279                debug!(target: "corpus", %err, "failed to remove synced corpus link {}", entry.path.display());
280                return false;
281            }
282            return true;
283        }
284
285        warn!(target: "corpus", "not overwriting conflicting corpus file {}", corpus_path.display());
286        let quarantine_path =
287            entry.path.with_file_name(format!("{}.{}.invalid", entry.name(), Uuid::new_v4()));
288        if let Err(err) = std::fs::rename(&entry.path, &quarantine_path) {
289            debug!(target: "corpus", %err, "failed to quarantine conflicting corpus file {}", entry.path.display());
290        }
291        return false;
292    }
293
294    if let Err(err) = std::fs::rename(&entry.path, corpus_path) {
295        debug!(target: "corpus", %err, "failed to move synced corpus from {:?} to {corpus_path:?} dir", entry.path);
296        return false;
297    }
298    true
299}
300
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub(crate) enum CorpusInsertionMode {
303    Live,
304    MemoryOnly,
305}
306
307struct ReplayOutcome {
308    keep_entry: bool,
309    new_coverage: bool,
310    /// Whether replay hit a first-time edge (advances the per-worker "time since new edge" timer).
311    new_edge: bool,
312    cmp_seq: Vec<Vec<ComparisonHint>>,
313    failed_replays: usize,
314}
315
316#[derive(Clone, Copy)]
317pub struct StatelessReplayTarget<'a> {
318    pub function: &'a Function,
319    pub address: Address,
320}
321
322impl StatelessReplayTarget<'_> {
323    fn can_replay(self, tx: &BasicTxDetails) -> bool {
324        tx.call_details.target == self.address
325            && tx
326                .call_details
327                .calldata
328                .get(..4)
329                .is_some_and(|selector| self.function.selector() == selector)
330    }
331}
332
333#[derive(Clone, Copy)]
334pub(crate) struct ReplayTarget<'a> {
335    pub(crate) stateless: Option<StatelessReplayTarget<'a>>,
336    pub(crate) fuzzed_contracts: Option<&'a FuzzRunIdentifiedContracts>,
337    pub(crate) dynamic: Option<&'a DynamicTargetCtx<'a>>,
338    /// Current invariant sender policy; persisted transactions from other senders are rejected.
339    pub(crate) senders: Option<&'a SenderFilters>,
340}
341
342struct ReplayCoverage<'a> {
343    history_map: &'a mut Vec<u8>,
344    edge_indices: &'a mut EdgeIndexMap,
345    sancov_history_map: &'a mut Vec<u8>,
346    metrics: Option<&'a mut CorpusMetrics>,
347}
348
349/// Campaign-level corpus state produced by replaying persisted corpus entries once.
350///
351/// Parallel invariant workers clone this seed so every worker starts with the same warmed corpus
352/// and coverage maps. That avoids each worker rediscovering persisted coverage relative to an empty
353/// local map.
354#[derive(Clone, Default)]
355pub(crate) struct WorkerCorpusSeed {
356    in_memory_corpus: Vec<CorpusEntry>,
357    history_map: Vec<u8>,
358    edge_indices: EdgeIndexMap,
359    sancov_history_map: Vec<u8>,
360    metrics: CorpusMetrics,
361    replay_dirs: Option<Vec<PathBuf>>,
362    failed_replays: usize,
363    pub(crate) optimization_best_value: Option<I256>,
364    pub(crate) optimization_best_sequence: Vec<BasicTxDetails>,
365    /// Set if persisted-corpus replay hit a first-time edge, so the timer starts at the baseline
366    /// load instead of reading "never" while `cumulative_edges_seen` is non-zero.
367    last_new_edge_at: Option<Instant>,
368}
369
370impl WorkerCorpusSeed {
371    fn empty(config: &FuzzCorpusConfig) -> Self {
372        // Hash mode always merges a fixed `MAX_EDGE_COUNT` bitmap, so preallocate to avoid moving
373        // the one-time 64 KiB resize into the first merge. Collision-free and sancov maps grow on
374        // demand and start empty.
375        let history_map =
376            if config.collect_evm_edge_coverage() && !config.evm_edge_coverage_collision_free() {
377                vec![0u8; MAX_EDGE_COUNT]
378            } else {
379                Vec::new()
380            };
381        Self { history_map, ..Default::default() }
382    }
383
384    fn with_optimization_state(mut self, config: &FuzzCorpusConfig) -> Self {
385        if let Some((value, sequence)) = load_optimization_state(config) {
386            self.optimization_best_value = Some(value);
387            self.optimization_best_sequence = sequence;
388        }
389        self
390    }
391
392    pub(crate) fn clone_for_worker(
393        &self,
394        worker_id: usize,
395        worker_count: usize,
396        include_cmp_seq: bool,
397    ) -> Self {
398        let in_memory_corpus = self
399            .in_memory_corpus
400            .iter()
401            .enumerate()
402            .filter(|(idx, _)| idx % worker_count == worker_id)
403            .map(|(_, entry)| {
404                let mut entry = entry.clone();
405                if !include_cmp_seq {
406                    entry.cmp_seq.clear();
407                }
408                entry
409            })
410            .collect::<Vec<_>>();
411
412        let mut metrics = self.metrics.clone();
413        metrics.corpus_count = in_memory_corpus.len();
414        metrics.favored_items = in_memory_corpus.iter().filter(|entry| entry.is_favored).count();
415
416        Self {
417            in_memory_corpus,
418            history_map: self.history_map.clone(),
419            edge_indices: self.edge_indices.clone(),
420            sancov_history_map: self.sancov_history_map.clone(),
421            metrics,
422            replay_dirs: self.replay_dirs.clone(),
423            failed_replays: self.failed_replays,
424            optimization_best_value: self.optimization_best_value,
425            optimization_best_sequence: self.optimization_best_sequence.clone(),
426            last_new_edge_at: self.last_new_edge_at,
427        }
428    }
429
430    pub(crate) fn retain_replayable(&mut self, targeted_contracts: &TargetedContracts) {
431        let is_replayable =
432            |tx_seq: &[BasicTxDetails]| tx_seq.iter().all(|tx| targeted_contracts.can_replay(tx));
433        self.in_memory_corpus.retain(|entry| is_replayable(&entry.tx_seq));
434        self.metrics.corpus_count = self.in_memory_corpus.len();
435        self.metrics.favored_items =
436            self.in_memory_corpus.iter().filter(|entry| entry.is_favored).count();
437
438        if !self.optimization_best_sequence.is_empty()
439            && !is_replayable(&self.optimization_best_sequence)
440        {
441            self.optimization_best_value = None;
442            self.optimization_best_sequence.clear();
443        }
444    }
445
446    pub(crate) fn load_from_disk<FEN: FoundryEvmNetwork>(
447        config: &FuzzCorpusConfig,
448        replay_root: Option<&Path>,
449        executor: Option<&Executor<FEN>>,
450        target: ReplayTarget<'_>,
451    ) -> Result<Self> {
452        let mut seed = Self::empty(config).with_optimization_state(config);
453        let Some(corpus_dir) = &config.corpus_dir else {
454            return Ok(seed);
455        };
456        let replay_dirs = canonical_replay_dirs(replay_root.unwrap_or(corpus_dir));
457        seed.replay_dirs = Some(replay_dirs.clone());
458
459        // Drop a persisted optimization seed with a sender the current policy no longer allows.
460        if let Some(senders) = target.senders
461            && !seed.optimization_best_sequence.iter().all(|tx| senders.allows(tx.sender))
462        {
463            seed.optimization_best_value = None;
464            seed.optimization_best_sequence.clear();
465        }
466
467        // Seed in-memory corpus with the persisted optimization best sequence so the mutation
468        // engine can build on it in future runs.
469        if !seed.optimization_best_sequence.is_empty() {
470            seed.in_memory_corpus.push(CorpusEntry::new(seed.optimization_best_sequence.clone()));
471            seed.metrics.corpus_count += 1;
472        }
473
474        if target.fuzzed_contracts.is_some() && has_legacy_invariant_corpus_dirs(corpus_dir) {
475            let _ = sh_warn!(
476                "Ignoring legacy invariant corpus directories under {}; new corpus entries are persisted under the contract-level corpus directory.",
477                corpus_dir.display(),
478            );
479        }
480
481        let Some(executor) = executor else {
482            return Ok(seed);
483        };
484        let mut seen_entries =
485            seed.in_memory_corpus.iter().map(|entry| entry.uuid).collect::<HashSet<_>>();
486        for entry in unique_corpus_entries(&replay_dirs, &mut seen_entries) {
487            // A corrupt or truncated corpus file must not abort the whole campaign startup: skip
488            // it and keep loading the rest of the corpus. Canonical entries are atomically
489            // published, but malformed files may come from older versions or manual edits.
490            let tx_seq = match entry.read_tx_seq() {
491                Ok(tx_seq) => tx_seq,
492                Err(err) => {
493                    let _ =
494                        sh_warn!("Skipping unreadable corpus file {}: {err}", entry.path.display());
495                    continue;
496                }
497            };
498            if tx_seq.is_empty() {
499                continue;
500            }
501
502            let coverage = ReplayCoverage {
503                history_map: &mut seed.history_map,
504                edge_indices: &mut seed.edge_indices,
505                sancov_history_map: &mut seed.sancov_history_map,
506                metrics: Some(&mut seed.metrics),
507            };
508            let ReplayOutcome { keep_entry, new_edge, cmp_seq, failed_replays, .. } =
509                replay_corpus_sequence(&tx_seq, executor, target, coverage)?;
510            seed.failed_replays += failed_replays;
511            // Start the timer at the baseline load if replay hit a first-time edge.
512            if new_edge {
513                seed.last_new_edge_at = Some(Instant::now());
514            }
515            if !keep_entry {
516                continue;
517            }
518
519            seed.metrics.corpus_count += 1;
520            debug!(
521                target: "corpus",
522                "load sequence with len {} from corpus file {}",
523                tx_seq.len(),
524                entry.path.display()
525            );
526            seed.in_memory_corpus.push(CorpusEntry::new_with_cmp(tx_seq, cmp_seq, entry.uuid));
527        }
528
529        Ok(seed)
530    }
531}
532
533#[derive(Default)]
534pub(crate) struct GlobalCorpusMetrics {
535    // Number of edges seen during the invariant run.
536    cumulative_edges_seen: AtomicUsize,
537    // Number of features (new hitcount bin of previously hit edge) seen during the invariant run.
538    cumulative_features_seen: AtomicUsize,
539    // Number of corpus entries.
540    corpus_count: AtomicUsize,
541    // Number of corpus entries that are favored.
542    favored_items: AtomicUsize,
543}
544
545pub(crate) struct CorpusSyncCoordinator {
546    workers: usize,
547    arrived: AtomicUsize,
548    phase: AtomicUsize,
549    aborted: AtomicBool,
550}
551
552impl CorpusSyncCoordinator {
553    pub(crate) const fn new(workers: usize) -> Self {
554        Self {
555            workers,
556            arrived: AtomicUsize::new(0),
557            phase: AtomicUsize::new(0),
558            aborted: AtomicBool::new(false),
559        }
560    }
561
562    pub(crate) fn abort(&self) {
563        self.aborted.store(true, Ordering::Release);
564    }
565
566    fn wait(&self) -> bool {
567        if self.aborted.load(Ordering::Acquire) {
568            return false;
569        }
570
571        let phase = self.phase.load(Ordering::Acquire);
572        if self.arrived.fetch_add(1, Ordering::AcqRel) + 1 == self.workers {
573            self.arrived.store(0, Ordering::Release);
574            self.phase.fetch_add(1, Ordering::AcqRel);
575            return true;
576        }
577
578        while self.phase.load(Ordering::Acquire) == phase && !self.aborted.load(Ordering::Acquire) {
579            rayon::yield_now();
580        }
581        !self.aborted.load(Ordering::Acquire)
582    }
583}
584
585impl fmt::Display for GlobalCorpusMetrics {
586    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587        self.load().fmt(f)
588    }
589}
590
591impl GlobalCorpusMetrics {
592    pub(crate) fn load(&self) -> CorpusMetrics {
593        CorpusMetrics {
594            cumulative_edges_seen: self.cumulative_edges_seen.load(Ordering::Relaxed),
595            cumulative_features_seen: self.cumulative_features_seen.load(Ordering::Relaxed),
596            corpus_count: self.corpus_count.load(Ordering::Relaxed),
597            favored_items: self.favored_items.load(Ordering::Relaxed),
598        }
599    }
600}
601
602#[derive(Serialize, Default, Clone)]
603pub(crate) struct CorpusMetrics {
604    // Number of edges seen during the invariant run.
605    cumulative_edges_seen: usize,
606    // Number of features (new hitcount bin of previously hit edge) seen during the invariant run.
607    cumulative_features_seen: usize,
608    // Number of corpus entries.
609    corpus_count: usize,
610    // Number of corpus entries that are favored.
611    favored_items: usize,
612}
613
614impl fmt::Display for CorpusMetrics {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        writeln!(f)?;
617        writeln!(f, "      Edge coverage metrics:")?;
618        writeln!(f, "        - cumulative edges seen: {}", self.cumulative_edges_seen)?;
619        writeln!(f, "        - cumulative features seen: {}", self.cumulative_features_seen)?;
620        writeln!(f, "        - corpus count: {}", self.corpus_count)?;
621        write!(f, "        - favored items: {}", self.favored_items)?;
622        Ok(())
623    }
624}
625
626impl CorpusMetrics {
627    /// Records number of new edges or features explored during the campaign.
628    pub const fn update_seen(&mut self, is_edge: bool) {
629        if is_edge {
630            self.cumulative_edges_seen += 1;
631        } else {
632            self.cumulative_features_seen += 1;
633        }
634    }
635
636    /// Updates campaign favored items.
637    pub const fn update_favored(&mut self, is_favored: bool, corpus_favored: bool) {
638        if is_favored && !corpus_favored {
639            self.favored_items += 1;
640        } else if !is_favored && corpus_favored {
641            self.favored_items -= 1;
642        }
643    }
644}
645
646/// Per-worker corpus manager.
647pub struct WorkerCorpus {
648    /// Worker Id
649    id: usize,
650    /// In-memory corpus entries populated from the persisted files and
651    /// runs administered by this worker.
652    in_memory_corpus: Vec<CorpusEntry>,
653    /// History of binned hitcount of edges seen during fuzzing
654    history_map: Vec<u8>,
655    /// Stable dense EVM edge IDs for this worker's history map.
656    edge_indices: EdgeIndexMap,
657    /// History of binned hitcount of sancov (native Rust) edges seen during fuzzing
658    sancov_history_map: Vec<u8>,
659    /// Number of failed replays from initial corpus
660    pub(crate) failed_replays: usize,
661    /// Worker Metrics
662    pub(crate) metrics: CorpusMetrics,
663    /// Shared transaction-sequence generator.
664    sequence_generator: SequenceGenerator,
665    /// Identifier of current mutated entry for this worker.
666    current_mutated_index: Option<usize>,
667    /// Config
668    config: Arc<FuzzCorpusConfig>,
669    /// Whether this corpus participates in stateless worker synchronization.
670    worker_sync_enabled: bool,
671    /// Sorted indices of new entries added to [`WorkerCorpus::in_memory_corpus`] since last sync.
672    new_entry_indices: Vec<usize>,
673    /// Corpus directories the master loaded at startup and still needs to distribute.
674    initial_export_dirs: Option<Vec<PathBuf>>,
675    /// Worker Dir
676    /// corpus_dir/worker1/
677    worker_dir: Option<PathBuf>,
678    /// Whether this worker already warned that a live corpus entry could not be persisted.
679    warned_persistence_failure: bool,
680    /// Metrics at last sync - used to calculate deltas while syncing with global metrics
681    last_sync_metrics: CorpusMetrics,
682    /// Optimization mode: the best value found so far (loaded from disk or discovered in-run).
683    optimization_best_value: Option<I256>,
684    /// Optimization mode: the call sequence that produced the best value.
685    optimization_best_sequence: Vec<BasicTxDetails>,
686    /// Monotonic time the worker's local map last gained a first-time edge; `None` until then.
687    ///
688    /// Updated wherever the map grows: live fuzzing, startup replay, and cross-worker sync. Tracks
689    /// *local* discovery (an edge new to this worker), not globally unique discovery. Kept out of
690    /// [`CorpusMetrics`] since a timestamp is neither additive across workers nor serializable.
691    last_new_edge_at: Option<Instant>,
692}
693
694/// Refs used during corpus replay to register contracts deployed mid-sequence as fuzz targets,
695/// mirroring the campaign loop so follow-up calls into them aren't dropped by `can_replay_tx`.
696#[derive(Clone, Copy)]
697pub struct DynamicTargetCtx<'a> {
698    pub project_contracts: &'a ContractsByArtifact,
699    pub setup_contracts: &'a ContractsByAddress,
700    pub artifact_filters: &'a ArtifactFilters,
701}
702
703/// Registers contracts created by the last tx so subsequent txs in the same replayed sequence
704/// can target them.
705pub(crate) fn register_replay_created(
706    state_changeset: &StateChangeset,
707    dynamic: Option<&DynamicTargetCtx<'_>>,
708    fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
709    created: &mut Vec<Address>,
710) {
711    let (Some(dynamic), Some(fuzzed_contracts)) = (dynamic, fuzzed_contracts) else {
712        return;
713    };
714    if let Err(error) = fuzzed_contracts.collect_created_contracts(
715        state_changeset,
716        dynamic.project_contracts,
717        dynamic.setup_contracts,
718        dynamic.artifact_filters,
719        created,
720    ) {
721        warn!(target: "corpus", "{error}");
722    }
723}
724
725/// Clears dynamic targets added during a replayed entry so they don't leak into the next one.
726pub(crate) fn rollback_replay_created(
727    fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
728    created: Vec<Address>,
729) {
730    if !created.is_empty()
731        && let Some(fuzzed_contracts) = fuzzed_contracts
732    {
733        fuzzed_contracts.clear_created_contracts(created);
734    }
735}
736
737fn load_optimization_state(config: &FuzzCorpusConfig) -> Option<(I256, Vec<BasicTxDetails>)> {
738    let corpus_dir = config.corpus_dir.as_ref()?;
739    let opt_path = corpus_dir.join(OPTIMIZATION_BEST_FILE);
740    if !opt_path.is_file() {
741        return None;
742    }
743
744    match foundry_common::fs::read_json_file::<OptimizationState>(&opt_path) {
745        Ok(state) => {
746            debug!(
747                target: "corpus",
748                "loaded optimization best value {} with sequence len {}",
749                state.best_value,
750                state.best_sequence.len()
751            );
752            Some((state.best_value, state.best_sequence))
753        }
754        Err(err) => {
755            let _ = sh_warn!(
756                "failed to load optimization state from {}: {err}; starting without persisted optimization seed",
757                opt_path.display()
758            );
759            None
760        }
761    }
762}
763
764fn replay_corpus_sequence<FEN: FoundryEvmNetwork>(
765    tx_seq: &[BasicTxDetails],
766    executor: &Executor<FEN>,
767    target: ReplayTarget<'_>,
768    coverage: ReplayCoverage<'_>,
769) -> Result<ReplayOutcome> {
770    let mut executor = executor.clone();
771    replay_corpus_sequence_with_executor(tx_seq, &mut executor, target, coverage, false, true)
772}
773
774fn replay_corpus_sequence_with_executor<FEN: FoundryEvmNetwork>(
775    tx_seq: &[BasicTxDetails],
776    executor: &mut Executor<FEN>,
777    target: ReplayTarget<'_>,
778    mut coverage: ReplayCoverage<'_>,
779    trace_sync: bool,
780    reject_unmatched_function: bool,
781) -> Result<ReplayOutcome> {
782    let mut cmp_seq = Vec::with_capacity(tx_seq.len());
783    let mut failed_replays = 0;
784    let mut new_coverage_for_entry = false;
785    let mut new_edge_for_entry = false;
786    let mut created: Vec<Address> = Vec::new();
787
788    for tx in tx_seq {
789        if WorkerCorpus::can_replay_tx(
790            tx,
791            target.stateless,
792            target.fuzzed_contracts,
793            target.senders,
794        ) {
795            let (kind, mut call_result) = if target.fuzzed_contracts.is_some() {
796                execute_invariant_replay_tx(executor, tx)?
797            } else {
798                (CampaignCallKind::Accepted, execute_tx(executor, tx)?)
799            };
800            cmp_seq.push(ComparisonHint::applicable(
801                &tx.call_details.calldata,
802                call_result
803                    .evm_cmp_values
804                    .take()
805                    .unwrap_or_default()
806                    .into_iter()
807                    .map(|cmp| ComparisonHint { lhs: cmp.op1, rhs: cmp.op2 }),
808            ));
809            let (new_coverage, is_edge) = call_result.merge_all_coverage(
810                coverage.history_map,
811                coverage.edge_indices,
812                coverage.sancov_history_map,
813            );
814            if new_coverage {
815                new_coverage_for_entry = true;
816                new_edge_for_entry |= is_edge;
817                if let Some(metrics) = coverage.metrics.as_deref_mut() {
818                    metrics.update_seen(is_edge);
819                }
820            }
821
822            if kind == CampaignCallKind::Accepted {
823                register_replay_created(
824                    &call_result.state_changeset,
825                    target.dynamic,
826                    target.fuzzed_contracts,
827                    &mut created,
828                );
829            }
830
831            if trace_sync {
832                trace!(
833                    target: "corpus",
834                    %new_coverage,
835                    ?tx,
836                    "replayed tx for syncing",
837                );
838            }
839        } else {
840            cmp_seq.push(Vec::new());
841            failed_replays += 1;
842
843            // The campaign executes loaded sequences verbatim, so a persisted sequence with any
844            // transaction outside the current targets, selectors, or senders must be dropped.
845            if reject_unmatched_function {
846                rollback_replay_created(target.fuzzed_contracts, created);
847                return Ok(ReplayOutcome {
848                    keep_entry: false,
849                    new_coverage: new_coverage_for_entry,
850                    new_edge: new_edge_for_entry,
851                    cmp_seq,
852                    failed_replays,
853                });
854            }
855        }
856    }
857    rollback_replay_created(target.fuzzed_contracts, created);
858
859    Ok(ReplayOutcome {
860        keep_entry: true,
861        new_coverage: new_coverage_for_entry,
862        new_edge: new_edge_for_entry,
863        cmp_seq,
864        failed_replays,
865    })
866}
867
868impl WorkerCorpus {
869    /// Produces the next sequence for either execution mode.
870    pub fn new_sequence(&mut self, test_runner: &mut TestRunner) -> Result<SequencePlan> {
871        if self.config.is_coverage_guided() && !self.in_memory_corpus.is_empty() {
872            self.evict_oldest_corpus()?;
873        }
874        let corpus_len = self.in_memory_corpus.len();
875        let plan = self.sequence_generator.start(
876            test_runner,
877            corpus_len,
878            |index| {
879                let entry = self
880                    .in_memory_corpus
881                    .get(index)
882                    .ok_or_else(|| eyre::eyre!("corpus index {index} is out of bounds"))?;
883                CorpusEntryView::new(&entry.tx_seq, &entry.cmp_seq)
884            },
885            self.config.is_coverage_guided(),
886        )?;
887        self.current_mutated_index = plan.source();
888        Ok(plan)
889    }
890
891    pub fn new<FEN: FoundryEvmNetwork>(
892        id: usize,
893        config: FuzzCorpusConfig,
894        sequence_generator: SequenceGenerator,
895        replay_root: Option<&Path>,
896        // Only required by master worker (id = 0) to replay existing corpus.
897        executor: Option<&Executor<FEN>>,
898        target: ReplayTarget<'_>,
899    ) -> Result<Self> {
900        let seed = if id == 0 {
901            WorkerCorpusSeed::load_from_disk(&config, replay_root, executor, target)?
902        } else {
903            WorkerCorpusSeed::empty(&config).with_optimization_state(&config)
904        };
905        let mut corpus = Self::from_seed(id, config, sequence_generator, seed)?;
906        corpus.worker_sync_enabled = true;
907        Ok(corpus)
908    }
909
910    pub(crate) fn from_seed(
911        id: usize,
912        config: FuzzCorpusConfig,
913        sequence_generator: SequenceGenerator,
914        mut seed: WorkerCorpusSeed,
915    ) -> Result<Self> {
916        let initial_export_dirs = if id == 0 { seed.replay_dirs.take() } else { None };
917        let worker_dir = if let Some(corpus_dir) = &config.corpus_dir {
918            let worker_dir = corpus_dir.join(format!("{WORKER}{id}"));
919            let worker_corpus = worker_dir.join(CORPUS_DIR);
920            let sync_dir = worker_dir.join(SYNC_DIR);
921
922            // Create the necessary directories for the worker.
923            foundry_common::fs::create_dir_all(&worker_corpus)?;
924            foundry_common::fs::create_dir_all(&sync_dir)?;
925
926            Some(worker_dir)
927        } else {
928            None
929        };
930
931        Ok(Self {
932            id,
933            in_memory_corpus: seed.in_memory_corpus,
934            history_map: seed.history_map,
935            edge_indices: seed.edge_indices,
936            sancov_history_map: seed.sancov_history_map,
937            failed_replays: seed.failed_replays,
938            metrics: seed.metrics,
939            sequence_generator,
940            current_mutated_index: None,
941            config: config.into(),
942            worker_sync_enabled: false,
943            new_entry_indices: Default::default(),
944            initial_export_dirs,
945            worker_dir,
946            warned_persistence_failure: false,
947            last_sync_metrics: Default::default(),
948            optimization_best_value: seed.optimization_best_value,
949            optimization_best_sequence: seed.optimization_best_sequence,
950            last_new_edge_at: seed.last_new_edge_at,
951        })
952    }
953
954    /// Updates stats for the given call sequence, if new coverage produced.
955    /// Persists the call sequence (if corpus directory is configured and new coverage or
956    /// improved optimization value) and updates in-memory corpus.
957    #[instrument(skip_all)]
958    pub fn process_inputs(
959        &mut self,
960        inputs: &[BasicTxDetails],
961        cmp_seq: &[Vec<CmpOperands>],
962        new_coverage: bool,
963        optimization: Option<(I256, Vec<BasicTxDetails>)>,
964    ) {
965        self.process_inputs_inner(
966            inputs,
967            cmp_seq,
968            new_coverage,
969            optimization,
970            CorpusInsertionMode::Live,
971            true,
972        );
973    }
974
975    /// Updates worker-local corpus state and persists interesting inputs immediately, while
976    /// leaving campaign-wide optimization persistence to the coordinator. Entries are retained
977    /// per worker rather than globally filtered so abrupt termination cannot discard discoveries
978    /// that have not yet reached the coordinator.
979    #[instrument(skip_all)]
980    pub fn process_inputs_for_campaign(
981        &mut self,
982        inputs: &[BasicTxDetails],
983        cmp_seq: &[Vec<CmpOperands>],
984        new_coverage: bool,
985        optimization: Option<(I256, Vec<BasicTxDetails>)>,
986    ) {
987        self.process_inputs_inner(
988            inputs,
989            cmp_seq,
990            new_coverage,
991            optimization,
992            CorpusInsertionMode::Live,
993            false,
994        );
995    }
996
997    fn process_inputs_inner(
998        &mut self,
999        inputs: &[BasicTxDetails],
1000        cmp_seq: &[Vec<CmpOperands>],
1001        new_coverage: bool,
1002        optimization: Option<(I256, Vec<BasicTxDetails>)>,
1003        insertion_mode: CorpusInsertionMode,
1004        persist_optimization: bool,
1005    ) {
1006        // Check if this run improved the optimization value.
1007        let improved_optimization = optimization.as_ref().is_some_and(|(value, _)| {
1008            self.optimization_best_value.is_none_or(|best| *value > best)
1009        });
1010
1011        // Update stats of current mutated primary corpus.
1012        if let Some(index) = self.current_mutated_index.take() {
1013            let should_credit = new_coverage || improved_optimization;
1014            if let Some(corpus) = self.in_memory_corpus.get_mut(index) {
1015                corpus.total_mutations += 1;
1016                if should_credit {
1017                    corpus.new_finds_produced += 1
1018                }
1019                let is_favored = (corpus.new_finds_produced as f64 / corpus.total_mutations as f64)
1020                    > FAVORABILITY_THRESHOLD;
1021                self.metrics.update_favored(is_favored, corpus.is_favored);
1022                corpus.is_favored = is_favored;
1023
1024                trace!(
1025                    target: "corpus",
1026                    "updated corpus {}, total mutations: {}, new finds: {}",
1027                    corpus.uuid, corpus.total_mutations, corpus.new_finds_produced
1028                );
1029            }
1030        }
1031        if let Some((value, best_seq)) = optimization
1032            && improved_optimization
1033        {
1034            self.optimization_best_value = Some(value);
1035            self.optimization_best_sequence = best_seq;
1036            if persist_optimization {
1037                self.persist_optimization_state();
1038            }
1039        }
1040
1041        if !self.config.is_coverage_guided() {
1042            return;
1043        }
1044
1045        // Collect inputs if current run produced new coverage or improved optimization.
1046        if !new_coverage && !improved_optimization {
1047            return;
1048        }
1049
1050        // When the run is interesting only because of optimization (no new coverage),
1051        // add the best prefix to the corpus instead of the full run — the prefix is
1052        // the sequence that actually achieved the best value.
1053        //
1054        // `inputs` can be empty when every call was discarded/popped but new coverage was
1055        // still recorded; there's nothing to persist, so skip without inserting an entry.
1056        let corpus_inputs = if improved_optimization && (!new_coverage || inputs.is_empty()) {
1057            self.optimization_best_sequence.clone()
1058        } else {
1059            inputs.to_vec()
1060        };
1061        if corpus_inputs.is_empty() {
1062            return;
1063        }
1064        let corpus_cmp_seq = cmp_seq
1065            .iter()
1066            .zip(&corpus_inputs)
1067            .map(|(values, tx)| {
1068                ComparisonHint::applicable(
1069                    &tx.call_details.calldata,
1070                    values.iter().map(|cmp| ComparisonHint { lhs: cmp.op1, rhs: cmp.op2 }),
1071                )
1072            })
1073            .collect();
1074        let corpus = CorpusEntry::new_with_cmp(corpus_inputs, corpus_cmp_seq, Uuid::new_v4());
1075
1076        self.insert_corpus_entry(corpus, insertion_mode)
1077    }
1078
1079    fn insert_corpus_entry(&mut self, corpus: CorpusEntry, insertion_mode: CorpusInsertionMode) {
1080        if matches!(insertion_mode, CorpusInsertionMode::Live)
1081            && let Some(worker_dir) = &self.worker_dir
1082        {
1083            let worker_corpus = worker_dir.join(CORPUS_DIR);
1084            let write_result = corpus.write_to_disk_in(&worker_corpus, self.config.corpus_gzip);
1085            if let Err(err) = write_result {
1086                if !self.warned_persistence_failure {
1087                    let _ = sh_warn!(
1088                        "Failed to persist coverage corpus entries for worker {} in {}: {err}",
1089                        self.id,
1090                        worker_corpus.display()
1091                    );
1092                    self.warned_persistence_failure = true;
1093                }
1094                debug!(target: "corpus", %err, "failed to record call sequence {:?}", corpus.tx_seq);
1095            } else {
1096                trace!(
1097                    target: "corpus",
1098                    "persisted {} inputs for new coverage for {} corpus",
1099                    corpus.tx_seq.len(),
1100                    corpus.uuid,
1101                );
1102            }
1103        }
1104
1105        self.push_corpus_entry(corpus);
1106    }
1107
1108    fn push_corpus_entry(&mut self, corpus: CorpusEntry) {
1109        let new_index = self.in_memory_corpus.len();
1110        if self.worker_sync_enabled {
1111            self.new_entry_indices.push(new_index);
1112        }
1113        self.metrics.corpus_count += 1;
1114        self.in_memory_corpus.push(corpus);
1115    }
1116
1117    /// Returns the previously persisted optimization best value and sequence (if any).
1118    pub fn optimization_initial_state(&self) -> (Option<I256>, Vec<BasicTxDetails>) {
1119        (self.optimization_best_value, self.optimization_best_sequence.clone())
1120    }
1121
1122    /// Persists the current optimization best value and sequence to disk.
1123    fn persist_optimization_state(&self) {
1124        let optimization_best = self
1125            .optimization_best_value
1126            .map(|value| (value, self.optimization_best_sequence.as_slice()));
1127        persist_optimization_output(&self.config, optimization_best);
1128    }
1129
1130    /// Collects EVM and sancov coverage from call result and updates metrics.
1131    pub fn merge_edge_coverage<FEN: FoundryEvmNetwork>(
1132        &mut self,
1133        call_result: &mut RawCallResult<FEN>,
1134    ) -> bool {
1135        if !self.config.collect_edge_coverage() {
1136            return false;
1137        }
1138
1139        let (new_coverage, is_edge) = call_result.merge_all_coverage(
1140            &mut self.history_map,
1141            &mut self.edge_indices,
1142            &mut self.sancov_history_map,
1143        );
1144        if new_coverage {
1145            self.metrics.update_seen(is_edge);
1146            // Only a first-time edge (not a new hitcount bucket, i.e. a "feature") resets the
1147            // timer.
1148            if is_edge {
1149                self.last_new_edge_at = Some(Instant::now());
1150            }
1151        }
1152        new_coverage
1153    }
1154
1155    /// Time since this worker last gained a first-time edge; `None` until it has seen one. See
1156    /// [`WorkerCorpus::last_new_edge_at`] for the local-vs-global caveat.
1157    pub(crate) fn time_since_new_edge(&self) -> Option<Duration> {
1158        self.last_new_edge_at.map(|at| at.elapsed())
1159    }
1160    /// Converts replayable observed sub-calls into one normal multi-transaction corpus entry.
1161    ///
1162    /// This captures calls shaped by a handler or another target call and lets the existing corpus
1163    /// machinery mutate, evict, sync, and persist them like any other interesting sequence.
1164    pub fn hoist_observed_calls(
1165        &mut self,
1166        observed: &[ObservedCall],
1167        parent_tx: &BasicTxDetails,
1168        targeted_contracts: &FuzzRunIdentifiedContracts,
1169        senders: &SenderFilters,
1170        insertion_mode: CorpusInsertionMode,
1171    ) {
1172        if !self.config.is_coverage_guided() || observed.is_empty() {
1173            return;
1174        }
1175
1176        let tx_seq = {
1177            let targets = targeted_contracts.targets();
1178            sequence_from_observed(
1179                observed,
1180                &targets,
1181                senders,
1182                Some((parent_tx.warp, parent_tx.roll)),
1183            )
1184        };
1185
1186        self.push_observed_sequence(tx_seq, insertion_mode)
1187    }
1188
1189    /// Seeds the corpus from sibling zero-input unit tests by replaying them on a clone of the
1190    /// post-setUp executor and keeping the direct replayable calls made by each test.
1191    ///
1192    /// Returns the number of test-derived corpus entries added.
1193    pub fn seed_from_test_traces<FEN: FoundryEvmNetwork>(
1194        &mut self,
1195        invariant_contract: &InvariantContract<'_>,
1196        targeted_contracts: &FuzzRunIdentifiedContracts,
1197        senders: &SenderFilters,
1198        executor: &Executor<FEN>,
1199    ) -> Result<usize> {
1200        if !self.config.is_coverage_guided() {
1201            return Ok(0);
1202        }
1203
1204        let mut added = 0;
1205
1206        for func in invariant_contract.abi.functions() {
1207            if !func.is_unit_test() {
1208                continue;
1209            }
1210            if invariant_contract
1211                .invariant_fns
1212                .iter()
1213                .any(|(invariant_fn, _)| func.selector() == invariant_fn.selector())
1214            {
1215                continue;
1216            }
1217
1218            let calldata = match func.abi_encode_input(&[]) {
1219                Ok(calldata) => Bytes::from(calldata),
1220                Err(_) => continue,
1221            };
1222
1223            let exec = executor.clone();
1224
1225            let raw = match exec.call_raw(CALLER, invariant_contract.address, calldata, U256::ZERO)
1226            {
1227                Ok(raw) => raw,
1228                Err(_) => continue,
1229            };
1230            if raw.reverted {
1231                continue;
1232            }
1233
1234            let observed = raw.observed_calls;
1235            if observed.is_empty() {
1236                continue;
1237            }
1238
1239            let Some(seq) = ({
1240                let targets = targeted_contracts.targets();
1241                let block = executor
1242                    .inspector()
1243                    .cheatcodes
1244                    .as_ref()
1245                    .and_then(|cheatcodes| cheatcodes.block.as_ref())
1246                    .unwrap_or(&executor.evm_env().block_env);
1247                let spec_id = executor
1248                    .inspector()
1249                    .cheatcodes
1250                    .as_ref()
1251                    .and_then(|cheatcodes| cheatcodes.execution_evm_version)
1252                    .unwrap_or_else(|| executor.spec_id());
1253                sequence_from_test_trace(
1254                    &observed,
1255                    &targets,
1256                    senders,
1257                    block.timestamp(),
1258                    block.number(),
1259                    spec_id.into(),
1260                )
1261            }) else {
1262                continue;
1263            };
1264
1265            let insertion_mode = if self.id == 0 {
1266                CorpusInsertionMode::Live
1267            } else {
1268                CorpusInsertionMode::MemoryOnly
1269            };
1270            let len_before = self.in_memory_corpus.len();
1271            self.push_observed_sequence(seq, insertion_mode);
1272            if self.in_memory_corpus.len() > len_before {
1273                debug!(target: "corpus", test = %func.name, "seeded corpus sequence from test trace");
1274                added += 1;
1275            }
1276        }
1277
1278        Ok(added)
1279    }
1280
1281    fn push_observed_sequence(
1282        &mut self,
1283        tx_seq: Vec<BasicTxDetails>,
1284        insertion_mode: CorpusInsertionMode,
1285    ) {
1286        if !self.config.is_coverage_guided() || tx_seq.is_empty() {
1287            return;
1288        }
1289
1290        let corpus = CorpusEntry::new(tx_seq);
1291
1292        self.insert_corpus_entry(corpus, insertion_mode)
1293    }
1294    /// Flush the oldest corpus mutated more than configured max mutations unless it is favored
1295    /// or pending synchronization.
1296    fn evict_oldest_corpus(&mut self) -> Result<()> {
1297        if self.in_memory_corpus.len() > self.config.corpus_min_size.max(1)
1298            && let Some(index) =
1299                self.in_memory_corpus.iter().enumerate().position(|(index, corpus)| {
1300                    self.new_entry_indices.binary_search(&index).is_err()
1301                        && corpus.total_mutations > self.config.corpus_min_mutations
1302                        && !corpus.is_favored
1303                })
1304        {
1305            let corpus = &self.in_memory_corpus[index];
1306
1307            trace!(target: "corpus", corpus=%serde_json::to_string(&corpus).unwrap(), "evict corpus");
1308
1309            // Remove corpus from memory.
1310            self.in_memory_corpus.remove(index);
1311
1312            // Adjust the tracked indices.
1313            self.new_entry_indices.retain_mut(|i| {
1314                if *i > index {
1315                    *i -= 1; // Shift indices down.
1316                    true // Keep this index.
1317                } else {
1318                    *i != index // Remove if it's the deleted index, keep otherwise.
1319                }
1320            });
1321        }
1322        Ok(())
1323    }
1324    // Sync Methods.
1325
1326    /// Imports the new corpus entries from the `sync` directory.
1327    /// These contain tx sequences which are replayed and used to update the history map.
1328    fn load_sync_corpus(&self, strict: bool) -> Result<Vec<(CorpusDirEntry, Vec<BasicTxDetails>)>> {
1329        let Some(worker_dir) = &self.worker_dir else {
1330            return Ok(vec![]);
1331        };
1332
1333        let sync_dir = worker_dir.join(SYNC_DIR);
1334        if !strict && !sync_dir.is_dir() {
1335            return Ok(vec![]);
1336        }
1337
1338        let mut imports = vec![];
1339        let entries = if strict {
1340            read_corpus_dir_strict(&sync_dir)?
1341        } else {
1342            read_corpus_dir(&sync_dir).collect()
1343        };
1344        for entry in entries {
1345            // A corrupt or truncated sync file must not abort the whole sync pass: skip it.
1346            let tx_seq = match entry.read_tx_seq() {
1347                Ok(tx_seq) => tx_seq,
1348                Err(err) if strict => {
1349                    return Err(eyre!(
1350                        "failed to read final corpus entry {}: {err}",
1351                        entry.path.display()
1352                    ));
1353                }
1354                Err(err) => {
1355                    warn!(target: "corpus", "skipping unreadable corpus file {}: {err}", entry.path.display());
1356                    let quarantine_path = entry.path.with_file_name(format!(
1357                        "{}.{}.invalid",
1358                        entry.name(),
1359                        Uuid::new_v4()
1360                    ));
1361                    if let Err(err) = std::fs::rename(&entry.path, &quarantine_path) {
1362                        debug!(target: "corpus", %err, "failed to quarantine unreadable corpus file {}", entry.path.display());
1363                    }
1364                    continue;
1365                }
1366            };
1367            if tx_seq.is_empty() {
1368                warn!(target: "corpus", "skipping empty corpus entry: {}", entry.path.display());
1369                if let Err(err) = std::fs::remove_file(&entry.path) {
1370                    if strict {
1371                        return Err(err.into());
1372                    }
1373                    debug!(target: "corpus", %err, "failed to remove empty corpus file {}", entry.path.display());
1374                }
1375                continue;
1376            }
1377            imports.push((entry, tx_seq));
1378        }
1379
1380        if !imports.is_empty() {
1381            debug!(target: "corpus", "imported {} new corpus entries", imports.len());
1382        }
1383
1384        Ok(imports)
1385    }
1386
1387    /// Adds a calibrated sync entry to the local corpus and queues it for fan-out on the master.
1388    fn push_synced_corpus_entry(
1389        &mut self,
1390        mut corpus: CorpusEntry,
1391        timestamp: u64,
1392        file_name: String,
1393    ) {
1394        corpus.timestamp = timestamp;
1395        corpus.persisted_file_name = Some(file_name);
1396        if self.worker_sync_enabled && self.id == 0 {
1397            self.new_entry_indices.push(self.in_memory_corpus.len());
1398        }
1399        self.in_memory_corpus.push(corpus);
1400    }
1401
1402    /// Syncs and calibrates the in memory corpus and updates the history_map if new coverage is
1403    /// found from the corpus findings of other workers.
1404    #[instrument(skip_all)]
1405    fn calibrate<FEN: FoundryEvmNetwork>(
1406        &mut self,
1407        executor: &Executor<FEN>,
1408        target: ReplayTarget<'_>,
1409        strict: bool,
1410    ) -> Result<()> {
1411        let Some(worker_dir) = &self.worker_dir else {
1412            return Ok(());
1413        };
1414        let corpus_dir = worker_dir.join(CORPUS_DIR);
1415
1416        for (entry, tx_seq) in self.load_sync_corpus(strict)? {
1417            let mut history_map = self.history_map.clone();
1418            let mut edge_indices = self.edge_indices.clone();
1419            let mut sancov_history_map = self.sancov_history_map.clone();
1420            let mut metrics = self.metrics.clone();
1421            let coverage = ReplayCoverage {
1422                history_map: &mut history_map,
1423                edge_indices: &mut edge_indices,
1424                sancov_history_map: &mut sancov_history_map,
1425                metrics: Some(&mut metrics),
1426            };
1427            let mut replay_executor = executor.clone();
1428            let ReplayOutcome { keep_entry, new_coverage, new_edge, cmp_seq, .. } =
1429                replay_corpus_sequence_with_executor(
1430                    &tx_seq,
1431                    &mut replay_executor,
1432                    target,
1433                    coverage,
1434                    true,
1435                    false,
1436                )?;
1437
1438            let sync_path = &entry.path;
1439            if keep_entry && new_coverage {
1440                // Move file from sync/ to corpus/ directory.
1441                let corpus_path = corpus_dir.join(sync_path.components().next_back().unwrap());
1442                if !accept_synced_corpus_file(&entry, &tx_seq, &corpus_path) {
1443                    if strict {
1444                        return Err(eyre!("failed to accept final corpus entry {}", entry.name()));
1445                    }
1446                    continue;
1447                }
1448
1449                self.history_map = history_map;
1450                self.edge_indices = edge_indices;
1451                self.sancov_history_map = sancov_history_map;
1452                self.metrics = metrics;
1453                // A synced edge is new to this worker's local map, so it advances the timer.
1454                if new_edge {
1455                    self.last_new_edge_at = Some(Instant::now());
1456                }
1457
1458                debug!(
1459                    target: "corpus",
1460                    name=%entry.name(),
1461                    "moved synced corpus to corpus dir",
1462                );
1463
1464                let corpus_entry = CorpusEntry::new_with_cmp(tx_seq.clone(), cmp_seq, entry.uuid);
1465                self.push_synced_corpus_entry(
1466                    corpus_entry,
1467                    entry.timestamp,
1468                    entry.name().to_owned(),
1469                );
1470            } else {
1471                // Remove the file as it did not generate new coverage.
1472                if let Err(err) = std::fs::remove_file(&entry.path) {
1473                    if strict {
1474                        return Err(err.into());
1475                    }
1476                    debug!(target: "corpus", %err, "failed to remove synced corpus from {sync_path:?}");
1477                    continue;
1478                }
1479                trace!(target: "corpus", "removed synced corpus from {sync_path:?}");
1480            }
1481        }
1482
1483        Ok(())
1484    }
1485
1486    /// Exports the new corpus entries to the master worker's sync dir.
1487    #[instrument(skip_all)]
1488    fn export_to_master(&mut self) -> Result<()> {
1489        // Master doesn't export (it only receives from others).
1490        assert_ne!(self.id, 0, "non-master only");
1491
1492        // Early return if no new entries or corpus dir not configured.
1493        if self.new_entry_indices.is_empty() || self.worker_dir.is_none() {
1494            return Ok(());
1495        }
1496
1497        let worker_dir = self.worker_dir.as_ref().unwrap();
1498        let Some(master_sync_dir) = self
1499            .config
1500            .corpus_dir
1501            .as_ref()
1502            .map(|dir| dir.join(format!("{WORKER}0")).join(SYNC_DIR))
1503        else {
1504            return Ok(());
1505        };
1506
1507        let mut exported = 0;
1508        let corpus_dir = worker_dir.join(CORPUS_DIR);
1509        let mut delivered = HashSet::new();
1510
1511        for &index in &self.new_entry_indices {
1512            let Some(corpus) = self.in_memory_corpus.get(index) else {
1513                delivered.insert(index);
1514                continue;
1515            };
1516            let file_name = corpus.file_name(corpus.should_gzip(self.config.corpus_gzip));
1517            let file_path = corpus_dir.join(&file_name);
1518            if !file_path.is_file()
1519                && let Err(err) = corpus.write_to_disk_in(&corpus_dir, self.config.corpus_gzip)
1520            {
1521                debug!(target: "corpus", %err, "failed to persist corpus {} for export", corpus.uuid);
1522                continue;
1523            }
1524            let sync_path = master_sync_dir.join(&file_name);
1525            if !link_corpus_file(&file_path, &sync_path) {
1526                continue;
1527            }
1528            exported += 1;
1529            delivered.insert(index);
1530        }
1531        self.new_entry_indices.retain(|index| !delivered.contains(index));
1532
1533        debug!(target: "corpus", "exported {exported} new corpus entries");
1534
1535        Ok(())
1536    }
1537
1538    /// Exports the global corpus to the `sync/` directories of all the non-master workers.
1539    #[instrument(skip_all)]
1540    fn export_to_workers(&mut self, num_workers: usize) -> Result<()> {
1541        assert_eq!(self.id, 0, "master worker only");
1542        if self.worker_dir.is_none() {
1543            return Ok(());
1544        }
1545
1546        let worker_dir = self.worker_dir.as_ref().unwrap();
1547        let master_corpus_dir = worker_dir.join(CORPUS_DIR);
1548        let startup_entries = if let Some(replay_dirs) = &self.initial_export_dirs {
1549            let mut seen_entries = HashSet::new();
1550            unique_corpus_entries(replay_dirs, &mut seen_entries)
1551                .map(|entry| entry.path)
1552                .collect::<Vec<_>>()
1553        } else {
1554            Vec::new()
1555        };
1556        let mut pending_entries = Vec::new();
1557        let mut delivered = HashSet::new();
1558        for &index in &self.new_entry_indices {
1559            let Some(corpus) = self.in_memory_corpus.get(index) else {
1560                delivered.insert(index);
1561                continue;
1562            };
1563            let path = master_corpus_dir
1564                .join(corpus.file_name(corpus.should_gzip(self.config.corpus_gzip)));
1565            if !path.is_file()
1566                && let Err(err) =
1567                    corpus.write_to_disk_in(&master_corpus_dir, self.config.corpus_gzip)
1568            {
1569                debug!(target: "corpus", %err, "failed to persist corpus {} for fan-out", corpus.uuid);
1570                continue;
1571            }
1572            pending_entries.push((index, path));
1573        }
1574
1575        let mut target_dirs = Vec::new();
1576        for target_worker in 1..num_workers {
1577            let target_dir = self
1578                .config
1579                .corpus_dir
1580                .as_ref()
1581                .unwrap()
1582                .join(format!("{WORKER}{target_worker}"))
1583                .join(SYNC_DIR);
1584            if !target_dir.is_dir() {
1585                foundry_common::fs::create_dir_all(&target_dir)?;
1586            }
1587            target_dirs.push(target_dir);
1588        }
1589
1590        let mut any_distributed = false;
1591        let mut startup_delivered = true;
1592        for path in &startup_entries {
1593            let Some(name) = path.file_name() else {
1594                startup_delivered = false;
1595                continue;
1596            };
1597            let mut delivered_to_all = true;
1598            for target_dir in &target_dirs {
1599                let sync_path = target_dir.join(name);
1600                if link_corpus_file(path, &sync_path) {
1601                    any_distributed = true;
1602                    trace!(target: "corpus", name=%name.to_string_lossy(), ?target_dir, "distributed corpus");
1603                } else {
1604                    delivered_to_all = false;
1605                }
1606            }
1607            startup_delivered &= delivered_to_all;
1608        }
1609
1610        for (index, path) in pending_entries {
1611            let Some(name) = path.file_name() else { continue };
1612            let mut delivered_to_all = true;
1613            for target_dir in &target_dirs {
1614                let sync_path = target_dir.join(name);
1615                if link_corpus_file(&path, &sync_path) {
1616                    any_distributed = true;
1617                    trace!(target: "corpus", name=%name.to_string_lossy(), ?target_dir, "distributed corpus");
1618                } else {
1619                    delivered_to_all = false;
1620                }
1621            }
1622            if delivered_to_all {
1623                delivered.insert(index);
1624            }
1625        }
1626
1627        self.new_entry_indices.retain(|index| !delivered.contains(index));
1628        if startup_delivered {
1629            self.initial_export_dirs = None;
1630        }
1631        debug!(target: "corpus", %any_distributed, "distributed master corpus to all workers");
1632
1633        Ok(())
1634    }
1635
1636    // TODO(dani): currently only master syncs metrics?
1637    /// Syncs local metrics with global corpus metrics by calculating and applying deltas.
1638    pub(crate) fn sync_metrics(&mut self, global_corpus_metrics: &GlobalCorpusMetrics) {
1639        // Calculate delta metrics since last sync.
1640        let edges_delta = self
1641            .metrics
1642            .cumulative_edges_seen
1643            .saturating_sub(self.last_sync_metrics.cumulative_edges_seen);
1644        let features_delta = self
1645            .metrics
1646            .cumulative_features_seen
1647            .saturating_sub(self.last_sync_metrics.cumulative_features_seen);
1648        // For corpus count and favored items, calculate deltas.
1649        let corpus_count_delta =
1650            self.metrics.corpus_count as isize - self.last_sync_metrics.corpus_count as isize;
1651        let favored_delta =
1652            self.metrics.favored_items as isize - self.last_sync_metrics.favored_items as isize;
1653
1654        // Add delta values to global metrics.
1655
1656        if edges_delta > 0 {
1657            global_corpus_metrics.cumulative_edges_seen.fetch_add(edges_delta, Ordering::Relaxed);
1658        }
1659        if features_delta > 0 {
1660            global_corpus_metrics
1661                .cumulative_features_seen
1662                .fetch_add(features_delta, Ordering::Relaxed);
1663        }
1664
1665        if corpus_count_delta > 0 {
1666            global_corpus_metrics
1667                .corpus_count
1668                .fetch_add(corpus_count_delta as usize, Ordering::Relaxed);
1669        } else if corpus_count_delta < 0 {
1670            global_corpus_metrics
1671                .corpus_count
1672                .fetch_sub((-corpus_count_delta) as usize, Ordering::Relaxed);
1673        }
1674
1675        if favored_delta > 0 {
1676            global_corpus_metrics
1677                .favored_items
1678                .fetch_add(favored_delta as usize, Ordering::Relaxed);
1679        } else if favored_delta < 0 {
1680            global_corpus_metrics
1681                .favored_items
1682                .fetch_sub((-favored_delta) as usize, Ordering::Relaxed);
1683        }
1684
1685        // Store current metrics as last sync metrics for next delta calculation.
1686        self.last_sync_metrics = self.metrics.clone();
1687    }
1688
1689    /// Syncs the workers in_memory_corpus and history_map with the findings from other workers.
1690    #[instrument(skip_all)]
1691    pub fn sync<FEN: FoundryEvmNetwork>(
1692        &mut self,
1693        num_workers: usize,
1694        executor: &Executor<FEN>,
1695        target: ReplayTarget<'_>,
1696        global_corpus_metrics: &GlobalCorpusMetrics,
1697    ) -> Result<()> {
1698        trace!(target: "corpus", "syncing");
1699
1700        self.sync_metrics(global_corpus_metrics);
1701
1702        self.calibrate(executor, target, false)?;
1703        if self.id == 0 {
1704            self.export_to_workers(num_workers)?;
1705        } else {
1706            self.export_to_master()?;
1707        }
1708
1709        debug!(target: "corpus", "synced");
1710
1711        Ok(())
1712    }
1713
1714    /// Performs the ordered final synchronization once every worker has stopped fuzzing.
1715    pub(crate) fn finalize_sync<FEN: FoundryEvmNetwork>(
1716        &mut self,
1717        executor: &Executor<FEN>,
1718        target: ReplayTarget<'_>,
1719        coordinator: &CorpusSyncCoordinator,
1720    ) -> Result<()> {
1721        if self.id != 0 {
1722            self.export_to_master()?;
1723            if self.worker_dir.is_some() && !self.new_entry_indices.is_empty() {
1724                return Err(eyre!("worker {} failed to complete final corpus export", self.id));
1725            }
1726        }
1727        if !coordinator.wait() {
1728            return Ok(());
1729        }
1730
1731        if self.id == 0 {
1732            self.calibrate(executor, target, true)?;
1733            self.export_to_workers(coordinator.workers)?;
1734            if self.worker_dir.is_some()
1735                && (!self.new_entry_indices.is_empty() || self.initial_export_dirs.is_some())
1736            {
1737                return Err(eyre!("master failed to complete final corpus fan-out"));
1738            }
1739        }
1740        if !coordinator.wait() {
1741            return Ok(());
1742        }
1743
1744        if self.id != 0 {
1745            self.calibrate(executor, target, true)?;
1746        }
1747        if !coordinator.wait() {
1748            return Ok(());
1749        }
1750
1751        Ok(())
1752    }
1753
1754    /// Helper to check if a tx can be replayed.
1755    pub(crate) fn can_replay_tx(
1756        tx: &BasicTxDetails,
1757        stateless: Option<StatelessReplayTarget<'_>>,
1758        fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
1759        senders: Option<&SenderFilters>,
1760    ) -> bool {
1761        senders.is_none_or(|senders| senders.allows(tx.sender))
1762            && (fuzzed_contracts.is_some_and(|contracts| contracts.targets().can_replay(tx))
1763                || stateless.is_some_and(|target| target.can_replay(tx)))
1764    }
1765}
1766
1767fn sequence_from_observed(
1768    observed: &[ObservedCall],
1769    targets: &TargetedContracts,
1770    senders: &SenderFilters,
1771    first_delay: Option<(Option<U256>, Option<U256>)>,
1772) -> Vec<BasicTxDetails> {
1773    let mut first_delay = first_delay;
1774    observed
1775        .iter()
1776        .filter_map(|call| {
1777            let mut tx = BasicTxDetails {
1778                warp: None,
1779                roll: None,
1780                sender: call.caller,
1781                call_details: CallDetails {
1782                    target: call.target,
1783                    calldata: call.calldata.clone(),
1784                    value: call.value,
1785                },
1786            };
1787            (targets.can_replay(&tx) && senders.allows(tx.sender)).then(|| {
1788                let (warp, roll) = first_delay.take().unwrap_or((None, None));
1789                tx.warp = warp;
1790                tx.roll = roll;
1791                tx
1792            })
1793        })
1794        .collect()
1795}
1796
1797fn sequence_from_test_trace(
1798    observed: &[ObservedCall],
1799    targets: &TargetedContracts,
1800    senders: &SenderFilters,
1801    mut timestamp: U256,
1802    mut block_number: U256,
1803    spec_id: SpecId,
1804) -> Option<Vec<BasicTxDetails>> {
1805    let mut next_timestamp = timestamp;
1806    let mut next_block_number = block_number;
1807    let mut sequence = Vec::new();
1808
1809    for call in observed {
1810        if call.target == CHEATCODE_ADDRESS {
1811            match <[u8; 4]>::try_from(call.calldata.get(..4)?).ok()? {
1812                warpCall::SELECTOR => {
1813                    if call.depth != 1 {
1814                        return None;
1815                    }
1816                    next_timestamp = warpCall::abi_decode(&call.calldata).ok()?.newTimestamp;
1817                }
1818                rollCall::SELECTOR => {
1819                    if call.depth != 1 {
1820                        return None;
1821                    }
1822                    let new_height = rollCall::abi_decode(&call.calldata).ok()?.newHeight;
1823                    // Prague rolls also write EIP-2935 history, which replaying a block-number
1824                    // delta cannot reproduce. Reject the entire seed, including earlier calls.
1825                    if spec_id >= SpecId::PRAGUE && new_height > next_block_number {
1826                        return None;
1827                    }
1828                    next_block_number = new_height;
1829                }
1830                // Snapshot restoration can undo target calls and environment changes, but the
1831                // observed trace does not retain enough snapshot state to reproduce that exactly.
1832                revertToCall::SELECTOR
1833                | revertToStateCall::SELECTOR
1834                | revertToAndDeleteCall::SELECTOR
1835                | revertToStateAndDeleteCall::SELECTOR
1836                | setEvmVersionCall::SELECTOR => return None,
1837                _ => {}
1838            }
1839            continue;
1840        }
1841        if call.depth != 1 {
1842            continue;
1843        }
1844
1845        if next_timestamp < timestamp || next_block_number < block_number {
1846            return None;
1847        }
1848        let tx = BasicTxDetails {
1849            warp: next_timestamp.checked_sub(timestamp).filter(|delay| !delay.is_zero()),
1850            roll: next_block_number.checked_sub(block_number).filter(|delay| !delay.is_zero()),
1851            sender: call.caller,
1852            call_details: CallDetails {
1853                target: call.target,
1854                calldata: call.calldata.clone(),
1855                value: call.value,
1856            },
1857        };
1858        if targets.can_replay(&tx) && senders.allows(tx.sender) {
1859            timestamp = next_timestamp;
1860            block_number = next_block_number;
1861            sequence.push(tx);
1862        }
1863    }
1864
1865    Some(sequence)
1866}
1867
1868fn persist_optimization_output(
1869    config: &FuzzCorpusConfig,
1870    optimization_best: Option<(I256, &[BasicTxDetails])>,
1871) {
1872    let Some(root) = &config.corpus_dir else {
1873        return;
1874    };
1875    let Some((value, sequence)) = optimization_best else {
1876        return;
1877    };
1878    let state = OptimizationState { best_value: value, best_sequence: sequence.to_vec() };
1879    let path = root.join(OPTIMIZATION_BEST_FILE);
1880    if let Err(err) = foundry_common::fs::write_json_file(&path, &state) {
1881        debug!(target: "corpus", %err, "failed to persist optimization state");
1882    } else {
1883        trace!(
1884            target: "corpus",
1885            "persisted optimization best value {} with sequence len {}",
1886            value,
1887            sequence.len()
1888        );
1889    }
1890}
1891
1892pub(crate) fn persist_campaign_optimization(
1893    config: &FuzzCorpusConfig,
1894    value: Option<I256>,
1895    sequence: &[BasicTxDetails],
1896) {
1897    persist_optimization_output(config, value.map(|value| (value, sequence)));
1898}
1899
1900fn has_legacy_invariant_corpus_dirs(path: &Path) -> bool {
1901    std::fs::read_dir(path).is_ok_and(|entries| {
1902        entries.flatten().any(|entry| {
1903            let path = entry.path();
1904            path.is_dir()
1905                && entry.file_name().to_str().is_some_and(|name| !name.starts_with(WORKER))
1906                && !path.join(OPTIMIZATION_BEST_FILE).is_file()
1907        })
1908    })
1909}
1910
1911fn unique_corpus_entries<'a>(
1912    replay_dirs: &'a [PathBuf],
1913    seen_entries: &'a mut HashSet<Uuid>,
1914) -> impl Iterator<Item = CorpusDirEntry> + 'a {
1915    replay_dirs.iter().flat_map(|replay_dir| read_corpus_dir(replay_dir)).filter(|entry| {
1916        let is_new = seen_entries.insert(entry.uuid);
1917        if !is_new {
1918            trace!(target: "corpus", "skipping duplicate corpus entry {}", entry.uuid);
1919        }
1920        is_new
1921    })
1922}
1923
1924#[cfg(test)]
1925mod tests {
1926
1927    use super::*;
1928    use crate::{
1929        executors::ExecutorBuilder,
1930        inspectors::{EdgeCovHit, EdgeCoverage, EdgeKey},
1931    };
1932    use alloy_dyn_abi::DynSolValue;
1933    use alloy_evm::EvmEnv;
1934    use foundry_cheatcodes::{Cheatcodes, CheatsConfig};
1935    use foundry_config::FuzzDictionaryConfig;
1936    use foundry_evm_core::{backend::Backend, constants::MAGIC_ASSUME, evm::EthEvmNetwork};
1937    use foundry_evm_fuzz::strategies::{EvmFuzzState, TxGenerator};
1938    use proptest::prelude::{Just, Strategy};
1939    use rayon::prelude::*;
1940    use revm::{DatabaseRef, bytecode::Bytecode, context::TxEnv, database::InMemoryDB};
1941    use std::fs;
1942
1943    fn basic_tx() -> BasicTxDetails {
1944        BasicTxDetails {
1945            warp: None,
1946            roll: None,
1947            sender: Address::ZERO,
1948            call_details: foundry_evm_fuzz::CallDetails {
1949                target: Address::ZERO,
1950                calldata: Bytes::new(),
1951                value: None,
1952            },
1953        }
1954    }
1955
1956    fn basic_tx_with_calldata(calldata: impl Into<Bytes>) -> BasicTxDetails {
1957        let mut tx = basic_tx();
1958        tx.call_details.calldata = calldata.into();
1959        tx
1960    }
1961
1962    fn tx_for_function(
1963        target: Address,
1964        function: &Function,
1965        args: &[DynSolValue],
1966    ) -> BasicTxDetails {
1967        BasicTxDetails {
1968            warp: None,
1969            roll: None,
1970            sender: Address::ZERO,
1971            call_details: foundry_evm_fuzz::CallDetails {
1972                target,
1973                calldata: Bytes::from(function.abi_encode_input(args).unwrap()),
1974                value: None,
1975            },
1976        }
1977    }
1978
1979    fn empty_fuzz_state() -> EvmFuzzState {
1980        EvmFuzzState::new(&[], &InMemoryDB::default(), FuzzDictionaryConfig::default(), None)
1981    }
1982
1983    fn temp_corpus_dir() -> PathBuf {
1984        let dir = std::env::temp_dir().join(format!("foundry-corpus-tests-{}", Uuid::new_v4()));
1985        let _ = fs::create_dir_all(&dir);
1986        dir
1987    }
1988
1989    fn corpus_config(corpus_dir: PathBuf) -> FuzzCorpusConfig {
1990        FuzzCorpusConfig {
1991            corpus_dir: Some(corpus_dir),
1992            corpus_gzip: false,
1993            corpus_min_mutations: 0,
1994            corpus_min_size: 0,
1995            ..Default::default()
1996        }
1997    }
1998
1999    fn test_sequence(config: &FuzzCorpusConfig, tx: TxGenerator) -> SequenceGenerator {
2000        SequenceGenerator::stateless(
2001            tx,
2002            empty_fuzz_state().stateless_worker(),
2003            Function::parse("test(uint256)").unwrap(),
2004            config,
2005        )
2006        .unwrap()
2007    }
2008
2009    fn worker_corpus(id: usize, corpus_root: PathBuf, seed: WorkerCorpusSeed) -> WorkerCorpus {
2010        let config = corpus_config(corpus_root);
2011        let generator =
2012            test_sequence(&config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
2013        let mut corpus = WorkerCorpus::from_seed(id, config, generator, seed).unwrap();
2014        corpus.worker_sync_enabled = true;
2015        corpus
2016    }
2017
2018    fn empty_worker_corpus(id: usize, corpus_root: PathBuf) -> WorkerCorpus {
2019        worker_corpus(id, corpus_root, WorkerCorpusSeed::default())
2020    }
2021
2022    #[test]
2023    fn worker_initialization_fails_when_corpus_directories_cannot_be_created() {
2024        let corpus_root = temp_corpus_dir().join("not-a-directory");
2025        fs::write(&corpus_root, b"blocked").unwrap();
2026        let config = corpus_config(corpus_root);
2027        let generator =
2028            test_sequence(&config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
2029
2030        assert!(
2031            WorkerCorpus::from_seed(0, config, generator, WorkerCorpusSeed::default()).is_err()
2032        );
2033    }
2034
2035    fn sync_test_executor(corpus_root: PathBuf, target: Address) -> Executor<EthEvmNetwork> {
2036        let mut executor = ExecutorBuilder::<EthEvmNetwork>::default().gas_limit(1 << 24).build(
2037            EvmEnv::default(),
2038            TxEnv::default(),
2039            Backend::spawn(None).unwrap(),
2040            Default::default(),
2041        );
2042        executor.inspector_mut().collect_edge_coverage_with_config(&corpus_config(corpus_root));
2043        // CALLDATALOAD(4); PUSH1 8; JUMPI; STOP; JUMPDEST; STOP.
2044        executor
2045            .set_code(
2046                target,
2047                Bytecode::new_raw(Bytes::from_static(&[
2048                    0x60, 0x04, 0x35, 0x60, 0x08, 0x57, 0x00, 0x00, 0x5b, 0x00,
2049                ])),
2050            )
2051            .unwrap();
2052        executor
2053    }
2054
2055    fn finalize_test_worker(
2056        worker: &mut WorkerCorpus,
2057        corpus_root: PathBuf,
2058        target_address: Address,
2059        coordinator: &CorpusSyncCoordinator,
2060    ) {
2061        let function = Function::parse("test(uint256)").unwrap();
2062        let executor = sync_test_executor(corpus_root, target_address);
2063        worker
2064            .finalize_sync(
2065                &executor,
2066                ReplayTarget {
2067                    stateless: Some(StatelessReplayTarget {
2068                        function: &function,
2069                        address: target_address,
2070                    }),
2071                    fuzzed_contracts: None,
2072                    dynamic: None,
2073                    senders: None,
2074                },
2075                coordinator,
2076            )
2077            .unwrap();
2078    }
2079
2080    fn seeded_worker_corpus(
2081        id: usize,
2082        corpus_root: PathBuf,
2083        entries: Vec<CorpusEntry>,
2084    ) -> WorkerCorpus {
2085        worker_corpus(
2086            id,
2087            corpus_root,
2088            WorkerCorpusSeed { in_memory_corpus: entries, ..Default::default() },
2089        )
2090    }
2091    fn new_manager_with_single_corpus() -> (WorkerCorpus, Uuid) {
2092        let corpus = CorpusEntry::new(vec![basic_tx()]);
2093        let seed_uuid = corpus.uuid;
2094        let mut manager = seeded_worker_corpus(0, temp_corpus_dir(), vec![corpus]);
2095        manager.current_mutated_index = Some(0);
2096
2097        (manager, seed_uuid)
2098    }
2099
2100    fn targeted_contracts_with_selective_functions(
2101        target: Address,
2102        functions: Vec<Function>,
2103        targeted_selectors: impl IntoIterator<Item = alloy_primitives::Selector>,
2104    ) -> FuzzRunIdentifiedContracts {
2105        use alloy_json_abi::JsonAbi;
2106        use foundry_evm_fuzz::invariant::TargetedContract;
2107
2108        let mut abi = JsonAbi::new();
2109        for function in functions {
2110            abi.functions.entry(function.name.clone()).or_default().push(function);
2111        }
2112
2113        let mut contract = TargetedContract::new("Target".to_string(), abi);
2114        contract.add_selectors(targeted_selectors, false).unwrap();
2115
2116        let mut targets = TargetedContracts::new();
2117        targets.inner.insert(target, contract);
2118        FuzzRunIdentifiedContracts::new(targets, false)
2119    }
2120
2121    #[test]
2122    fn stateful_replay_restores_assumption_state_and_delays() {
2123        let target = Address::repeat_byte(0x11);
2124        let function = Function::parse("test(uint256)").unwrap();
2125        let contracts = targeted_contracts_with_selective_functions(
2126            target,
2127            vec![function.clone()],
2128            [function.selector()],
2129        );
2130        let mut rejected =
2131            tx_for_function(target, &function, &[DynSolValue::Uint(U256::ZERO, 256)]);
2132        rejected.warp = Some(U256::from(10));
2133        rejected.roll = Some(U256::from(5));
2134        let mut accepted = tx_for_function(target, &function, &[DynSolValue::Uint(U256::ONE, 256)]);
2135        accepted.warp = Some(U256::from(3));
2136        accepted.roll = Some(U256::from(2));
2137
2138        // Branch on the argument. The rejected branch writes state and returns MAGIC_ASSUME;
2139        // the accepted branch writes the current timestamp. Both branches compare the argument
2140        // with 42, so both record comparisons that cmp mutations can apply to the calldata.
2141        let mut code = vec![0x60, 0x04, 0x35, 0x60, 0x00, 0x57];
2142        code.extend_from_slice(&[0x60, 0x2a, 0x60, 0x04, 0x35, 0x10, 0x50, 0x60, 0x63, 0x5f, 0x55]);
2143        code.push(0x6e); // PUSH15.
2144        code.extend_from_slice(MAGIC_ASSUME);
2145        code.extend_from_slice(&[0x5f, 0x52, 0x60, 0x0f, 0x60, 0x11, 0xf3]);
2146        code[4] = code.len().try_into().unwrap();
2147        code.extend_from_slice(&[
2148            0x5b, 0x60, 0x2a, 0x60, 0x04, 0x35, 0x10, 0x50, 0x42, 0x5f, 0x55, 0x00,
2149        ]);
2150
2151        for accept_last in [false, true] {
2152            let mut executor = sync_test_executor(temp_corpus_dir(), target);
2153            executor.inspector_mut().cheatcodes =
2154                Some(Box::new(Cheatcodes::new(Arc::new(CheatsConfig::default()))));
2155            executor.inspector_mut().collect_evm_cmp_log(true);
2156            executor.set_code(target, Bytecode::new_raw(code.clone().into())).unwrap();
2157            let initial_block = executor.evm_env().block_env.clone();
2158            let initial_cheatcode_block =
2159                executor.inspector().cheatcodes.as_ref().unwrap().block.clone();
2160            let mut history_map = Vec::new();
2161            let mut edge_indices = EdgeIndexMap::default();
2162            let mut sancov_history_map = Vec::new();
2163            let sequence =
2164                [rejected.clone(), if accept_last { accepted.clone() } else { rejected.clone() }];
2165            let outcome = replay_corpus_sequence_with_executor(
2166                &sequence,
2167                &mut executor,
2168                ReplayTarget {
2169                    stateless: None,
2170                    fuzzed_contracts: Some(&contracts),
2171                    dynamic: None,
2172                    senders: None,
2173                },
2174                ReplayCoverage {
2175                    history_map: &mut history_map,
2176                    edge_indices: &mut edge_indices,
2177                    sancov_history_map: &mut sancov_history_map,
2178                    metrics: None,
2179                },
2180                false,
2181                true,
2182            )
2183            .unwrap();
2184            assert!(outcome.keep_entry);
2185            assert_eq!(outcome.failed_replays, 0);
2186            assert_eq!(outcome.cmp_seq.len(), sequence.len());
2187            // Rejected inputs remain useful mutation seeds; this fix preserves their feedback.
2188            assert!(outcome.cmp_seq.iter().all(|hints| !hints.is_empty()));
2189            assert!(outcome.new_coverage);
2190            assert!(history_map.iter().any(|&hit| hit != 0));
2191            if accept_last {
2192                let timestamp = initial_block.timestamp() + U256::from(3);
2193                assert_eq!(executor.evm_env().block_env.timestamp(), timestamp);
2194                assert_eq!(
2195                    executor.evm_env().block_env.number(),
2196                    initial_block.number() + U256::from(2)
2197                );
2198                assert_eq!(executor.backend().storage_ref(target, U256::ZERO).unwrap(), timestamp);
2199            } else {
2200                assert_eq!(executor.evm_env().block_env, initial_block);
2201                assert_eq!(
2202                    executor.inspector().cheatcodes.as_ref().unwrap().block,
2203                    initial_cheatcode_block
2204                );
2205                assert_eq!(executor.backend().storage_ref(target, U256::ZERO).unwrap(), U256::ZERO);
2206            }
2207        }
2208    }
2209
2210    // A corrupt/truncated corpus file (valid name, unparsable content) must surface as a per-entry
2211    // read error rather than break directory scanning, so the load/sync loops can skip malformed
2212    // files from older versions or manual edits instead of aborting the whole campaign.
2213    #[test]
2214    fn corrupt_corpus_file_surfaces_as_error_for_load_to_skip() {
2215        let dir = temp_corpus_dir();
2216
2217        // A valid entry round-trips through the on-disk format.
2218        let valid = CorpusEntry::new(vec![basic_tx()]);
2219        valid.write_to_disk_in(&dir, false).unwrap();
2220
2221        // A file with a valid corpus name but garbage content.
2222        let corrupt_path = dir.join(format!("{}-123.json", Uuid::new_v4()));
2223        fs::write(&corrupt_path, b"{ not valid json").unwrap();
2224
2225        let entries = read_corpus_dir(&dir).collect::<Vec<_>>();
2226        assert_eq!(entries.len(), 2, "directory scan should surface both files");
2227
2228        let (mut ok, mut err) = (0u32, 0u32);
2229        for entry in &entries {
2230            match entry.read_tx_seq() {
2231                Ok(seq) => {
2232                    ok += 1;
2233                    assert_eq!(seq.len(), 1);
2234                }
2235                Err(_) => err += 1,
2236            }
2237        }
2238        assert_eq!((ok, err), (1, 1), "the corrupt file must read as Err, the valid one as Ok");
2239    }
2240
2241    #[test]
2242    fn sync_inbox_loads_entries_regardless_of_timestamp() {
2243        let corpus_root = temp_corpus_dir();
2244        let manager = empty_worker_corpus(0, corpus_root.clone());
2245        let sync_dir = corpus_root.join("worker0").join(SYNC_DIR);
2246        let mut corpus = CorpusEntry::new(vec![basic_tx()]);
2247        corpus.timestamp = 0;
2248        corpus.write_to_disk_in(&sync_dir, false).unwrap();
2249        let mut empty = CorpusEntry::new(vec![]);
2250        empty.timestamp = 0;
2251        let empty_path = empty.write_to_disk_in(&sync_dir, false).unwrap();
2252        let corrupt_path = sync_dir.join(format!("{}-0.json", Uuid::new_v4()));
2253        fs::write(&corrupt_path, b"{ not valid json").unwrap();
2254
2255        let imports = manager.load_sync_corpus(false).unwrap();
2256
2257        assert_eq!(imports.len(), 1);
2258        assert_eq!(imports[0].0.timestamp, 0);
2259        assert!(!empty_path.exists());
2260        assert!(!corrupt_path.exists());
2261        assert!(fs::read_dir(&sync_dir).unwrap().flatten().any(|entry| {
2262            entry.file_name().to_string_lossy().ends_with(".invalid") && entry.path().is_file()
2263        }));
2264    }
2265
2266    #[test]
2267    fn synced_entries_are_queued_for_fanout_only_on_master() {
2268        let corpus_root = temp_corpus_dir();
2269        let mut master = empty_worker_corpus(0, corpus_root.clone());
2270        let mut worker = empty_worker_corpus(1, corpus_root);
2271        let master_entry = CorpusEntry::new(vec![basic_tx()]);
2272        let worker_entry = CorpusEntry::new(vec![basic_tx()]);
2273
2274        master.push_synced_corpus_entry(master_entry, 1, "master-1.json".to_string());
2275        worker.push_synced_corpus_entry(worker_entry, 2, "worker-2.json".to_string());
2276
2277        assert_eq!(master.new_entry_indices, [0]);
2278        assert!(worker.new_entry_indices.is_empty());
2279        assert_eq!(master.in_memory_corpus[0].timestamp, 1);
2280        assert_eq!(worker.in_memory_corpus[0].timestamp, 2);
2281        assert_eq!(master.metrics.corpus_count, 0);
2282        assert_eq!(worker.metrics.corpus_count, 0);
2283    }
2284
2285    #[test]
2286    fn master_distributes_old_synced_entries() {
2287        let corpus_root = temp_corpus_dir();
2288        let mut master = empty_worker_corpus(0, corpus_root.clone());
2289        master.initial_export_dirs = None;
2290        let mut corpus =
2291            CorpusEntry::new(vec![basic_tx_with_calldata(vec![0; GZIP_THRESHOLD * 2])]);
2292        corpus.timestamp = 0;
2293        let path =
2294            corpus.write_to_disk_in(&corpus_root.join("worker0").join(CORPUS_DIR), true).unwrap();
2295        let name = path.file_name().unwrap().to_str().unwrap().to_owned();
2296        assert!(name.ends_with(".json.gz"));
2297        master.push_synced_corpus_entry(corpus, 0, name.clone());
2298
2299        master.export_to_workers(3).unwrap();
2300
2301        assert!(corpus_root.join("worker1").join(SYNC_DIR).join(&name).is_file());
2302        assert!(corpus_root.join("worker2").join(SYNC_DIR).join(name).is_file());
2303        assert!(master.new_entry_indices.is_empty());
2304    }
2305
2306    #[test]
2307    fn worker_retries_failed_export() {
2308        let corpus_root = temp_corpus_dir();
2309        let mut worker = empty_worker_corpus(1, corpus_root.clone());
2310        let corpus = CorpusEntry::new(vec![basic_tx_with_calldata([1])]);
2311        let name = corpus.file_name(false);
2312        worker.push_corpus_entry(corpus);
2313
2314        let master_sync = corpus_root.join("worker0").join(SYNC_DIR);
2315        fs::create_dir_all(&master_sync).unwrap();
2316        let destination = master_sync.join(&name);
2317        foundry_common::fs::write_json_file(&destination, &vec![basic_tx_with_calldata([2])])
2318            .unwrap();
2319
2320        worker.export_to_master().unwrap();
2321        assert_eq!(worker.new_entry_indices, [0]);
2322        assert!(corpus_root.join("worker1").join(CORPUS_DIR).join(&name).is_file());
2323
2324        fs::remove_file(&destination).unwrap();
2325        worker.export_to_master().unwrap();
2326
2327        assert!(worker.new_entry_indices.is_empty());
2328        let exported = read_corpus_dir(&master_sync).next().unwrap().read_tx_seq().unwrap();
2329        assert!(same_tx_sequence(&exported, &[basic_tx_with_calldata([1])]));
2330    }
2331
2332    #[test]
2333    fn master_retries_partial_fanout() {
2334        let corpus_root = temp_corpus_dir();
2335        let mut master = empty_worker_corpus(0, corpus_root.clone());
2336        master.initial_export_dirs = None;
2337        let corpus = CorpusEntry::new(vec![basic_tx_with_calldata([1])]);
2338        let name = corpus.file_name(false);
2339        corpus.write_to_disk_in(&corpus_root.join("worker0").join(CORPUS_DIR), false).unwrap();
2340        master.push_corpus_entry(corpus);
2341
2342        let worker2_sync = corpus_root.join("worker2").join(SYNC_DIR);
2343        fs::create_dir_all(&worker2_sync).unwrap();
2344        let worker2_destination = worker2_sync.join(&name);
2345        foundry_common::fs::write_json_file(
2346            &worker2_destination,
2347            &vec![basic_tx_with_calldata([2])],
2348        )
2349        .unwrap();
2350
2351        master.export_to_workers(3).unwrap();
2352        assert_eq!(master.new_entry_indices, [0]);
2353        assert!(corpus_root.join("worker1").join(SYNC_DIR).join(&name).is_file());
2354
2355        fs::remove_file(&worker2_destination).unwrap();
2356        master.export_to_workers(3).unwrap();
2357
2358        assert!(master.new_entry_indices.is_empty());
2359        assert!(worker2_destination.is_file());
2360    }
2361
2362    #[test]
2363    fn final_sync_coordinator_yields_to_nested_rayon_workers() {
2364        rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap().install(|| {
2365            (0..2usize).into_par_iter().for_each(|_| {
2366                let coordinator = CorpusSyncCoordinator::new(2);
2367                (0..2usize).into_par_iter().for_each(|_| assert!(coordinator.wait()));
2368            });
2369        });
2370    }
2371
2372    #[test]
2373    fn final_calibration_rejects_non_file_corpus_entry() {
2374        let corpus_root = temp_corpus_dir();
2375        let mut worker = empty_worker_corpus(1, corpus_root.clone());
2376        let sync_dir = corpus_root.join("worker1").join(SYNC_DIR);
2377        fs::create_dir(sync_dir.join("00000000-0000-0000-0000-000000000001-1.json")).unwrap();
2378        let target = Address::repeat_byte(0x11);
2379        let executor = sync_test_executor(corpus_root, target);
2380        let function = Function::parse("test(uint256)").unwrap();
2381
2382        let err = worker
2383            .calibrate(
2384                &executor,
2385                ReplayTarget {
2386                    stateless: Some(StatelessReplayTarget { function: &function, address: target }),
2387                    fuzzed_contracts: None,
2388                    dynamic: None,
2389                    senders: None,
2390                },
2391                true,
2392            )
2393            .unwrap_err()
2394            .to_string();
2395
2396        assert!(err.contains("not a regular file"), "{err}");
2397    }
2398
2399    #[test]
2400    fn final_sync_completes_interleaved_corpus_lifecycle() {
2401        let corpus_root = temp_corpus_dir();
2402        let function = Function::parse("test(uint256)").unwrap();
2403        let target_address = Address::repeat_byte(0x11);
2404        let startup = CorpusEntry::new(vec![tx_for_function(
2405            target_address,
2406            &function,
2407            &[DynSolValue::Uint(U256::ZERO, 256)],
2408        )]);
2409        let startup_name = startup.file_name(false);
2410        fs::create_dir_all(corpus_root.join("worker1").join(CORPUS_DIR)).unwrap();
2411        startup.write_to_disk_in(&corpus_root.join("worker1").join(CORPUS_DIR), false).unwrap();
2412        let coordinator = Arc::new(CorpusSyncCoordinator::new(3));
2413        let setup = Arc::new(std::sync::Barrier::new(3));
2414
2415        std::thread::scope(|scope| {
2416            let root = corpus_root.clone();
2417            let master_startup_name = startup_name.clone();
2418            let master_coordinator = coordinator.clone();
2419            let master_setup = setup.clone();
2420            scope.spawn(move || {
2421                let function = Function::parse("test(uint256)").unwrap();
2422                let seed = WorkerCorpusSeed {
2423                    replay_dirs: Some(canonical_replay_dirs(&root)),
2424                    ..Default::default()
2425                };
2426                let mut master = worker_corpus(0, root.clone(), seed);
2427                let periodic = CorpusEntry::new(vec![tx_for_function(
2428                    target_address,
2429                    &function,
2430                    &[DynSolValue::Uint(U256::ZERO, 256)],
2431                )]);
2432                let periodic_name = periodic.file_name(false);
2433                periodic.write_to_disk_in(&root.join("worker0").join(CORPUS_DIR), false).unwrap();
2434                master.push_corpus_entry(periodic);
2435
2436                // The periodic fan-out reaches worker 1 but remains pending for worker 2.
2437                let blocked = root.join("worker2").join(SYNC_DIR).join(&periodic_name);
2438                fs::create_dir_all(&blocked).unwrap();
2439                master.export_to_workers(3).unwrap();
2440                assert_eq!(master.new_entry_indices, [0]);
2441                assert!(root.join("worker2").join(SYNC_DIR).join(master_startup_name).is_file());
2442                fs::remove_dir(blocked).unwrap();
2443                master_setup.wait();
2444
2445                finalize_test_worker(&mut master, root, target_address, &master_coordinator);
2446            });
2447
2448            for id in 1..=2 {
2449                let root = corpus_root.clone();
2450                let coordinator = coordinator.clone();
2451                let setup = setup.clone();
2452                scope.spawn(move || {
2453                    let mut worker = empty_worker_corpus(id, root.clone());
2454                    setup.wait();
2455
2456                    if id == 1 {
2457                        // This old-timestamp finding appears after the last periodic sync.
2458                        let function = Function::parse("test(uint256)").unwrap();
2459                        let mut late = CorpusEntry::new(vec![tx_for_function(
2460                            target_address,
2461                            &function,
2462                            &[DynSolValue::Uint(U256::ONE, 256)],
2463                        )]);
2464                        late.timestamp = 0;
2465                        worker.push_corpus_entry(late);
2466                    }
2467
2468                    finalize_test_worker(&mut worker, root, target_address, &coordinator);
2469                });
2470            }
2471        });
2472
2473        assert_eq!(read_corpus_dir(&corpus_root.join("worker2").join(CORPUS_DIR)).count(), 2);
2474        assert!(read_corpus_dir(&corpus_root.join("worker2").join(SYNC_DIR)).next().is_none());
2475    }
2476
2477    #[test]
2478    fn master_distributes_startup_corpus_only_once() {
2479        let corpus_root = temp_corpus_dir();
2480        let corpus = CorpusEntry::new(vec![basic_tx()]);
2481        let name = corpus.file_name(false);
2482        let non_master_corpus = corpus_root.join("worker1").join(CORPUS_DIR);
2483        fs::create_dir_all(&non_master_corpus).unwrap();
2484        corpus.write_to_disk_in(&non_master_corpus, false).unwrap();
2485        let seed = WorkerCorpusSeed {
2486            replay_dirs: Some(canonical_replay_dirs(&corpus_root)),
2487            ..Default::default()
2488        };
2489        let mut master = worker_corpus(0, corpus_root.clone(), seed);
2490
2491        let other_worker_sync_dir = corpus_root.join("worker2").join(SYNC_DIR);
2492        fs::create_dir_all(&other_worker_sync_dir).unwrap();
2493        let other_worker_sync = other_worker_sync_dir.join(&name);
2494        foundry_common::fs::write_json_file(&other_worker_sync, &vec![basic_tx_with_calldata([1])])
2495            .unwrap();
2496
2497        master.export_to_workers(3).unwrap();
2498        let source_worker_sync = corpus_root.join("worker1").join(SYNC_DIR).join(&name);
2499        assert!(source_worker_sync.is_file());
2500        assert!(other_worker_sync.is_file());
2501        assert!(master.initial_export_dirs.is_some());
2502
2503        fs::remove_file(&other_worker_sync).unwrap();
2504        master.export_to_workers(3).unwrap();
2505        assert!(other_worker_sync.is_file());
2506        assert!(master.initial_export_dirs.is_none());
2507
2508        let source_entry = read_corpus_dir(source_worker_sync.parent().unwrap()).next().unwrap();
2509        assert!(accept_synced_corpus_file(
2510            &source_entry,
2511            &source_entry.read_tx_seq().unwrap(),
2512            &non_master_corpus.join(&name),
2513        ));
2514        assert!(!source_worker_sync.exists());
2515        assert!(non_master_corpus.join(&name).is_file());
2516
2517        fs::remove_file(&other_worker_sync).unwrap();
2518        master.export_to_workers(3).unwrap();
2519        assert!(!other_worker_sync.exists());
2520
2521        let flat_root = temp_corpus_dir();
2522        let flat_corpus = CorpusEntry::new(vec![basic_tx()]);
2523        let flat_name = flat_corpus.file_name(false);
2524        flat_corpus.write_to_disk_in(&flat_root, false).unwrap();
2525        let config = corpus_config(flat_root.clone());
2526        let seed = WorkerCorpusSeed::load_from_disk::<foundry_evm_core::evm::EthEvmNetwork>(
2527            &config,
2528            None,
2529            None,
2530            ReplayTarget { stateless: None, fuzzed_contracts: None, dynamic: None, senders: None },
2531        )
2532        .unwrap();
2533        fs::create_dir_all(flat_root.join("worker1").join(CORPUS_DIR)).unwrap();
2534        let generator =
2535            test_sequence(&config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
2536        let mut flat_master = WorkerCorpus::from_seed(0, config, generator, seed).unwrap();
2537        flat_master.worker_sync_enabled = true;
2538        let pending = CorpusEntry::new(vec![basic_tx_with_calldata([1])]);
2539        let pending_name = pending.file_name(false);
2540        pending.write_to_disk_in(&flat_root.join("worker0").join(CORPUS_DIR), false).unwrap();
2541        flat_master.push_corpus_entry(pending);
2542
2543        flat_master.export_to_workers(2).unwrap();
2544
2545        assert!(flat_root.join("worker1").join(SYNC_DIR).join(flat_name).is_file());
2546        assert!(flat_root.join("worker1").join(SYNC_DIR).join(pending_name).is_file());
2547        assert!(flat_master.new_entry_indices.is_empty());
2548    }
2549
2550    #[test]
2551    fn pending_master_fanout_entries_are_not_evicted() {
2552        let corpus_root = temp_corpus_dir();
2553        let mut master = empty_worker_corpus(0, corpus_root.clone());
2554        master.initial_export_dirs = None;
2555        let mut pending = CorpusEntry::new(vec![basic_tx_with_calldata([1])]);
2556        pending.total_mutations = 1;
2557        let retained = CorpusEntry::new(vec![basic_tx_with_calldata([2])]);
2558        let pending_name = pending.file_name(false);
2559        let retained_name = retained.file_name(false);
2560        let pending_timestamp = pending.timestamp;
2561        let retained_timestamp = retained.timestamp;
2562        let master_corpus = corpus_root.join("worker0").join(CORPUS_DIR);
2563        pending.write_to_disk_in(&master_corpus, false).unwrap();
2564        retained.write_to_disk_in(&master_corpus, false).unwrap();
2565        master.push_synced_corpus_entry(pending, pending_timestamp, pending_name.clone());
2566        master.push_synced_corpus_entry(retained, retained_timestamp, retained_name.clone());
2567
2568        master.evict_oldest_corpus().unwrap();
2569        master.export_to_workers(2).unwrap();
2570
2571        let worker_sync = corpus_root.join("worker1").join(SYNC_DIR);
2572        assert!(worker_sync.join(pending_name).is_file());
2573        assert!(worker_sync.join(retained_name).is_file());
2574    }
2575
2576    #[test]
2577    fn campaign_processing_writes_worker_file_immediately() {
2578        let corpus_root = temp_corpus_dir();
2579        let worker_subdir = corpus_root.join("worker1");
2580        let mut manager = empty_worker_corpus(1, corpus_root);
2581
2582        manager.process_inputs_for_campaign(&[basic_tx()], &[], true, None);
2583
2584        assert_eq!(manager.in_memory_corpus.len(), 1);
2585        assert_eq!(manager.metrics.corpus_count, 1);
2586        assert_eq!(read_corpus_dir(&worker_subdir.join(CORPUS_DIR)).count(), 1);
2587    }
2588
2589    /// `RawCallResult` carrying a single edge hit, to drive `merge_edge_coverage` without the EVM.
2590    fn edge_call(edge: EdgeKey, count: u8) -> RawCallResult {
2591        RawCallResult {
2592            edge_coverage: Some(EdgeCoverage::CollisionFree(vec![EdgeCovHit { edge, count }])),
2593            ..Default::default()
2594        }
2595    }
2596
2597    #[test]
2598    fn merge_edge_coverage_advances_timer_only_for_new_edges() {
2599        let corpus_root = temp_corpus_dir();
2600        let mut manager = empty_worker_corpus(1, corpus_root);
2601
2602        // No edge seen yet.
2603        assert!(manager.time_since_new_edge().is_none());
2604        assert_eq!(manager.metrics.cumulative_edges_seen, 0);
2605
2606        let edge =
2607            EdgeKey { address: Address::ZERO, depth: None, pc: 0, jump_dest: U256::from(10) };
2608
2609        // First-time edge starts the timer.
2610        assert!(manager.merge_edge_coverage(&mut edge_call(edge, 1)));
2611        let first = manager.last_new_edge_at.expect("timer set after first new edge");
2612        assert_eq!(manager.metrics.cumulative_edges_seen, 1);
2613
2614        // Same edge, higher bucket = a feature, not an edge: timer must not advance.
2615        assert!(manager.merge_edge_coverage(&mut edge_call(edge, 8)));
2616        assert_eq!(manager.last_new_edge_at, Some(first));
2617        assert_eq!(manager.metrics.cumulative_edges_seen, 1);
2618        assert_eq!(manager.metrics.cumulative_features_seen, 1);
2619
2620        // A distinct edge advances the timer.
2621        let other =
2622            EdgeKey { address: Address::ZERO, depth: None, pc: 1, jump_dest: U256::from(20) };
2623        assert!(manager.merge_edge_coverage(&mut edge_call(other, 1)));
2624        let second = manager.last_new_edge_at.expect("timer present");
2625        assert!(second >= first);
2626        assert_eq!(manager.metrics.cumulative_edges_seen, 2);
2627        assert!(manager.time_since_new_edge().is_some());
2628    }
2629
2630    #[test]
2631    fn empty_input_sequence_with_new_coverage_does_not_panic_or_insert() {
2632        // A run where every executed call was discarded (magic assume) or popped (reverts
2633        // without `fail_on_revert`, handler assertions) leaves no surviving inputs, yet
2634        // `new_coverage` can still be true because edge coverage is collected before the
2635        // input is popped. Processing must not panic and must not persist an entry.
2636        let corpus_root = temp_corpus_dir();
2637        let worker_subdir = corpus_root.join("worker1");
2638        let mut manager = empty_worker_corpus(1, corpus_root);
2639
2640        manager.process_inputs_for_campaign(&[], &[], true, None);
2641
2642        assert_eq!(manager.in_memory_corpus.len(), 0);
2643        assert_eq!(manager.metrics.corpus_count, 0);
2644        assert_eq!(read_corpus_dir(&worker_subdir.join(CORPUS_DIR)).count(), 0);
2645
2646        // Live processing path must also tolerate the empty sequence.
2647        manager.process_inputs(&[], &[], true, None);
2648        assert_eq!(manager.in_memory_corpus.len(), 0);
2649        assert_eq!(read_corpus_dir(&worker_subdir.join(CORPUS_DIR)).count(), 0);
2650    }
2651
2652    #[test]
2653    fn campaign_processing_defers_only_optimization_persistence() {
2654        let corpus_root = temp_corpus_dir();
2655        let mut manager = empty_worker_corpus(1, corpus_root.clone());
2656        let sequence = vec![basic_tx()];
2657        manager.process_inputs_for_campaign(
2658            &sequence,
2659            &[],
2660            false,
2661            Some((I256::try_from(7).unwrap(), sequence.clone())),
2662        );
2663
2664        let worker_corpus_dir = corpus_root.join("worker1").join(CORPUS_DIR);
2665        let entries = read_corpus_dir(&worker_corpus_dir).collect::<Vec<_>>();
2666        assert_eq!(entries.len(), 1);
2667        let persisted_sequence = entries[0].read_tx_seq().unwrap();
2668        assert_eq!(persisted_sequence.len(), sequence.len());
2669        assert_eq!(persisted_sequence[0].sender, sequence[0].sender);
2670        assert_eq!(persisted_sequence[0].call_details.target, sequence[0].call_details.target);
2671        assert_eq!(persisted_sequence[0].call_details.calldata, sequence[0].call_details.calldata);
2672        assert!(!corpus_root.join(OPTIMIZATION_BEST_FILE).exists());
2673
2674        persist_campaign_optimization(
2675            &corpus_config(corpus_root.clone()),
2676            Some(I256::try_from(7).unwrap()),
2677            &sequence,
2678        );
2679
2680        let state: OptimizationState =
2681            foundry_common::fs::read_json_file(&corpus_root.join(OPTIMIZATION_BEST_FILE)).unwrap();
2682        assert_eq!(state.best_value, I256::try_from(7).unwrap());
2683        assert_eq!(state.best_sequence.len(), sequence.len());
2684        assert_eq!(state.best_sequence[0].sender, sequence[0].sender);
2685        assert_eq!(state.best_sequence[0].call_details.target, sequence[0].call_details.target);
2686        assert_eq!(state.best_sequence[0].call_details.calldata, sequence[0].call_details.calldata);
2687    }
2688
2689    #[test]
2690    fn persisted_worker_corpus_entries_are_deduped_by_uuid() {
2691        let corpus_root = temp_corpus_dir();
2692        let corpus = CorpusEntry::new(vec![basic_tx()]);
2693        let duplicate = corpus.clone();
2694
2695        let worker0_corpus = corpus_root.join("worker0").join(CORPUS_DIR);
2696        let worker1_corpus = corpus_root.join("worker1").join(CORPUS_DIR);
2697        fs::create_dir_all(&worker0_corpus).unwrap();
2698        fs::create_dir_all(&worker1_corpus).unwrap();
2699        corpus.write_to_disk_in(&worker0_corpus, false).unwrap();
2700        duplicate.write_to_disk_in(&worker1_corpus, false).unwrap();
2701
2702        let mut seen = HashSet::new();
2703        let entries = unique_corpus_entries(&canonical_replay_dirs(&corpus_root), &mut seen)
2704            .collect::<Vec<_>>();
2705
2706        assert_eq!(entries.len(), 1);
2707        assert_eq!(entries[0].uuid, corpus.uuid);
2708    }
2709
2710    #[test]
2711    fn corpus_entry_write_uses_unparsable_temp_file() {
2712        let corpus_dir = temp_corpus_dir();
2713        let corpus = CorpusEntry::new(vec![basic_tx()]);
2714        let temp_path =
2715            corpus_dir.join(format!(".{}.{}.tmp", corpus.file_name(false), Uuid::new_v4()));
2716        fs::write(&temp_path, b"{").unwrap();
2717
2718        let path = corpus.write_to_disk_in(&corpus_dir, false).unwrap();
2719        let entries = read_corpus_dir(&corpus_dir).collect::<Vec<_>>();
2720
2721        assert_eq!(entries.len(), 1);
2722        assert_eq!(entries[0].path, path);
2723        assert!(temp_path.exists());
2724    }
2725
2726    #[test]
2727    fn persist_corpus_seed_skips_duplicate_sequence() {
2728        let corpus_root = temp_corpus_dir();
2729        let config = corpus_config(corpus_root.clone());
2730        let sequence = vec![basic_tx_with_calldata(vec![0x12, 0x34])];
2731
2732        let first = persist_corpus_seed(&config, sequence.clone()).unwrap().unwrap();
2733        let second = persist_corpus_seed(&config, sequence).unwrap().unwrap();
2734        let entries =
2735            read_corpus_dir(&corpus_root.join("worker0").join(CORPUS_DIR)).collect::<Vec<_>>();
2736
2737        assert_eq!(first, second);
2738        assert_eq!(entries.len(), 1);
2739    }
2740
2741    #[test]
2742    fn non_master_campaign_worker_uses_persisted_optimization_baseline() {
2743        let corpus_root = temp_corpus_dir();
2744        let persisted_sequence = vec![basic_tx()];
2745        let persisted_state = OptimizationState {
2746            best_value: I256::try_from(100).unwrap(),
2747            best_sequence: persisted_sequence,
2748        };
2749        foundry_common::fs::write_json_file(
2750            &corpus_root.join(OPTIMIZATION_BEST_FILE),
2751            &persisted_state,
2752        )
2753        .unwrap();
2754        let config = corpus_config(corpus_root);
2755        let generator =
2756            test_sequence(&config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
2757        let mut manager = WorkerCorpus::new::<foundry_evm_core::evm::EthEvmNetwork>(
2758            1,
2759            config,
2760            generator,
2761            None,
2762            None,
2763            ReplayTarget { stateless: None, fuzzed_contracts: None, dynamic: None, senders: None },
2764        )
2765        .unwrap();
2766
2767        let worse_sequence = vec![basic_tx()];
2768        manager.process_inputs_for_campaign(
2769            &worse_sequence,
2770            &[],
2771            false,
2772            Some((I256::try_from(50).unwrap(), worse_sequence.clone())),
2773        );
2774
2775        let better_sequence = vec![basic_tx()];
2776        manager.process_inputs_for_campaign(
2777            &better_sequence,
2778            &[],
2779            false,
2780            Some((I256::try_from(150).unwrap(), better_sequence.clone())),
2781        );
2782        assert_eq!(manager.optimization_best_value, Some(I256::try_from(150).unwrap()));
2783    }
2784
2785    #[test]
2786    fn worker_can_initialize_from_warmed_seed() {
2787        let corpus_root = temp_corpus_dir();
2788        let tx_seq = vec![basic_tx()];
2789        let seed = WorkerCorpusSeed {
2790            in_memory_corpus: vec![CorpusEntry::new(tx_seq.clone())],
2791            history_map: vec![1, 2, 3],
2792            edge_indices: EdgeIndexMap::default(),
2793            sancov_history_map: vec![4, 5],
2794            metrics: CorpusMetrics {
2795                cumulative_edges_seen: 7,
2796                cumulative_features_seen: 11,
2797                corpus_count: 1,
2798                favored_items: 0,
2799            },
2800            replay_dirs: None,
2801            failed_replays: 13,
2802            optimization_best_value: Some(I256::try_from(17).unwrap()),
2803            optimization_best_sequence: tx_seq,
2804            last_new_edge_at: None,
2805        };
2806
2807        let config = corpus_config(corpus_root);
2808        let generator =
2809            test_sequence(&config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
2810        let manager = WorkerCorpus::from_seed(1, config, generator, seed).unwrap();
2811
2812        assert_eq!(manager.in_memory_corpus.len(), 1);
2813        assert_eq!(manager.history_map, vec![1, 2, 3]);
2814        assert_eq!(manager.sancov_history_map, vec![4, 5]);
2815        assert_eq!(manager.metrics.cumulative_edges_seen, 7);
2816        assert_eq!(manager.metrics.cumulative_features_seen, 11);
2817        assert_eq!(manager.metrics.corpus_count, 1);
2818        assert_eq!(manager.failed_replays, 13);
2819        let (value, sequence) = manager.optimization_initial_state();
2820        assert_eq!(value, Some(I256::try_from(17).unwrap()));
2821        assert_eq!(sequence.len(), 1);
2822    }
2823
2824    #[test]
2825    fn clone_for_worker_shards_warmed_corpus_and_recomputes_metrics() {
2826        let entries = (0..10)
2827            .map(|idx| {
2828                let mut entry = CorpusEntry::new(vec![basic_tx()]);
2829                entry.is_favored = idx % 2 == 0;
2830                entry
2831            })
2832            .collect::<Vec<_>>();
2833        let entry_ids = entries.iter().map(|entry| entry.uuid).collect::<Vec<_>>();
2834        let seed = WorkerCorpusSeed {
2835            in_memory_corpus: entries,
2836            history_map: vec![1, 2, 3],
2837            edge_indices: EdgeIndexMap::default(),
2838            sancov_history_map: vec![4, 5],
2839            metrics: CorpusMetrics {
2840                cumulative_edges_seen: 7,
2841                cumulative_features_seen: 11,
2842                corpus_count: 10,
2843                favored_items: 5,
2844            },
2845            replay_dirs: None,
2846            failed_replays: 13,
2847            optimization_best_value: Some(I256::try_from(17).unwrap()),
2848            optimization_best_sequence: vec![basic_tx()],
2849            last_new_edge_at: None,
2850        };
2851
2852        let worker_count = 3;
2853        let shards = (0..worker_count)
2854            .map(|worker_id| seed.clone_for_worker(worker_id, worker_count, true))
2855            .collect::<Vec<_>>();
2856        let mut sharded_ids = shards
2857            .iter()
2858            .flat_map(|shard| shard.in_memory_corpus.iter().map(|entry| entry.uuid))
2859            .collect::<Vec<_>>();
2860        let mut expected_ids = entry_ids.clone();
2861        sharded_ids.sort_unstable();
2862        expected_ids.sort_unstable();
2863
2864        assert_eq!(sharded_ids, expected_ids);
2865        assert_eq!(
2866            shards[0].in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
2867            [entry_ids[0], entry_ids[3], entry_ids[6], entry_ids[9]]
2868        );
2869        assert_eq!(
2870            shards[1].in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
2871            [entry_ids[1], entry_ids[4], entry_ids[7]]
2872        );
2873        assert_eq!(
2874            shards[2].in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
2875            [entry_ids[2], entry_ids[5], entry_ids[8]]
2876        );
2877        assert_eq!(
2878            shards.iter().map(|shard| shard.in_memory_corpus.len()).collect::<Vec<_>>(),
2879            [4, 3, 3]
2880        );
2881        assert_eq!(
2882            shards.iter().map(|shard| shard.metrics.corpus_count).collect::<Vec<_>>(),
2883            [4, 3, 3]
2884        );
2885        assert_eq!(
2886            shards.iter().map(|shard| shard.metrics.favored_items).collect::<Vec<_>>(),
2887            [2, 1, 2]
2888        );
2889        assert!(shards.iter().all(|shard| shard.history_map == seed.history_map));
2890        assert!(shards.iter().all(|shard| shard.sancov_history_map == seed.sancov_history_map));
2891        assert!(shards.iter().all(|shard| shard.metrics.cumulative_edges_seen == 7));
2892        assert!(shards.iter().all(|shard| shard.metrics.cumulative_features_seen == 11));
2893    }
2894
2895    #[test]
2896    fn clone_for_worker_can_strip_cmp_sequences() {
2897        let cmp = CmpOperands {
2898            op1: U256::ONE,
2899            op2: U256::from(2),
2900            pc: 3,
2901            address: Address::ZERO,
2902            opcode: 0,
2903        };
2904        let entries = (0..2)
2905            .map(|_| {
2906                CorpusEntry::new_with_cmp(
2907                    vec![basic_tx()],
2908                    vec![vec![ComparisonHint { lhs: cmp.op1, rhs: cmp.op2 }]],
2909                    Uuid::new_v4(),
2910                )
2911            })
2912            .collect::<Vec<_>>();
2913        let seed = WorkerCorpusSeed { in_memory_corpus: entries, ..Default::default() };
2914
2915        let with_cmp = seed.clone_for_worker(0, 1, true);
2916        let without_cmp = seed.clone_for_worker(0, 1, false);
2917
2918        assert!(with_cmp.in_memory_corpus.iter().all(|entry| !entry.cmp_seq[0].is_empty()));
2919        assert!(without_cmp.in_memory_corpus.iter().all(|entry| entry.cmp_seq.is_empty()));
2920    }
2921
2922    #[test]
2923    fn retain_replayable_removes_off_target_corpus_entries() {
2924        let target = Address::repeat_byte(0x11);
2925        let foo = Function::parse("foo()").unwrap();
2926        let bar = Function::parse("bar()").unwrap();
2927        let foo_selector = foo.selector();
2928        let foo_tx = tx_for_function(target, &foo, &[]);
2929        let bar_tx = tx_for_function(target, &bar, &[]);
2930        let mut foo_entry = CorpusEntry::new(vec![foo_tx.clone()]);
2931        foo_entry.is_favored = true;
2932        let mut bar_entry = CorpusEntry::new(vec![bar_tx.clone()]);
2933        bar_entry.is_favored = true;
2934        let mut seed = WorkerCorpusSeed {
2935            in_memory_corpus: vec![foo_entry, bar_entry],
2936            metrics: CorpusMetrics { corpus_count: 2, favored_items: 2, ..Default::default() },
2937            optimization_best_value: Some(I256::try_from(17).unwrap()),
2938            optimization_best_sequence: vec![bar_tx],
2939            ..Default::default()
2940        };
2941        let targeted_contracts =
2942            targeted_contracts_with_selective_functions(target, vec![foo, bar], [foo_selector]);
2943        let targets = targeted_contracts.targets();
2944
2945        seed.retain_replayable(&targets);
2946
2947        assert_eq!(seed.in_memory_corpus.len(), 1);
2948        assert_eq!(seed.in_memory_corpus[0].tx_seq.len(), 1);
2949        assert_eq!(
2950            seed.in_memory_corpus[0].tx_seq[0].call_details.target,
2951            foo_tx.call_details.target
2952        );
2953        assert_eq!(
2954            seed.in_memory_corpus[0].tx_seq[0].call_details.calldata,
2955            foo_tx.call_details.calldata
2956        );
2957        assert_eq!(seed.metrics.corpus_count, 1);
2958        assert_eq!(seed.metrics.favored_items, 1);
2959        assert!(seed.optimization_best_value.is_none());
2960        assert!(seed.optimization_best_sequence.is_empty());
2961    }
2962
2963    #[test]
2964    fn hoist_observed_calls_bundles_replayable_subcalls_into_one_corpus_entry() {
2965        let target = Address::repeat_byte(0x42);
2966        let other = Address::repeat_byte(0x43);
2967        let sender = Address::repeat_byte(0xaa);
2968        let observed_caller = Address::repeat_byte(0xbb);
2969        let foo = Function::parse("foo(uint256)").unwrap();
2970        let bar = Function::parse("bar()").unwrap();
2971        let foo_selector = foo.selector();
2972        let bar_selector = bar.selector();
2973        let targeted_contracts = targeted_contracts_with_selective_functions(
2974            target,
2975            vec![foo, bar],
2976            [foo_selector, bar_selector],
2977        );
2978
2979        let mut foo_calldata = vec![0u8; 36];
2980        foo_calldata[..4].copy_from_slice(&foo_selector[..]);
2981        let bar_calldata = bar_selector.to_vec();
2982        let mut unknown_selector = vec![0u8; 36];
2983        unknown_selector[..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
2984        let value = U256::ONE;
2985
2986        let observed = vec![
2987            ObservedCall {
2988                depth: 1,
2989                caller: observed_caller,
2990                target: other,
2991                calldata: Bytes::from(foo_calldata.clone()),
2992                value: Some(value),
2993            },
2994            ObservedCall {
2995                depth: 1,
2996                caller: observed_caller,
2997                target,
2998                calldata: Bytes::from(foo_calldata),
2999                value: None,
3000            },
3001            ObservedCall {
3002                depth: 2,
3003                caller: observed_caller,
3004                target,
3005                calldata: Bytes::from(bar_calldata),
3006                value: None,
3007            },
3008            ObservedCall {
3009                depth: 1,
3010                caller: observed_caller,
3011                target,
3012                calldata: Bytes::from(unknown_selector),
3013                value: None,
3014            },
3015            ObservedCall {
3016                depth: 1,
3017                caller: observed_caller,
3018                target,
3019                calldata: Bytes::from(vec![0u8; 3]),
3020                value: None,
3021            },
3022        ];
3023        let parent_tx = BasicTxDetails {
3024            warp: Some(U256::from(123)),
3025            roll: Some(U256::from(456)),
3026            sender,
3027            call_details: CallDetails {
3028                target: Address::repeat_byte(0x99),
3029                calldata: Bytes::new(),
3030                value: None,
3031            },
3032        };
3033        let mut manager = empty_worker_corpus(0, temp_corpus_dir());
3034
3035        manager.hoist_observed_calls(
3036            &observed,
3037            &parent_tx,
3038            &targeted_contracts,
3039            &SenderFilters::default(),
3040            CorpusInsertionMode::Live,
3041        );
3042
3043        assert_eq!(manager.in_memory_corpus.len(), 1);
3044        assert_eq!(manager.metrics.corpus_count, 1);
3045
3046        let entry = manager.in_memory_corpus.last().unwrap();
3047        assert_eq!(entry.tx_seq.len(), 2);
3048        let tx = &entry.tx_seq[0];
3049        assert_eq!(tx.warp, parent_tx.warp);
3050        assert_eq!(tx.roll, parent_tx.roll);
3051        assert_eq!(tx.sender, observed_caller);
3052        assert_eq!(tx.call_details.target, target);
3053        assert_eq!(&tx.call_details.calldata[..4], &foo_selector[..]);
3054        assert_eq!(tx.call_details.value, None);
3055
3056        let tx = &entry.tx_seq[1];
3057        assert_eq!(tx.warp, None);
3058        assert_eq!(tx.roll, None);
3059        assert_eq!(tx.sender, observed_caller);
3060        assert_eq!(tx.call_details.target, target);
3061        assert_eq!(&tx.call_details.calldata[..4], &bar_selector[..]);
3062        assert_eq!(tx.call_details.value, None);
3063    }
3064
3065    #[test]
3066    fn hoist_observed_calls_persists_immediately() {
3067        let target = Address::repeat_byte(0x42);
3068        let foo = Function::parse("foo()").unwrap();
3069        let selector = foo.selector();
3070        let targeted_contracts = targeted_contracts_with_selective_functions(target, vec![foo], []);
3071        let observed = vec![ObservedCall {
3072            depth: 1,
3073            caller: Address::repeat_byte(0xaa),
3074            target,
3075            calldata: selector.into(),
3076            value: None,
3077        }];
3078        let corpus_root = temp_corpus_dir();
3079        let worker_corpus_dir = corpus_root.join("worker1").join(CORPUS_DIR);
3080        let mut manager = empty_worker_corpus(1, corpus_root);
3081
3082        manager.hoist_observed_calls(
3083            &observed,
3084            &basic_tx(),
3085            &targeted_contracts,
3086            &SenderFilters::default(),
3087            CorpusInsertionMode::Live,
3088        );
3089
3090        assert_eq!(manager.in_memory_corpus.len(), 1);
3091        assert_eq!(read_corpus_dir(&worker_corpus_dir).count(), 1);
3092    }
3093
3094    #[test]
3095    fn hoist_observed_calls_skips_empty_or_non_coverage_guided_inputs() {
3096        let target = Address::repeat_byte(0x42);
3097        let foo = Function::parse("foo()").unwrap();
3098        let selector = foo.selector();
3099        let targeted_contracts = targeted_contracts_with_selective_functions(target, vec![foo], []);
3100        let observed = vec![ObservedCall {
3101            depth: 1,
3102            caller: Address::repeat_byte(0xaa),
3103            target,
3104            calldata: selector.into(),
3105            value: None,
3106        }];
3107
3108        let mut no_corpus_config = corpus_config(temp_corpus_dir());
3109        no_corpus_config.corpus_dir = None;
3110        let generator =
3111            test_sequence(&no_corpus_config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
3112        let mut manager =
3113            WorkerCorpus::from_seed(0, no_corpus_config, generator, WorkerCorpusSeed::default())
3114                .unwrap();
3115        manager.hoist_observed_calls(
3116            &observed,
3117            &basic_tx(),
3118            &targeted_contracts,
3119            &SenderFilters::default(),
3120            CorpusInsertionMode::Live,
3121        );
3122        assert!(manager.in_memory_corpus.is_empty());
3123
3124        let mut manager = empty_worker_corpus(0, temp_corpus_dir());
3125        manager.hoist_observed_calls(
3126            &[],
3127            &basic_tx(),
3128            &targeted_contracts,
3129            &SenderFilters::default(),
3130            CorpusInsertionMode::Live,
3131        );
3132        assert!(manager.in_memory_corpus.is_empty());
3133    }
3134
3135    #[test]
3136    fn sequence_from_test_trace_preserves_forward_environment_and_rejects_restores() {
3137        let target = Address::repeat_byte(0x42);
3138        let sender = Address::repeat_byte(0xaa);
3139        let foo = Function::parse("foo()").unwrap();
3140        let foo_selector = foo.selector();
3141        let targeted_contracts =
3142            targeted_contracts_with_selective_functions(target, vec![foo], [foo_selector]);
3143        let targets = targeted_contracts.targets();
3144        let target_call = || ObservedCall {
3145            depth: 1,
3146            caller: sender,
3147            target,
3148            calldata: foo_selector.into(),
3149            value: Some(U256::from(7)),
3150        };
3151        let cheatcode_call = |calldata| ObservedCall {
3152            depth: 1,
3153            caller: sender,
3154            target: CHEATCODE_ADDRESS,
3155            calldata: Bytes::from(calldata),
3156            value: None,
3157        };
3158        let observed = vec![
3159            cheatcode_call(warpCall { newTimestamp: U256::from(105) }.abi_encode()),
3160            target_call(),
3161            cheatcode_call(rollCall { newHeight: U256::from(12) }.abi_encode()),
3162            cheatcode_call(warpCall { newTimestamp: U256::from(110) }.abi_encode()),
3163            target_call(),
3164        ];
3165
3166        let sequence = sequence_from_test_trace(
3167            &observed,
3168            &targets,
3169            &SenderFilters::default(),
3170            U256::from(100),
3171            U256::from(10),
3172            SpecId::CANCUN,
3173        )
3174        .unwrap();
3175
3176        assert_eq!(sequence.len(), 2);
3177        assert_eq!(sequence[0].warp, Some(U256::from(5)));
3178        assert_eq!(sequence[0].roll, None);
3179        assert_eq!(sequence[1].warp, Some(U256::from(5)));
3180        assert_eq!(sequence[1].roll, Some(U256::from(2)));
3181        assert!(sequence.iter().all(|tx| {
3182            tx.sender == sender
3183                && tx.call_details.target == target
3184                && tx.call_details.value == Some(U256::from(7))
3185        }));
3186
3187        for spec_id in [SpecId::PRAGUE, SpecId::OSAKA] {
3188            assert!(
3189                sequence_from_test_trace(
3190                    &observed,
3191                    &targets,
3192                    &SenderFilters::default(),
3193                    U256::from(100),
3194                    U256::from(10),
3195                    spec_id,
3196                )
3197                .is_none()
3198            );
3199
3200            let unchanged_block = [
3201                cheatcode_call(warpCall { newTimestamp: U256::from(105) }.abi_encode()),
3202                cheatcode_call(rollCall { newHeight: U256::from(10) }.abi_encode()),
3203                target_call(),
3204            ];
3205            let sequence = sequence_from_test_trace(
3206                &unchanged_block,
3207                &targets,
3208                &SenderFilters::default(),
3209                U256::from(100),
3210                U256::from(10),
3211                spec_id,
3212            )
3213            .unwrap();
3214            assert_eq!(sequence.len(), 1);
3215            assert_eq!(sequence[0].warp, Some(U256::from(5)));
3216            assert_eq!(sequence[0].roll, None);
3217        }
3218
3219        let backwards =
3220            [cheatcode_call(warpCall { newTimestamp: U256::from(99) }.abi_encode()), target_call()];
3221        assert!(
3222            sequence_from_test_trace(
3223                &backwards,
3224                &targets,
3225                &SenderFilters::default(),
3226                U256::from(100),
3227                U256::from(10),
3228                SpecId::CANCUN
3229            )
3230            .is_none()
3231        );
3232
3233        for calldata in [
3234            revertToCall { snapshotId: U256::ONE }.abi_encode(),
3235            revertToStateCall { snapshotId: U256::ONE }.abi_encode(),
3236            revertToAndDeleteCall { snapshotId: U256::ONE }.abi_encode(),
3237            revertToStateAndDeleteCall { snapshotId: U256::ONE }.abi_encode(),
3238            setEvmVersionCall { evm: "prague".to_string() }.abi_encode(),
3239        ] {
3240            let restored = [
3241                cheatcode_call(warpCall { newTimestamp: U256::from(105) }.abi_encode()),
3242                cheatcode_call(rollCall { newHeight: U256::from(12) }.abi_encode()),
3243                cheatcode_call(calldata),
3244                target_call(),
3245            ];
3246            assert!(
3247                sequence_from_test_trace(
3248                    &restored,
3249                    &targets,
3250                    &SenderFilters::default(),
3251                    U256::from(100),
3252                    U256::from(10),
3253                    SpecId::CANCUN
3254                )
3255                .is_none()
3256            );
3257        }
3258    }
3259
3260    #[test]
3261    fn push_observed_sequence_live_persists_and_memory_only_does_not() {
3262        let corpus_root = temp_corpus_dir();
3263        let worker0_corpus_dir = corpus_root.join("worker0").join(CORPUS_DIR);
3264        let mut manager = empty_worker_corpus(0, corpus_root.clone());
3265
3266        manager.push_observed_sequence(vec![basic_tx()], CorpusInsertionMode::Live);
3267        assert_eq!(manager.in_memory_corpus.len(), 1);
3268        assert_eq!(read_corpus_dir(&worker0_corpus_dir).count(), 1);
3269
3270        let mut manager = empty_worker_corpus(1, corpus_root.clone());
3271        let worker1_corpus_dir = corpus_root.join("worker1").join(CORPUS_DIR);
3272        manager.push_observed_sequence(vec![basic_tx()], CorpusInsertionMode::MemoryOnly);
3273        assert_eq!(manager.in_memory_corpus.len(), 1);
3274        assert_eq!(read_corpus_dir(&worker1_corpus_dir).count(), 0);
3275    }
3276
3277    #[test]
3278    fn detects_legacy_invariant_corpus_dirs_without_matching_worker_dirs() {
3279        let corpus_root = temp_corpus_dir();
3280        fs::create_dir_all(corpus_root.join("worker0")).unwrap();
3281        assert!(!has_legacy_invariant_corpus_dirs(&corpus_root));
3282
3283        fs::create_dir_all(corpus_root.join("invariant_a")).unwrap();
3284        assert!(has_legacy_invariant_corpus_dirs(&corpus_root));
3285    }
3286
3287    #[test]
3288    fn ignores_optimization_invariant_corpus_dirs_when_detecting_legacy_dirs() {
3289        let corpus_root = temp_corpus_dir();
3290        fs::create_dir_all(corpus_root.join("worker0")).unwrap();
3291        let optimization_dir = corpus_root.join("invariant_optimize");
3292        fs::create_dir_all(optimization_dir.join("worker0")).unwrap();
3293        fs::write(optimization_dir.join(OPTIMIZATION_BEST_FILE), "{}").unwrap();
3294
3295        assert!(!has_legacy_invariant_corpus_dirs(&corpus_root));
3296
3297        fs::create_dir_all(corpus_root.join("invariant_legacy").join("worker0")).unwrap();
3298        assert!(has_legacy_invariant_corpus_dirs(&corpus_root));
3299    }
3300
3301    #[test]
3302    fn favored_sets_true_and_metrics_increment_when_ratio_gt_threshold() {
3303        let (mut manager, uuid) = new_manager_with_single_corpus();
3304        let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
3305        corpus.total_mutations = 4;
3306        corpus.new_finds_produced = 2; // ratio currently 0.5 if both increment → 3/5 = 0.6 > 0.3.
3307        corpus.is_favored = false;
3308
3309        // Ensure metrics start at 0.
3310        assert_eq!(manager.metrics.favored_items, 0);
3311
3312        // Mark this as the currently mutated corpus and process a run with new coverage.
3313        manager.current_mutated_index = Some(0);
3314        manager.process_inputs(&[basic_tx()], &[], true, None);
3315
3316        let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
3317        assert!(corpus.is_favored, "expected favored to be true when ratio > threshold");
3318        assert_eq!(
3319            manager.metrics.favored_items, 1,
3320            "favored_items should increment on false→true"
3321        );
3322    }
3323
3324    #[test]
3325    fn favored_sets_false_and_metrics_decrement_when_ratio_lt_threshold() {
3326        let (mut manager, uuid) = new_manager_with_single_corpus();
3327        let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
3328        corpus.total_mutations = 9;
3329        corpus.new_finds_produced = 3; // 3/9 = 0.333.. > 0.3; after +1: 3/10 = 0.3 => not favored.
3330        corpus.is_favored = true; // Start as favored.
3331
3332        manager.metrics.favored_items = 1;
3333
3334        // Next run does NOT produce coverage → only total_mutations increments, ratio drops.
3335        manager.current_mutated_index = Some(0);
3336        manager.process_inputs(&[basic_tx()], &[], false, None);
3337
3338        let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
3339        assert!(!corpus.is_favored, "expected favored to be false when ratio < threshold");
3340        assert_eq!(
3341            manager.metrics.favored_items, 0,
3342            "favored_items should decrement on true→false"
3343        );
3344    }
3345
3346    #[test]
3347    fn favored_is_false_on_ratio_equal_threshold() {
3348        let (mut manager, uuid) = new_manager_with_single_corpus();
3349        let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
3350        // After this call with new_coverage=true, totals become 10 and 3 → 0.3.
3351        corpus.total_mutations = 9;
3352        corpus.new_finds_produced = 2;
3353        corpus.is_favored = false;
3354
3355        manager.current_mutated_index = Some(0);
3356        manager.process_inputs(&[basic_tx()], &[], true, None);
3357
3358        let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
3359        assert!(
3360            !(corpus.is_favored),
3361            "with strict '>' comparison, favored must be false when ratio == threshold"
3362        );
3363    }
3364
3365    #[test]
3366    fn eviction_skips_favored_and_evicts_non_favored() {
3367        // Manager with two corpora.
3368        let mut favored = CorpusEntry::new(vec![basic_tx()]);
3369        favored.total_mutations = 2;
3370        favored.is_favored = true;
3371
3372        let mut non_favored = CorpusEntry::new(vec![basic_tx()]);
3373        non_favored.total_mutations = 2;
3374        non_favored.is_favored = false;
3375        let non_favored_uuid = non_favored.uuid;
3376
3377        let mut manager = seeded_worker_corpus(0, temp_corpus_dir(), vec![favored, non_favored]);
3378
3379        // First eviction should remove the non-favored one.
3380        manager.evict_oldest_corpus().unwrap();
3381        assert_eq!(manager.in_memory_corpus.len(), 1);
3382        assert!(manager.in_memory_corpus.iter().all(|c| c.is_favored));
3383
3384        // Attempt eviction again: only favored remains → should not remove.
3385        manager.evict_oldest_corpus().unwrap();
3386        assert_eq!(manager.in_memory_corpus.len(), 1, "favored corpus must not be evicted");
3387
3388        // Ensure the evicted one was the non-favored uuid.
3389        assert!(manager.in_memory_corpus.iter().all(|c| c.uuid != non_favored_uuid));
3390    }
3391
3392    #[test]
3393    fn non_synchronizing_entries_remain_evictable() {
3394        let corpus_root = temp_corpus_dir();
3395        let config = corpus_config(corpus_root);
3396        let generator =
3397            test_sequence(&config, TxGenerator::from_strategy(Just(basic_tx()).boxed()));
3398        let mut manager =
3399            WorkerCorpus::from_seed(0, config, generator, WorkerCorpusSeed::default()).unwrap();
3400        let mut evictable = CorpusEntry::new(vec![basic_tx_with_calldata([1])]);
3401        evictable.total_mutations = 1;
3402        let evictable_uuid = evictable.uuid;
3403        manager.push_corpus_entry(evictable);
3404        manager.push_corpus_entry(CorpusEntry::new(vec![basic_tx_with_calldata([2])]));
3405
3406        manager.evict_oldest_corpus().unwrap();
3407
3408        assert_eq!(manager.in_memory_corpus.len(), 1);
3409        assert!(manager.in_memory_corpus.iter().all(|entry| entry.uuid != evictable_uuid));
3410        assert!(manager.new_entry_indices.is_empty());
3411    }
3412
3413    #[test]
3414    fn invariant_load_drops_entries_outside_current_targets_or_senders() {
3415        let corpus_root = temp_corpus_dir();
3416        let target = Address::repeat_byte(0x11);
3417        let allowed = Address::repeat_byte(0xaa);
3418        let excluded = Address::repeat_byte(0xbb);
3419        let foo = Function::parse("foo()").unwrap();
3420        let bar = Function::parse("bar()").unwrap();
3421        let foo_selector = foo.selector();
3422        let tx = |sender, function: &Function| BasicTxDetails {
3423            sender,
3424            ..tx_for_function(target, function, &[])
3425        };
3426
3427        let kept = CorpusEntry::new(vec![tx(allowed, &foo)]);
3428        let kept_uuid = kept.uuid;
3429        for entry in [
3430            kept,
3431            CorpusEntry::new(vec![tx(excluded, &foo)]),
3432            CorpusEntry::new(vec![tx(allowed, &bar)]),
3433            CorpusEntry::new(vec![tx(allowed, &foo), tx(excluded, &foo)]),
3434        ] {
3435            entry.write_to_disk_in(&corpus_root, false).unwrap();
3436        }
3437        foundry_common::fs::write_json_file(
3438            &corpus_root.join(OPTIMIZATION_BEST_FILE),
3439            &OptimizationState {
3440                best_value: I256::try_from(100).unwrap(),
3441                best_sequence: vec![tx(excluded, &foo)],
3442            },
3443        )
3444        .unwrap();
3445
3446        let targeted_contracts =
3447            targeted_contracts_with_selective_functions(target, vec![foo, bar], [foo_selector]);
3448        let senders = SenderFilters::new(vec![allowed], vec![]);
3449        let executor = sync_test_executor(corpus_root.clone(), target);
3450        let seed = WorkerCorpusSeed::load_from_disk(
3451            &corpus_config(corpus_root),
3452            None,
3453            Some(&executor),
3454            ReplayTarget {
3455                stateless: None,
3456                fuzzed_contracts: Some(&targeted_contracts),
3457                dynamic: None,
3458                senders: Some(&senders),
3459            },
3460        )
3461        .unwrap();
3462
3463        assert_eq!(
3464            seed.in_memory_corpus.iter().map(|entry| entry.uuid).collect::<Vec<_>>(),
3465            [kept_uuid]
3466        );
3467        assert_eq!(seed.metrics.corpus_count, 1);
3468        assert!(seed.optimization_best_value.is_none());
3469        assert!(seed.optimization_best_sequence.is_empty());
3470    }
3471
3472    #[test]
3473    fn observed_sequences_skip_disallowed_senders() {
3474        let target = Address::repeat_byte(0x42);
3475        let allowed = Address::repeat_byte(0xaa);
3476        let handler = Address::repeat_byte(0xbb);
3477        let foo = Function::parse("foo()").unwrap();
3478        let foo_selector = foo.selector();
3479        let targeted_contracts =
3480            targeted_contracts_with_selective_functions(target, vec![foo], [foo_selector]);
3481        let targets = targeted_contracts.targets();
3482        let call = |caller| ObservedCall {
3483            depth: 1,
3484            caller,
3485            target,
3486            calldata: foo_selector.into(),
3487            value: None,
3488        };
3489        let observed = [call(handler), call(allowed)];
3490
3491        // Cover both `targetSenders` and `excludeSenders` policies.
3492        for senders in
3493            [SenderFilters::new(vec![allowed], vec![]), SenderFilters::new(vec![], vec![handler])]
3494        {
3495            for sequence in [
3496                sequence_from_observed(&observed, &targets, &senders, None),
3497                sequence_from_test_trace(
3498                    &observed,
3499                    &targets,
3500                    &senders,
3501                    U256::ZERO,
3502                    U256::ZERO,
3503                    SpecId::CANCUN,
3504                )
3505                .unwrap(),
3506            ] {
3507                assert_eq!(sequence.iter().map(|tx| tx.sender).collect::<Vec<_>>(), [allowed]);
3508            }
3509        }
3510    }
3511}