Skip to main content

foundry_evm/executors/
showmap.rs

1//! AFL-`afl-showmap`-style corpus replay.
2//!
3//! Replays a persisted corpus through a fresh executor and emits one text file
4//! per trial (or per corpus entry). Each line has the form `<id>:<count>`:
5//!
6//! - EVM IDs use the *deterministic* `(bytecode_hash, pc)` derived from the line-coverage `HitMap`
7//!   so that IDs are stable across `forge` invocations and meaningful for cross-approach analysis.
8//!   Format: `evm_<bytecode_hash[:16]>_<pc:04x>`.
9//! - Sancov IDs use the deterministic guard index from the sancov bitmap: `sancov_0x<index:04x>`.
10//!
11//! Counts are raw saturating-summed hitcounts across the replayed corpus.
12//!
13//! Output is consumable by tools like `riesentoaster/differential-coverage`.
14
15use 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/// Which coverage bitmap(s) to dump.
53#[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/// Per-replay options.
81#[derive(Clone, Debug)]
82pub struct ShowmapOpts {
83    /// Output root directory; emitted files live under `<out_dir>/<approach>/`.
84    pub out_dir: PathBuf,
85    /// Approach directory name; test identity is folded in here so each
86    /// `<approach>/` contains trials of one test (matches `differential-coverage`).
87    pub approach: String,
88    /// Rerun identifier used as the filename so multiple trials accumulate side-by-side.
89    pub trial: String,
90    /// Whether to emit one file per corpus entry or one aggregated file.
91    pub per_input: bool,
92    /// Which bitmap(s) to dump.
93    pub domain: ShowmapDomain,
94    /// Whether to write showmap files. Disabled by `forge fuzz replay`.
95    pub emit_files: bool,
96}
97
98/// Stats returned from a single trial replay.
99#[derive(Clone, Debug, Default)]
100pub struct ShowmapStats {
101    /// Number of corpus entries successfully replayed.
102    pub corpus_entries: usize,
103    /// Number of files written to disk.
104    pub showmap_files: usize,
105    /// Number of corpus entries skipped because they couldn't be replayed
106    /// against the current target (e.g. selector mismatch).
107    pub skipped_entries: usize,
108    /// Number of corpus entries skipped because they could not be read.
109    pub unreadable_entries: usize,
110    /// True if sancov coverage was requested. Lets the caller distinguish
111    /// "sancov not asked for" from "sancov asked for but produced nothing".
112    pub sancov_requested: bool,
113    /// True if any non-zero sancov hits were observed across the replay.
114    pub sancov_observed: bool,
115}
116
117/// Test target metadata needed to replay corpus entries.
118pub 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/// Invariant replay settings that affect when terminal checks run.
129#[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/// A structured identity for a failure observed during corpus replay.
137#[derive(Clone, Debug)]
138pub enum ReplayFailure {
139    /// A stateless fuzz test call failed. Keyed by selector, code-path fingerprint, and output.
140    Fuzz { selector: Selector, fingerprint: Option<B256>, output: B256 },
141    /// An invariant handler call hit an assertion.
142    /// Keyed by `(target, selector)`. The fingerprint is retained as replay metadata but does not
143    /// distinguish failure identities.
144    Handler { target: Address, selector: Selector, fingerprint: Option<B256> },
145    /// A handler reverted for an invariant configured with `fail_on_revert`.
146    /// Keyed by invariant name; the handler site is retained for diagnostics.
147    HandlerRevert { name: String, target: Address, selector: Selector },
148    /// A broken invariant predicate. Keyed by the invariant function name.
149    Invariant { name: String },
150    /// The `afterInvariant` hook reverted.
151    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    /// Whether this failure comes from an invariant predicate or `afterInvariant` hook.
227    const fn is_predicate(&self) -> bool {
228        matches!(self, Self::Invariant { .. } | Self::AfterInvariant)
229    }
230}
231
232/// Facts observed while replaying one candidate for corpus minimization.
233#[derive(Clone, Debug, Default, PartialEq, Eq)]
234pub struct ReplayObservation {
235    /// AFL-bucketed EVM edge coverage for the candidate.
236    pub evm_edges: Vec<u8>,
237    /// AFL-bucketed native sancov edge coverage for the candidate.
238    pub sancov_edges: Vec<u8>,
239    /// All unique failure identities observed while replaying the candidate.
240    pub failures: BTreeSet<ReplayFailure>,
241    /// Number of replayable transactions executed.
242    pub replayed: usize,
243    /// Number of transactions that do not target this fuzz/invariant context.
244    pub unmatched: usize,
245    /// Number of transactions rejected via `vm.assume`/`vm.skip`.
246    pub skipped: usize,
247}
248
249impl ReplayObservation {
250    /// Whether replay observed a stateless fuzz or invariant handler failure.
251    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
262/// Replay every corpus entry under `corpus_dir` and emit showmap files.
263///
264/// `stateless` is set for stateless fuzz tests; `fuzzed_contracts` is set for
265/// invariant tests (txs are committed between calls in that case).
266/// `dynamic` lets invariant replay register contracts deployed mid-sequence so
267/// follow-up calls into them aren't dropped.
268pub 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    // Reused per call. In aggregate mode it accumulates across all entries; in per-input mode it
289    // is cleared after each entry's file is written.
290    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        // Targets deployed during this entry, cleared after the entry.
308        let mut created: Vec<Address> = Vec::new();
309        // Number of committed (non-`vm.assume`) calls, used to gate invariant checks.
310        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            // Snapshot the edge fingerprint before any coverage merge zeroes the buffer.
320            let fingerprint = snapshot_edge_fingerprint(&call_result);
321            // `vm.assume` rejects and cheatcode `vm.skip` are discarded by the campaign: the call
322            // is not committed, checked, or counted toward coverage.
323            if call_result.result.as_ref() == MAGIC_ASSUME || call_result.skip_reason().is_some() {
324                continue;
325            }
326            // Coverage-collection asymmetry across calls within a stateful sequence:
327            // - line_coverage is per-call: `Executor::call_raw` returns a fresh HitMap each time,
328            //   so we can simply accumulate it.
329            // - sancov_coverage is the inspector's shared `Vec<u8>` buffer that keeps growing
330            //   across calls, so after consuming it we zero it out to avoid double-counting on the
331            //   next iteration.
332            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            // Stateful tests need the tx committed so subsequent calls see its effects.
356            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        // Final invariant + afterInvariant checks (replay mode only): mirror the
409        // campaign's "always check on the last call", and run afterInvariant unless a
410        // predicate already broke.
411        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            // <trial>__<uuid>-<ts>.txt
453            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            // Reset for the next entry; preserves capacity so we don't reallocate.
457            evm_buf.clear();
458            san_buf.fill(0);
459        }
460    }
461
462    if opts.emit_files && !opts.per_input {
463        // <trial>.txt
464        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    /// Whether to stop once every selected invariant has failed, mirroring a tmin campaign.
486    pub stop_at_campaign_end: bool,
487}
488
489/// Replays one candidate input and returns coverage/failure facts for minimizers.
490pub 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
627/// Whether the just-executed handler call passed the campaign's success gate.
628fn 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
670/// Returns replay failures produced directly by a handler call, mirroring the campaign:
671/// assertions are keyed by handler site, while a non-assertion revert breaks every invariant
672/// configured with `fail_on_revert`.
673fn 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
781/// Whether the invariant predicate should be evaluated after the `accepted`-th
782/// committed (non-`vm.assume`) call.
783///
784/// Mirrors the campaign: with `check_interval == 0` only the final call is checked
785/// (callers additionally perform a final check after the sequence ends); with
786/// `check_interval == 1` every call is checked; otherwise every N-th call.
787fn 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
794/// Saturating-add per-(bytecode, pc) hits from a `HitMaps` snapshot into `dst`.
795fn 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
805/// Saturating-add `src` (u8 raw counts) into `dst` (u64 aggregated counts).
806fn 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
818/// Write a single showmap file. Returns 1 if a file was written, 0 if skipped
819/// (no nonzero entries).
820fn write_showmap_file(path: &Path, evm: &EvmShowmap, san: &[u64]) -> Result<usize> {
821    // Pre-check so we don't create empty files.
822    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
840/// Each EVM ID is `evm_<bytecode_hash[:16hex]>_<pc:04x>`. The 16-hex prefix
841/// (64 bits) of the keccak256 bytecode hash makes IDs deterministic across
842/// processes while keeping line lengths short.
843fn 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            // Underscore (not `:`) between prefix and id keeps the showmap
858            // `<id>:<count>` parser unambiguous.
859            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); // skipped (count=0)
892        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        // check_interval == 1: every accepted call.
1038        assert!(should_check_invariant(1, 1, false));
1039        assert!(should_check_invariant(2, 1, false));
1040        // check_interval == 0: never inline (callers do a final check instead).
1041        assert!(!should_check_invariant(1, 0, false));
1042        assert!(!should_check_invariant(5, 0, false));
1043        // check_interval == N: every N-th accepted call.
1044        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        // Optimization mode evaluates every prefix to track the best value.
1049        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}