1use 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
94const GZIP_THRESHOLD: usize = 4 * 1024;
97
98#[derive(Clone, Serialize, Deserialize)]
100struct OptimizationState {
101 best_value: I256,
102 best_sequence: Vec<BasicTxDetails>,
103}
104
105#[derive(Clone, Serialize)]
107struct CorpusEntry {
108 uuid: Uuid,
110 total_mutations: usize,
112 new_finds_produced: usize,
114 #[serde(skip_serializing)]
116 tx_seq: Vec<BasicTxDetails>,
117 #[serde(skip_serializing)]
120 cmp_seq: Vec<Vec<ComparisonHint>>,
121 is_favored: bool,
124 #[serde(skip_serializing)]
126 timestamp: u64,
127 #[serde(skip_serializing)]
129 persisted_file_name: Option<String>,
130}
131
132impl CorpusEntry {
133 pub fn new(tx_seq: Vec<BasicTxDetails>) -> Self {
135 Self::new_with_cmp(tx_seq, Vec::new(), Uuid::new_v4())
136 }
137
138 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
200pub 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 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 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#[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 last_new_edge_at: Option<Instant>,
368}
369
370impl WorkerCorpusSeed {
371 fn empty(config: &FuzzCorpusConfig) -> Self {
372 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 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 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 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 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 cumulative_edges_seen: AtomicUsize,
537 cumulative_features_seen: AtomicUsize,
539 corpus_count: AtomicUsize,
541 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 cumulative_edges_seen: usize,
606 cumulative_features_seen: usize,
608 corpus_count: usize,
610 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 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 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
646pub struct WorkerCorpus {
648 id: usize,
650 in_memory_corpus: Vec<CorpusEntry>,
653 history_map: Vec<u8>,
655 edge_indices: EdgeIndexMap,
657 sancov_history_map: Vec<u8>,
659 pub(crate) failed_replays: usize,
661 pub(crate) metrics: CorpusMetrics,
663 sequence_generator: SequenceGenerator,
665 current_mutated_index: Option<usize>,
667 config: Arc<FuzzCorpusConfig>,
669 worker_sync_enabled: bool,
671 new_entry_indices: Vec<usize>,
673 initial_export_dirs: Option<Vec<PathBuf>>,
675 worker_dir: Option<PathBuf>,
678 warned_persistence_failure: bool,
680 last_sync_metrics: CorpusMetrics,
682 optimization_best_value: Option<I256>,
684 optimization_best_sequence: Vec<BasicTxDetails>,
686 last_new_edge_at: Option<Instant>,
692}
693
694#[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
703pub(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
725pub(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 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 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 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 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 #[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 #[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 let improved_optimization = optimization.as_ref().is_some_and(|(value, _)| {
1008 self.optimization_best_value.is_none_or(|best| *value > best)
1009 });
1010
1011 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 if !new_coverage && !improved_optimization {
1047 return;
1048 }
1049
1050 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 pub fn optimization_initial_state(&self) -> (Option<I256>, Vec<BasicTxDetails>) {
1119 (self.optimization_best_value, self.optimization_best_sequence.clone())
1120 }
1121
1122 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 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 if is_edge {
1149 self.last_new_edge_at = Some(Instant::now());
1150 }
1151 }
1152 new_coverage
1153 }
1154
1155 pub(crate) fn time_since_new_edge(&self) -> Option<Duration> {
1158 self.last_new_edge_at.map(|at| at.elapsed())
1159 }
1160 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 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 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 self.in_memory_corpus.remove(index);
1311
1312 self.new_entry_indices.retain_mut(|i| {
1314 if *i > index {
1315 *i -= 1; true } else {
1318 *i != index }
1320 });
1321 }
1322 Ok(())
1323 }
1324 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 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 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 #[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 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 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 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 #[instrument(skip_all)]
1488 fn export_to_master(&mut self) -> Result<()> {
1489 assert_ne!(self.id, 0, "non-master only");
1491
1492 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 #[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 pub(crate) fn sync_metrics(&mut self, global_corpus_metrics: &GlobalCorpusMetrics) {
1639 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 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 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 self.last_sync_metrics = self.metrics.clone();
1687 }
1688
1689 #[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 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 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 if spec_id >= SpecId::PRAGUE && new_height > next_block_number {
1826 return None;
1827 }
1828 next_block_number = new_height;
1829 }
1830 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 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 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); 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 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 #[test]
2214 fn corrupt_corpus_file_surfaces_as_error_for_load_to_skip() {
2215 let dir = temp_corpus_dir();
2216
2217 let valid = CorpusEntry::new(vec![basic_tx()]);
2219 valid.write_to_disk_in(&dir, false).unwrap();
2220
2221 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 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 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 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 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 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 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 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 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 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; corpus.is_favored = false;
3308
3309 assert_eq!(manager.metrics.favored_items, 0);
3311
3312 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; corpus.is_favored = true; manager.metrics.favored_items = 1;
3333
3334 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 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 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 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 manager.evict_oldest_corpus().unwrap();
3386 assert_eq!(manager.in_memory_corpus.len(), 1, "favored corpus must not be evicted");
3387
3388 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 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}