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