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