1use crate::{
16 executors::{
17 Executor,
18 corpus::{
19 DynamicTargetCtx, StatelessReplayTarget, WorkerCorpus, register_replay_created,
20 rollback_replay_created,
21 },
22 corpus_io::read_corpus_tree,
23 invariant::{
24 call_after_invariant_function, call_invariant_function, did_fail_on_assert, execute_tx,
25 snapshot_edge_fingerprint,
26 },
27 },
28 inspectors::EdgeIndexMap,
29};
30use alloy_dyn_abi::JsonAbiExt;
31use alloy_json_abi::Function;
32use alloy_primitives::{Address, B256, Selector, hex, keccak256};
33use eyre::Result;
34use foundry_config::FuzzCorpusConfig;
35use foundry_evm_core::{
36 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
37 decode::SkipReason,
38 evm::FoundryEvmNetwork,
39};
40use foundry_evm_coverage::HitMaps;
41use foundry_evm_fuzz::{BasicTxDetails, invariant::FuzzRunIdentifiedContracts};
42use std::{
43 borrow::Cow,
44 cmp::Ordering,
45 collections::{BTreeSet, HashMap},
46 fmt,
47 fs::File,
48 io::{BufWriter, Write},
49 path::{Path, PathBuf},
50};
51
52type EvmShowmap = HashMap<(B256, u32), u64>;
53const MAX_REPORTED_REPLAY_FAILURES: usize = 20;
54
55#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
57pub enum ShowmapDomain {
58 #[default]
59 Evm,
60 Sancov,
61 Both,
62}
63
64impl ShowmapDomain {
65 pub const fn includes_evm(self) -> bool {
66 matches!(self, Self::Evm | Self::Both)
67 }
68 pub const fn includes_sancov(self) -> bool {
69 matches!(self, Self::Sancov | Self::Both)
70 }
71}
72
73impl fmt::Display for ShowmapDomain {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self {
76 Self::Evm => f.write_str("evm"),
77 Self::Sancov => f.write_str("sancov"),
78 Self::Both => f.write_str("both"),
79 }
80 }
81}
82
83#[derive(Clone, Debug)]
85pub struct ShowmapOpts {
86 pub out_dir: PathBuf,
88 pub approach: String,
91 pub trial: String,
93 pub per_input: bool,
95 pub domain: ShowmapDomain,
97 pub emit_files: bool,
99}
100
101#[derive(Clone, Debug, Default)]
103pub struct ShowmapStats {
104 pub corpus_entries: usize,
106 pub showmap_files: usize,
108 pub skipped_entries: usize,
111 pub unreadable_entries: usize,
113 pub sancov_requested: bool,
116 pub sancov_observed: bool,
118}
119
120pub struct ShowmapReplayTarget<'a> {
122 pub stateless: Option<StatelessReplayTarget<'a>>,
123 pub fuzz_fail_on_revert: bool,
124 pub fuzzed_contracts: Option<&'a FuzzRunIdentifiedContracts>,
125 pub invariant_address: Option<Address>,
126 pub invariant_fns: &'a [(&'a Function, bool)],
127 pub invariant_replay: InvariantReplayOptions,
128 pub dynamic: Option<&'a DynamicTargetCtx<'a>>,
129}
130
131#[derive(Clone, Copy, Debug, Default)]
133pub struct InvariantReplayOptions {
134 pub check_interval: u32,
135 pub call_after_invariant: bool,
136 pub is_optimization: bool,
137}
138
139#[derive(Clone, Debug)]
141pub enum ReplayFailure {
142 Fuzz { selector: Selector, fingerprint: Option<B256>, output: B256 },
144 Handler { target: Address, selector: Selector, fingerprint: Option<B256> },
148 HandlerRevert { name: String, target: Address, selector: Selector },
151 Invariant { name: String },
153 AfterInvariant,
155}
156
157impl PartialEq for ReplayFailure {
158 fn eq(&self, other: &Self) -> bool {
159 self.cmp(other) == Ordering::Equal
160 }
161}
162
163impl Eq for ReplayFailure {}
164
165impl PartialOrd for ReplayFailure {
166 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
167 Some(self.cmp(other))
168 }
169}
170
171impl Ord for ReplayFailure {
172 fn cmp(&self, other: &Self) -> Ordering {
173 match (self, other) {
174 (
175 Self::Fuzz { selector, fingerprint, output },
176 Self::Fuzz {
177 selector: other_selector,
178 fingerprint: other_fingerprint,
179 output: other_output,
180 },
181 ) => (selector, fingerprint, output).cmp(&(
182 other_selector,
183 other_fingerprint,
184 other_output,
185 )),
186 (
187 Self::Handler { target, selector, .. },
188 Self::Handler { target: other_target, selector: other_selector, .. },
189 ) => (target, selector).cmp(&(other_target, other_selector)),
190 (Self::HandlerRevert { name, .. }, Self::HandlerRevert { name: other_name, .. }) => {
191 name.cmp(other_name)
192 }
193 (Self::Invariant { name }, Self::Invariant { name: other_name }) => {
194 name.cmp(other_name)
195 }
196 (Self::AfterInvariant, Self::AfterInvariant) => Ordering::Equal,
197 _ => replay_failure_rank(self).cmp(&replay_failure_rank(other)),
198 }
199 }
200}
201
202const fn replay_failure_rank(failure: &ReplayFailure) -> u8 {
203 match failure {
204 ReplayFailure::Fuzz { .. } => 0,
205 ReplayFailure::Handler { .. } => 1,
206 ReplayFailure::HandlerRevert { .. } => 2,
207 ReplayFailure::Invariant { .. } => 3,
208 ReplayFailure::AfterInvariant => 4,
209 }
210}
211
212impl fmt::Display for ReplayFailure {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 match self {
215 Self::Fuzz { selector, .. } => write!(f, "fuzz call {selector:?} failed"),
216 Self::Handler { target, selector, .. } => {
217 write!(f, "handler {selector:?} on {target:?} failed")
218 }
219 Self::HandlerRevert { name, target, selector } => {
220 write!(f, "invariant `{name}` failed on handler {selector:?} on {target:?}")
221 }
222 Self::Invariant { name } => write!(f, "invariant `{name}` broken"),
223 Self::AfterInvariant => f.write_str("afterInvariant broken"),
224 }
225 }
226}
227
228impl ReplayFailure {
229 const fn is_predicate(&self) -> bool {
231 matches!(self, Self::Invariant { .. } | Self::AfterInvariant)
232 }
233}
234
235#[derive(Clone, Debug, Default, PartialEq, Eq)]
237pub struct ReplayObservation {
238 pub evm_edges: Vec<u8>,
240 pub sancov_edges: Vec<u8>,
242 pub failures: BTreeSet<ReplayFailure>,
244 pub replayed: usize,
246 pub unmatched: usize,
248 pub skipped: usize,
250}
251
252impl ReplayObservation {
253 pub fn has_non_predicate_failure(&self) -> bool {
255 self.failures.iter().any(|failure| !failure.is_predicate())
256 }
257
258 fn has_invariant_failure(&self) -> bool {
259 self.failures.iter().any(|failure| {
260 matches!(failure, ReplayFailure::HandlerRevert { .. } | ReplayFailure::Invariant { .. })
261 })
262 }
263}
264
265pub fn replay_corpus_to_showmap<FEN: FoundryEvmNetwork>(
272 executor: &Executor<FEN>,
273 corpus_dir: &Path,
274 target: ShowmapReplayTarget<'_>,
275 opts: &ShowmapOpts,
276) -> Result<ShowmapStats> {
277 let entries = read_corpus_tree(corpus_dir)?;
278 if opts.emit_files && entries.is_empty() {
279 return Err(eyre::eyre!("corpus directory not found: {}", corpus_dir.display()));
280 }
281
282 let approach_dir = opts.out_dir.join(&opts.approach);
283 if opts.emit_files {
284 foundry_common::fs::create_dir_all(&approach_dir)?;
285 }
286
287 let mut stats =
288 ShowmapStats { sancov_requested: opts.domain.includes_sancov(), ..Default::default() };
289 let mut replay_failures = Vec::new();
290 let mut failed_entries = 0usize;
291 let mut evm_buf = EvmShowmap::new();
294 let mut san_buf: Vec<u64> = Vec::new();
295
296 for entry in entries {
297 let tx_seq = match entry.read_tx_seq() {
298 Ok(seq) if !seq.is_empty() => seq,
299 Ok(_) => continue,
300 Err(err) => {
301 debug!(target: "showmap", %err, ?entry.path, "failed to read corpus entry");
302 stats.unreadable_entries += 1;
303 stats.skipped_entries += 1;
304 continue;
305 }
306 };
307
308 let mut had_accepted = false;
309 let mut executor = executor.clone();
310 let mut created: Vec<Address> = Vec::new();
312 let mut accepted = 0usize;
314 let mut last_accepted_checked_invariant = false;
315 let mut entry_failure: Option<ReplayFailure> = None;
316 for tx in &tx_seq {
317 if !WorkerCorpus::can_replay_tx(tx, target.stateless, target.fuzzed_contracts) {
318 continue;
319 }
320
321 let mut call_result = execute_tx(&mut executor, tx)?;
322 let fingerprint = snapshot_edge_fingerprint(&call_result);
324 if call_result.result.as_ref() == MAGIC_ASSUME
327 || (call_result.reverter == Some(CHEATCODE_ADDRESS)
328 && SkipReason::decode(&call_result.result).is_some())
329 {
330 continue;
331 }
332 if opts.domain.includes_evm() {
339 accumulate_evm(&mut evm_buf, call_result.line_coverage.as_ref());
340 }
341 if opts.domain.includes_sancov() {
342 accumulate_sancov(&mut san_buf, call_result.sancov_coverage.as_deref());
343 if let Some(buf) = call_result.sancov_coverage.as_mut() {
344 buf.fill(0);
345 }
346 }
347
348 had_accepted = true;
349
350 register_replay_created(
351 &call_result.state_changeset,
352 target.dynamic,
353 target.fuzzed_contracts,
354 &mut created,
355 );
356
357 let target_addr = tx.call_details.target;
358 let selector =
359 tx.call_details.calldata.get(..4).map(Selector::from_slice).unwrap_or_default();
360
361 if target.fuzzed_contracts.is_some() {
363 accepted += 1;
364 last_accepted_checked_invariant = false;
365 if !opts.emit_files
366 && let Some(failure) = invariant_replay_failures(
367 target_addr,
368 selector,
369 did_fail_on_assert(&call_result, &call_result.state_changeset),
370 target.invariant_fns,
371 &call_result,
372 fingerprint,
373 )
374 .into_iter()
375 .next()
376 {
377 entry_failure = Some(failure);
378 break;
379 }
380 executor.commit(&mut call_result);
381 if !opts.emit_files
382 && should_check_invariant(
383 accepted,
384 target.invariant_replay.check_interval,
385 target.invariant_replay.is_optimization,
386 )
387 {
388 last_accepted_checked_invariant = true;
389 if !target.invariant_replay.is_optimization
390 && let Some(address) = target.invariant_address
391 && let Some(failure) =
392 first_broken_invariant(&executor, address, target.invariant_fns)?
393 {
394 entry_failure = Some(failure);
395 break;
396 }
397 }
398 } else if !opts.emit_files
399 && !fuzz_replay_call_succeeded(
400 &executor,
401 target_addr,
402 &mut call_result,
403 target.fuzz_fail_on_revert,
404 )
405 {
406 entry_failure = Some(ReplayFailure::Fuzz {
407 selector,
408 fingerprint,
409 output: keccak256(call_result.result.as_ref()),
410 });
411 break;
412 }
413 }
414 if !opts.emit_files
418 && entry_failure.is_none()
419 && accepted > 0
420 && target.fuzzed_contracts.is_some()
421 && let Some(address) = target.invariant_address
422 {
423 if !target.invariant_replay.is_optimization
424 && !last_accepted_checked_invariant
425 && let Some(failure) =
426 first_broken_invariant(&executor, address, target.invariant_fns)?
427 {
428 entry_failure = Some(failure);
429 } else if target.invariant_replay.call_after_invariant
430 && let Some(failure) = broken_after_invariant(&executor, address)?
431 {
432 entry_failure = Some(failure);
433 }
434 }
435 if let Some(failure) = entry_failure {
436 rollback_replay_created(target.fuzzed_contracts, created);
437 failed_entries += 1;
438 if replay_failures.len() < MAX_REPORTED_REPLAY_FAILURES {
439 replay_failures.push(format!(
440 "corpus entry {} failed during replay: {failure}",
441 entry.path.display()
442 ));
443 }
444 continue;
445 }
446 rollback_replay_created(target.fuzzed_contracts, created);
447
448 if !had_accepted {
449 stats.skipped_entries += 1;
450 continue;
451 }
452 stats.corpus_entries += 1;
453 if !stats.sancov_observed && san_buf.iter().any(|&x| x != 0) {
454 stats.sancov_observed = true;
455 }
456
457 if opts.emit_files && opts.per_input {
458 let stem = format!("{}__{}-{}", opts.trial, entry.uuid, entry.timestamp);
460 stats.showmap_files +=
461 write_showmap_file(&approach_dir.join(format!("{stem}.txt")), &evm_buf, &san_buf)?;
462 evm_buf.clear();
464 san_buf.fill(0);
465 }
466 }
467
468 if opts.emit_files && !opts.per_input {
469 stats.showmap_files += write_showmap_file(
471 &approach_dir.join(format!("{}.txt", opts.trial)),
472 &evm_buf,
473 &san_buf,
474 )?;
475 }
476
477 if failed_entries > 0 {
478 return Err(eyre::eyre!(
479 "corpus replay failed:\n{}",
480 replay_failure_report(&replay_failures, failed_entries)
481 ));
482 }
483
484 Ok(stats)
485}
486
487pub struct MinimizationReplayInput<'a> {
488 pub sequence: &'a [BasicTxDetails],
489 pub evm_edge_indices: &'a mut EdgeIndexMap,
490 pub corpus: &'a FuzzCorpusConfig,
491 pub stop_at_campaign_end: bool,
493}
494
495pub fn replay_sequence_for_minimization<FEN: FoundryEvmNetwork>(
497 executor: &Executor<FEN>,
498 input: MinimizationReplayInput<'_>,
499 target: ShowmapReplayTarget<'_>,
500) -> Result<ReplayObservation> {
501 let mut observation = ReplayObservation::default();
502 let mut executor = executor.clone();
503 executor.inspector_mut().collect_edge_coverage_with_config(input.corpus);
504 executor.inspector_mut().collect_sancov_edges(input.corpus.collect_sancov_edges());
505 executor.inspector_mut().collect_sancov_trace_cmp(input.corpus.collect_sancov_trace_cmp());
506
507 let mut created = Vec::new();
508 let mut accepted = 0usize;
509 let mut last_accepted_checked_invariant = false;
510 let mut last_accepted_handlers_succeeded = false;
511 for tx in input.sequence {
512 if !WorkerCorpus::can_replay_tx(tx, target.stateless, target.fuzzed_contracts) {
513 observation.unmatched += 1;
514 continue;
515 }
516
517 let mut call_result = execute_tx(&mut executor, tx)?;
518 let target_addr = tx.call_details.target;
519 let selector =
520 tx.call_details.calldata.get(..4).map(Selector::from_slice).unwrap_or_default();
521 let fingerprint = snapshot_edge_fingerprint(&call_result);
522
523 if call_result.result.as_ref() == MAGIC_ASSUME
524 || (call_result.reverter == Some(CHEATCODE_ADDRESS)
525 && SkipReason::decode(&call_result.result).is_some())
526 {
527 observation.skipped += 1;
528 continue;
529 }
530
531 call_result.merge_all_coverage(
532 &mut observation.evm_edges,
533 input.evm_edge_indices,
534 &mut observation.sancov_edges,
535 );
536 observation.replayed += 1;
537
538 register_replay_created(
539 &call_result.state_changeset,
540 target.dynamic,
541 target.fuzzed_contracts,
542 &mut created,
543 );
544
545 if target.fuzzed_contracts.is_some() {
546 accepted += 1;
547 last_accepted_checked_invariant = false;
548 last_accepted_handlers_succeeded =
549 invariant_handlers_succeeded(&executor, &target, &call_result);
550 for failure in invariant_replay_failures(
551 target_addr,
552 selector,
553 did_fail_on_assert(&call_result, &call_result.state_changeset),
554 target.invariant_fns,
555 &call_result,
556 fingerprint,
557 ) {
558 observation.failures.insert(failure);
559 }
560 executor.commit(&mut call_result);
561 if input.stop_at_campaign_end
562 && all_invariants_failed(&observation.failures, target.invariant_fns)
563 {
564 break;
565 }
566 if should_check_invariant(
567 accepted,
568 target.invariant_replay.check_interval,
569 target.invariant_replay.is_optimization,
570 ) && last_accepted_handlers_succeeded
571 {
572 last_accepted_checked_invariant = true;
573 if !target.invariant_replay.is_optimization
574 && let Some(address) = target.invariant_address
575 {
576 for failure in newly_broken_invariants(
577 &executor,
578 address,
579 target.invariant_fns,
580 &observation.failures,
581 )? {
582 observation.failures.insert(failure);
583 }
584 if input.stop_at_campaign_end
585 && all_invariants_failed(&observation.failures, target.invariant_fns)
586 {
587 break;
588 }
589 }
590 }
591 } else if !fuzz_replay_call_succeeded(
592 &executor,
593 target_addr,
594 &mut call_result,
595 target.fuzz_fail_on_revert,
596 ) {
597 observation.failures.insert(ReplayFailure::Fuzz {
598 selector,
599 fingerprint,
600 output: keccak256(call_result.result.as_ref()),
601 });
602 break;
603 }
604 }
605
606 if !all_invariants_failed(&observation.failures, target.invariant_fns)
607 && accepted > 0
608 && target.fuzzed_contracts.is_some()
609 && let Some(address) = target.invariant_address
610 {
611 if !target.invariant_replay.is_optimization
612 && last_accepted_handlers_succeeded
613 && !last_accepted_checked_invariant
614 {
615 for failure in newly_broken_invariants(
616 &executor,
617 address,
618 target.invariant_fns,
619 &observation.failures,
620 )? {
621 observation.failures.insert(failure);
622 }
623 }
624 if target.invariant_replay.call_after_invariant
625 && !observation.has_invariant_failure()
626 && let Some(failure) = broken_after_invariant(&executor, address)?
627 {
628 observation.failures.insert(failure);
629 }
630 }
631
632 rollback_replay_created(target.fuzzed_contracts, created);
633 Ok(observation)
634}
635
636fn invariant_handlers_succeeded<FEN: FoundryEvmNetwork>(
638 executor: &Executor<FEN>,
639 target: &ShowmapReplayTarget<'_>,
640 call_result: &crate::executors::RawCallResult<FEN>,
641) -> bool {
642 if call_result.reverted {
643 return false;
644 }
645
646 if !executor.legacy_assertions() {
647 return target.invariant_address.is_some_and(|address| {
648 executor.is_success_handler_gate(
649 address,
650 false,
651 Cow::Borrowed(&call_result.state_changeset),
652 )
653 });
654 }
655
656 target.fuzzed_contracts.is_some_and(|contracts| {
657 contracts.targets().keys().all(|address| {
658 executor.is_success_handler_gate(
659 *address,
660 false,
661 Cow::Borrowed(&call_result.state_changeset),
662 )
663 })
664 })
665}
666
667fn replay_failure_report(replay_failures: &[String], failed_entries: usize) -> String {
668 let mut report = replay_failures.join("\n");
669 let unreported = failed_entries.saturating_sub(replay_failures.len());
670 if unreported > 0 {
671 if !report.is_empty() {
672 report.push('\n');
673 }
674 report.push_str(&format!("... and {unreported} more failed corpus entries"));
675 }
676 report
677}
678
679fn invariant_replay_failures<FEN: FoundryEvmNetwork>(
683 target: Address,
684 selector: Selector,
685 assertion_failure: bool,
686 invariant_fns: &[(&Function, bool)],
687 call_result: &crate::executors::RawCallResult<FEN>,
688 fingerprint: Option<B256>,
689) -> Vec<ReplayFailure> {
690 if assertion_failure {
691 return vec![ReplayFailure::Handler { target, selector, fingerprint }];
692 }
693 if !call_result.reverted || call_result.result.as_ref() == MAGIC_ASSUME {
694 return Vec::new();
695 }
696 invariant_fns
697 .iter()
698 .filter(|(_, fail_on_revert)| *fail_on_revert)
699 .map(|(invariant, _)| ReplayFailure::HandlerRevert {
700 name: invariant.name.clone(),
701 target,
702 selector,
703 })
704 .collect()
705}
706
707fn fuzz_replay_call_succeeded<FEN: FoundryEvmNetwork>(
708 executor: &Executor<FEN>,
709 target_addr: Address,
710 call_result: &mut crate::executors::RawCallResult<FEN>,
711 fail_on_revert: bool,
712) -> bool {
713 if !fail_on_revert
714 && call_result
715 .reverter
716 .is_some_and(|reverter| reverter != target_addr && reverter != CHEATCODE_ADDRESS)
717 {
718 true
719 } else {
720 executor.is_raw_call_mut_success(target_addr, call_result, false)
721 }
722}
723
724fn newly_broken_invariants<FEN: FoundryEvmNetwork>(
725 executor: &Executor<FEN>,
726 invariant_address: Address,
727 invariant_fns: &[(&Function, bool)],
728 observed: &BTreeSet<ReplayFailure>,
729) -> Result<Vec<ReplayFailure>> {
730 let mut failures = Vec::new();
731 for (invariant, _) in invariant_fns {
732 if has_replay_invariant_failure(observed, &invariant.name) {
733 continue;
734 }
735 let (_, success) = call_invariant_function(
736 executor,
737 invariant_address,
738 invariant.abi_encode_input(&[])?.into(),
739 )?;
740 if !success {
741 failures.push(ReplayFailure::Invariant { name: invariant.name.clone() });
742 }
743 }
744 Ok(failures)
745}
746
747fn first_broken_invariant<FEN: FoundryEvmNetwork>(
748 executor: &Executor<FEN>,
749 invariant_address: Address,
750 invariant_fns: &[(&Function, bool)],
751) -> Result<Option<ReplayFailure>> {
752 for (invariant, _) in invariant_fns {
753 let (_, success) = call_invariant_function(
754 executor,
755 invariant_address,
756 invariant.abi_encode_input(&[])?.into(),
757 )?;
758 if !success {
759 return Ok(Some(ReplayFailure::Invariant { name: invariant.name.clone() }));
760 }
761 }
762 Ok(None)
763}
764
765fn has_replay_invariant_failure(failures: &BTreeSet<ReplayFailure>, name: &str) -> bool {
766 failures.iter().any(|failure| {
767 matches!(
768 failure,
769 ReplayFailure::HandlerRevert { name: observed, .. }
770 | ReplayFailure::Invariant { name: observed }
771 if observed == name
772 )
773 })
774}
775
776fn all_invariants_failed(
777 failures: &BTreeSet<ReplayFailure>,
778 invariant_fns: &[(&Function, bool)],
779) -> bool {
780 invariant_fns
781 .iter()
782 .all(|(invariant, _)| has_replay_invariant_failure(failures, &invariant.name))
783}
784
785fn broken_after_invariant<FEN: FoundryEvmNetwork>(
786 executor: &Executor<FEN>,
787 invariant_address: Address,
788) -> Result<Option<ReplayFailure>> {
789 let (_, success) = call_after_invariant_function(executor, invariant_address)?;
790 Ok((!success).then_some(ReplayFailure::AfterInvariant))
791}
792
793fn should_check_invariant(accepted: usize, check_interval: u32, is_optimization: bool) -> bool {
800 debug_assert!(accepted > 0);
801 is_optimization
802 || check_interval == 1
803 || (check_interval > 1 && accepted.is_multiple_of(check_interval as usize))
804}
805
806fn accumulate_evm(dst: &mut EvmShowmap, src: Option<&HitMaps>) {
808 let Some(maps) = src else { return };
809 for (hash, hitmap) in maps.iter() {
810 for (pc, hits) in hitmap.iter() {
811 let slot = dst.entry((*hash, pc)).or_default();
812 *slot = slot.saturating_add(hits as u64);
813 }
814 }
815}
816
817fn accumulate_sancov(dst: &mut Vec<u64>, src: Option<&[u8]>) {
819 let Some(src) = src else { return };
820 if dst.len() < src.len() {
821 dst.resize(src.len(), 0);
822 }
823 for (d, &s) in dst.iter_mut().zip(src) {
824 if s != 0 {
825 *d = d.saturating_add(s as u64);
826 }
827 }
828}
829
830fn write_showmap_file(path: &Path, evm: &EvmShowmap, san: &[u64]) -> Result<usize> {
833 let has_evm = evm.values().any(|&c| c != 0);
835 let has_san = san.iter().any(|&c| c != 0);
836 if !has_evm && !has_san {
837 return Ok(0);
838 }
839 let mut w = BufWriter::new(File::create_new(path).map_err(|err| {
840 eyre::eyre!(
841 "failed to create showmap file {}: {err}; pick a different --showmap-trial or remove \
842 the existing file",
843 path.display()
844 )
845 })?);
846 write_evm(&mut w, evm)?;
847 write_sancov(&mut w, san)?;
848 w.flush()?;
849 Ok(1)
850}
851
852fn write_evm<W: Write>(out: &mut W, evm: &EvmShowmap) -> std::io::Result<()> {
856 let mut entries = evm.iter().filter(|(_, count)| **count != 0).collect::<Vec<_>>();
857 entries.sort_unstable_by_key(|((hash, pc), _)| (*hash, *pc));
858
859 for ((hash, pc), count) in entries {
860 let h = hex::encode(&hash.as_slice()[..8]);
861 writeln!(out, "evm_{h}_{pc:04x}:{count}")?;
862 }
863 Ok(())
864}
865
866fn write_sancov<W: Write>(out: &mut W, bitmap: &[u64]) -> std::io::Result<()> {
867 for (idx, &count) in bitmap.iter().enumerate() {
868 if count != 0 {
869 writeln!(out, "sancov_0x{idx:04x}:{count}")?;
872 }
873 }
874 Ok(())
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880 use crate::executors::{RawCallResult, corpus_io::canonical_replay_dirs};
881 use foundry_evm_core::evm::EthEvmNetwork;
882 use revm::interpreter::InstructionResult;
883 use uuid::Uuid;
884
885 fn temp_dir() -> PathBuf {
886 let dir = std::env::temp_dir().join(format!("foundry-showmap-{}", Uuid::new_v4()));
887 std::fs::create_dir_all(&dir).unwrap();
888 dir
889 }
890
891 #[test]
892 fn accumulate_sancov_resizes_and_saturating_adds() {
893 let mut dst: Vec<u64> = vec![10];
894 accumulate_sancov(&mut dst, Some(&[1u8, 2, 3]));
895 assert_eq!(dst, vec![11, 2, 3]);
896 }
897
898 #[test]
899 fn write_evm_emits_only_nonzero_deterministic_ids() {
900 let mut buf: Vec<u8> = Vec::new();
901 let h = B256::with_last_byte(0xab);
902 let mut evm = EvmShowmap::new();
903 evm.insert((h, 1u32), 0u64); evm.insert((h, 0x2au32), 3u64);
905 write_evm(&mut buf, &evm).unwrap();
906 let h_hex = hex::encode(&h.as_slice()[..8]);
907 assert_eq!(String::from_utf8(buf).unwrap(), format!("evm_{h_hex}_002a:3\n"));
908 }
909
910 #[test]
911 fn write_sancov_emits_only_nonzero_hex_ids() {
912 let mut buf: Vec<u8> = Vec::new();
913 write_sancov(&mut buf, &[0, 3, 0, 1]).unwrap();
914 assert_eq!(String::from_utf8(buf).unwrap(), "sancov_0x0001:3\nsancov_0x0003:1\n");
915 }
916
917 #[test]
918 fn write_showmap_file_skips_when_empty() {
919 let dir = temp_dir();
920 let path = dir.join("trial.txt");
921 let written = write_showmap_file(&path, &EvmShowmap::new(), &[]).unwrap();
922 assert_eq!(written, 0);
923 assert!(!path.exists());
924 }
925
926 #[test]
927 fn write_showmap_file_writes_combined_domains() {
928 let dir = temp_dir();
929 let path = dir.join("trial.txt");
930 let h = B256::with_last_byte(0xff);
931 let mut evm = EvmShowmap::new();
932 evm.insert((h, 7u32), 5u64);
933 let written = write_showmap_file(&path, &evm, &[2]).unwrap();
934 assert_eq!(written, 1);
935 let body = std::fs::read_to_string(&path).unwrap();
936 let h_hex = hex::encode(&h.as_slice()[..8]);
937 assert_eq!(body, format!("evm_{h_hex}_0007:5\nsancov_0x0000:2\n"));
938 }
939
940 #[test]
941 fn write_showmap_file_does_not_overwrite_existing_file() {
942 let dir = temp_dir();
943 let path = dir.join("trial.txt");
944 std::fs::write(&path, "keep me").unwrap();
945 let h = B256::with_last_byte(0xff);
946 let mut evm = EvmShowmap::new();
947 evm.insert((h, 7u32), 5u64);
948 let err = write_showmap_file(&path, &evm, &[]).unwrap_err();
949 assert!(err.to_string().contains("pick a different --showmap-trial"), "{err:?}");
950 assert_eq!(std::fs::read_to_string(&path).unwrap(), "keep me");
951 }
952
953 #[test]
954 fn replay_failure_report_caps_details() {
955 let failures = (0..MAX_REPORTED_REPLAY_FAILURES)
956 .map(|idx| format!("corpus entry {idx} failed during replay: fuzz call failed"))
957 .collect::<Vec<_>>();
958 let report = replay_failure_report(&failures, MAX_REPORTED_REPLAY_FAILURES + 3);
959
960 assert!(report.contains("corpus entry 0 failed during replay"), "{report}");
961 assert!(report.contains("corpus entry 19 failed during replay"), "{report}");
962 assert!(report.contains("... and 3 more failed corpus entries"), "{report}");
963 assert!(!report.contains("corpus entry 20 failed during replay"), "{report}");
964 }
965
966 #[test]
967 fn replay_observation_derives_failure_classification() {
968 let handler = ReplayFailure::Handler {
969 target: Address::with_last_byte(1),
970 selector: Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
971 fingerprint: None,
972 };
973 let mut observation = ReplayObservation::default();
974
975 assert!(!observation.has_non_predicate_failure());
976 assert!(!observation.has_invariant_failure());
977
978 observation
979 .failures
980 .insert(ReplayFailure::Invariant { name: "invariant_first".to_string() });
981 assert!(!observation.has_non_predicate_failure());
982 assert!(observation.has_invariant_failure());
983
984 observation.failures.insert(handler);
985 assert!(observation.has_non_predicate_failure());
986
987 observation.failures.insert(ReplayFailure::Fuzz {
988 selector: Selector::ZERO,
989 fingerprint: None,
990 output: B256::ZERO,
991 });
992 assert!(observation.has_non_predicate_failure());
993 }
994
995 #[test]
996 fn handler_failure_identity_ignores_path_fingerprint() {
997 let target = Address::with_last_byte(1);
998 let selector = Selector::from([0xaa, 0xbb, 0xcc, 0xdd]);
999 let failures = BTreeSet::from([
1000 ReplayFailure::Handler { target, selector, fingerprint: Some(B256::with_last_byte(1)) },
1001 ReplayFailure::Handler { target, selector, fingerprint: Some(B256::with_last_byte(2)) },
1002 ]);
1003
1004 let observation = ReplayObservation { failures, ..ReplayObservation::default() };
1005 assert_eq!(observation.failures.len(), 1);
1006 assert!(observation.has_non_predicate_failure());
1007 }
1008
1009 #[test]
1010 fn handler_revert_identity_ignores_handler_site() {
1011 let failures = BTreeSet::from([
1012 ReplayFailure::HandlerRevert {
1013 name: "invariant_ok".to_string(),
1014 target: Address::with_last_byte(1),
1015 selector: Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
1016 },
1017 ReplayFailure::HandlerRevert {
1018 name: "invariant_ok".to_string(),
1019 target: Address::with_last_byte(2),
1020 selector: Selector::from([0x11, 0x22, 0x33, 0x44]),
1021 },
1022 ]);
1023
1024 let observation = ReplayObservation { failures, ..ReplayObservation::default() };
1025 assert_eq!(observation.failures.len(), 1);
1026 assert!(observation.has_non_predicate_failure());
1027 assert!(observation.has_invariant_failure());
1028 }
1029
1030 #[test]
1031 fn has_replay_invariant_failure_matches_only_observed_predicate() {
1032 let failures = BTreeSet::from([
1033 ReplayFailure::HandlerRevert {
1034 name: "invariant_revert".to_string(),
1035 target: Address::with_last_byte(1),
1036 selector: Selector::ZERO,
1037 },
1038 ReplayFailure::Invariant { name: "invariant_first".to_string() },
1039 ReplayFailure::AfterInvariant,
1040 ]);
1041
1042 assert!(has_replay_invariant_failure(&failures, "invariant_revert"));
1043 assert!(has_replay_invariant_failure(&failures, "invariant_first"));
1044 assert!(!has_replay_invariant_failure(&failures, "invariant_second"));
1045 }
1046
1047 #[test]
1048 fn should_check_invariant_matches_campaign_intervals() {
1049 assert!(should_check_invariant(1, 1, false));
1051 assert!(should_check_invariant(2, 1, false));
1052 assert!(!should_check_invariant(1, 0, false));
1054 assert!(!should_check_invariant(5, 0, false));
1055 assert!(!should_check_invariant(1, 3, false));
1057 assert!(!should_check_invariant(2, 3, false));
1058 assert!(should_check_invariant(3, 3, false));
1059 assert!(should_check_invariant(6, 3, false));
1060 assert!(should_check_invariant(1, 0, true));
1062 }
1063
1064 #[test]
1065 fn invariant_replay_failures_ignore_plain_revert() {
1066 let call_result = RawCallResult::<EthEvmNetwork> {
1067 reverted: true,
1068 exit_reason: Some(InstructionResult::Revert),
1069 ..Default::default()
1070 };
1071 let failures = invariant_replay_failures(
1072 Address::with_last_byte(1),
1073 Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
1074 did_fail_on_assert(&call_result, &call_result.state_changeset),
1075 &[],
1076 &call_result,
1077 None,
1078 );
1079 assert!(failures.is_empty());
1080 }
1081
1082 #[test]
1083 fn invariant_replay_failures_report_every_fail_on_revert_invariant() {
1084 let call_result = RawCallResult::<EthEvmNetwork> {
1085 reverted: true,
1086 exit_reason: Some(InstructionResult::Revert),
1087 ..Default::default()
1088 };
1089 let first = serde_json::from_value::<Function>(serde_json::json!({
1090 "type": "function",
1091 "name": "invariant_first",
1092 "inputs": [],
1093 "outputs": [],
1094 "stateMutability": "view"
1095 }))
1096 .unwrap();
1097 let second = serde_json::from_value::<Function>(serde_json::json!({
1098 "type": "function",
1099 "name": "invariant_second",
1100 "inputs": [],
1101 "outputs": [],
1102 "stateMutability": "view"
1103 }))
1104 .unwrap();
1105 let ignored = serde_json::from_value::<Function>(serde_json::json!({
1106 "type": "function",
1107 "name": "invariant_ignored",
1108 "inputs": [],
1109 "outputs": [],
1110 "stateMutability": "view"
1111 }))
1112 .unwrap();
1113 let failures = invariant_replay_failures(
1114 Address::with_last_byte(1),
1115 Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
1116 did_fail_on_assert(&call_result, &call_result.state_changeset),
1117 &[(&first, true), (&second, true), (&ignored, false)],
1118 &call_result,
1119 None,
1120 );
1121 assert_eq!(
1122 failures,
1123 [
1124 ReplayFailure::HandlerRevert {
1125 name: "invariant_first".to_string(),
1126 target: Address::with_last_byte(1),
1127 selector: Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
1128 },
1129 ReplayFailure::HandlerRevert {
1130 name: "invariant_second".to_string(),
1131 target: Address::with_last_byte(1),
1132 selector: Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
1133 },
1134 ]
1135 );
1136 }
1137
1138 #[test]
1139 fn invariant_replay_failures_prefer_assertion_over_fail_on_revert() {
1140 let call_result = RawCallResult::<EthEvmNetwork> {
1141 reverted: true,
1142 exit_reason: Some(InstructionResult::Revert),
1143 ..Default::default()
1144 };
1145 let invariant = serde_json::from_value::<Function>(serde_json::json!({
1146 "type": "function",
1147 "name": "invariant_ok",
1148 "inputs": [],
1149 "outputs": [],
1150 "stateMutability": "view"
1151 }))
1152 .unwrap();
1153 let failures = invariant_replay_failures(
1154 Address::with_last_byte(1),
1155 Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
1156 true,
1157 &[(&invariant, true)],
1158 &call_result,
1159 None,
1160 );
1161 assert!(matches!(failures.as_slice(), [ReplayFailure::Handler { .. }]));
1162 }
1163
1164 #[test]
1165 fn canonical_replay_dirs_collects_all_workers() {
1166 let dir = temp_dir();
1167 let w0 = dir.join("worker0").join("corpus");
1168 let w1 = dir.join("worker1").join("corpus");
1169 std::fs::create_dir_all(&w0).unwrap();
1170 std::fs::create_dir_all(&w1).unwrap();
1171 assert_eq!(canonical_replay_dirs(&dir), vec![w0, w1]);
1172 }
1173
1174 #[test]
1175 fn canonical_replay_dirs_falls_back_when_no_workers() {
1176 let dir = temp_dir();
1177 assert_eq!(canonical_replay_dirs(&dir), vec![dir]);
1178 }
1179}