1use crate::inspectors::CmpOperands;
2use alloy_json_abi::Function;
3use alloy_primitives::{
4 Address, I256, U256,
5 map::{Entry, HashMap},
6};
7use foundry_common::fs;
8use foundry_evm_fuzz::{BasicTxDetails, FuzzRunMetadata};
9use revm::bytecode::opcode;
10use serde::{Serialize, Serializer};
11use std::{
12 path::Path,
13 sync::Arc,
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17const FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v1";
18const STATEFUL_FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v2";
19pub(super) const FRONTIER_FILE: &str = "branch-frontiers.json";
20
21#[derive(Debug, Serialize)]
22pub(super) struct FuzzBranchFrontierArtifact {
23 schema: &'static str,
25 version: u32,
27 generated_at: u64,
29 test: String,
31 limit: usize,
33 frontiers: Vec<FuzzBranchFrontier>,
35}
36
37impl FuzzBranchFrontierArtifact {
38 pub(super) fn new(
39 func: &Function,
40 limit: usize,
41 mut frontiers: Vec<FuzzBranchFrontier>,
42 ) -> Self {
43 for (id, frontier) in frontiers.iter_mut().enumerate() {
44 frontier.id = id as u64;
45 }
46
47 Self {
48 schema: FRONTIER_SCHEMA,
49 version: 1,
50 generated_at: SystemTime::now()
51 .duration_since(UNIX_EPOCH)
52 .expect("time went backwards")
53 .as_secs(),
54 test: func.signature(),
55 limit,
56 frontiers,
57 }
58 }
59}
60
61#[derive(Debug, Serialize)]
62pub(in crate::executors) struct StatefulFuzzBranchFrontierArtifact {
63 schema: &'static str,
65 version: u32,
67 generated_at: u64,
69 test: String,
71 limit: usize,
73 #[serde(serialize_with = "serialize_sequences")]
75 sequences: Vec<StatefulFuzzSequence>,
76 frontiers: Vec<StatefulFuzzBranchFrontier>,
78}
79
80impl StatefulFuzzBranchFrontierArtifact {
81 pub(in crate::executors) fn new(
82 func: &Function,
83 limit: usize,
84 frontiers: Vec<FuzzBranchFrontier>,
85 ) -> Self {
86 let mut sequence_indexes = HashMap::<*const BasicTxDetails, usize>::default();
87 let mut sequences = Vec::<StatefulFuzzSequence>::new();
88 let mut records = Vec::with_capacity(frontiers.len());
89 for (id, frontier) in frontiers.into_iter().enumerate() {
90 let sequence_key = frontier.sequence.as_ptr();
91 let required_len = frontier.call_index + 1;
92 let sequence_index = match sequence_indexes.entry(sequence_key) {
93 Entry::Occupied(entry) => {
94 let index = *entry.get();
95 sequences[index].len = sequences[index].len.max(required_len);
96 index
97 }
98 Entry::Vacant(entry) => {
99 let index = sequences.len();
100 entry.insert(index);
101 sequences.push(StatefulFuzzSequence {
102 calls: Arc::clone(&frontier.sequence),
103 len: required_len,
104 });
105 index
106 }
107 };
108 records.push(StatefulFuzzBranchFrontier {
109 id: id as u64,
110 call_index: frontier.call_index,
111 sequence_index,
112 site: frontier.site,
113 operands: frontier.operands,
114 });
115 }
116
117 Self {
118 schema: STATEFUL_FRONTIER_SCHEMA,
119 version: 2,
120 generated_at: SystemTime::now()
121 .duration_since(UNIX_EPOCH)
122 .expect("time went backwards")
123 .as_secs(),
124 test: func.signature(),
125 limit,
126 sequences,
127 frontiers: records,
128 }
129 }
130}
131
132#[derive(Debug)]
133struct StatefulFuzzSequence {
134 calls: Arc<[BasicTxDetails]>,
135 len: usize,
136}
137
138#[derive(Debug, Serialize)]
139pub(in crate::executors) struct FuzzBranchFrontier {
140 id: u64,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 seed: Option<U256>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 run: Option<u32>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 worker: Option<u32>,
151 #[serde(skip_serializing_if = "Option::is_none")]
154 new_coverage: Option<bool>,
155 call_index: usize,
157 #[serde(serialize_with = "serialize_sequence")]
159 sequence: Arc<[BasicTxDetails]>,
160 site: FuzzBranchFrontierSite,
162 operands: FuzzBranchFrontierOperands,
164}
165
166#[derive(Debug, Serialize)]
167struct StatefulFuzzBranchFrontier {
168 id: u64,
169 call_index: usize,
170 sequence_index: usize,
171 site: FuzzBranchFrontierSite,
172 operands: FuzzBranchFrontierOperands,
173}
174
175#[derive(Clone, Copy, Debug, Serialize)]
176struct FuzzBranchFrontierSite {
177 address: Address,
178 pc: usize,
179 opcode: u8,
180 opcode_name: &'static str,
181}
182
183#[derive(Clone, Copy, Debug, Serialize)]
184struct FuzzBranchFrontierOperands {
185 lhs: U256,
186 rhs: U256,
187 result: bool,
189 operand_delta: U256,
191}
192
193#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
194struct FuzzBranchFrontierKey {
195 address: Address,
196 pc: usize,
197 opcode: u8,
198 result: bool,
199}
200
201impl FuzzBranchFrontierKey {
202 const fn new(cmp: &CmpOperands, result: bool) -> Self {
203 Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode, result }
204 }
205}
206
207#[derive(Debug, Default)]
208pub(in crate::executors) struct FuzzFrontierRecorder {
209 limit: usize,
210 frontiers: Vec<FuzzBranchFrontier>,
211 indexes: HashMap<FuzzBranchFrontierKey, usize>,
212}
213
214impl FuzzFrontierRecorder {
215 pub(in crate::executors) fn new(limit: usize) -> Self {
216 Self { limit, frontiers: Vec::with_capacity(limit.min(32)), indexes: HashMap::default() }
217 }
218
219 pub(super) fn capture_call(
220 &mut self,
221 run: Option<&FuzzRunMetadata>,
222 sequence: &[BasicTxDetails],
223 call_index: usize,
224 cmp_values: &[CmpOperands],
225 new_coverage: Option<bool>,
226 ) {
227 if self.limit == 0 || cmp_values.is_empty() {
228 return;
229 }
230 debug_assert!(call_index < sequence.len());
231
232 let mut recorded_sequence = None;
233 self.capture_comparisons(
234 run,
235 sequence,
236 call_index,
237 cmp_values,
238 new_coverage,
239 &mut recorded_sequence,
240 );
241 }
242
243 pub(in crate::executors) fn capture_sequence(
244 &mut self,
245 sequence: &[BasicTxDetails],
246 cmp_sequence: &[Vec<CmpOperands>],
247 ) {
248 if self.limit == 0 || cmp_sequence.is_empty() {
249 return;
250 }
251 debug_assert!(sequence.len() >= cmp_sequence.len());
252 let sequence = &sequence[..cmp_sequence.len()];
253
254 let mut recorded_sequence = None;
255 for (call_index, cmp_values) in cmp_sequence.iter().enumerate() {
256 self.capture_comparisons(
257 None,
258 sequence,
259 call_index,
260 cmp_values,
261 None,
262 &mut recorded_sequence,
263 );
264 }
265 }
266
267 fn capture_comparisons(
268 &mut self,
269 run: Option<&FuzzRunMetadata>,
270 sequence: &[BasicTxDetails],
271 call_index: usize,
272 cmp_values: &[CmpOperands],
273 new_coverage: Option<bool>,
274 recorded_sequence: &mut Option<Arc<[BasicTxDetails]>>,
275 ) {
276 if cmp_values.is_empty() {
277 return;
278 }
279
280 let mut new_frontier = |cmp: &CmpOperands, result, operand_delta| {
281 let sequence = recorded_sequence.get_or_insert_with(|| Arc::from(sequence));
282 FuzzBranchFrontier::new(
283 run,
284 Arc::clone(sequence),
285 *cmp,
286 result,
287 operand_delta,
288 new_coverage,
289 call_index,
290 )
291 };
292
293 for cmp in cmp_values {
294 let result = comparison_result(cmp);
295 let key = FuzzBranchFrontierKey::new(cmp, result);
296 let operand_delta = operand_delta(cmp);
297
298 match self.indexes.entry(key) {
299 Entry::Occupied(entry) => {
300 let index = *entry.get();
301 let previous = &self.frontiers[index];
302 if operand_delta < previous.operands.operand_delta
303 || (operand_delta == previous.operands.operand_delta
304 && call_index < previous.call_index)
305 {
306 self.frontiers[index] = new_frontier(cmp, result, operand_delta);
307 }
308 }
309 Entry::Vacant(entry) => {
310 if self.frontiers.len() < self.limit {
311 entry.insert(self.frontiers.len());
312 let frontier = new_frontier(cmp, result, operand_delta);
313 self.frontiers.push(frontier);
314 }
315 }
316 }
317 }
318 }
319
320 pub(in crate::executors) fn into_frontiers(self) -> Vec<FuzzBranchFrontier> {
321 self.frontiers
322 }
323}
324
325impl FuzzBranchFrontier {
326 const fn key(&self) -> FuzzBranchFrontierKey {
327 FuzzBranchFrontierKey {
328 address: self.site.address,
329 pc: self.site.pc,
330 opcode: self.site.opcode,
331 result: self.operands.result,
332 }
333 }
334
335 fn new(
336 run: Option<&FuzzRunMetadata>,
337 sequence: Arc<[BasicTxDetails]>,
338 cmp: CmpOperands,
339 result: bool,
340 operand_delta: U256,
341 new_coverage: Option<bool>,
342 call_index: usize,
343 ) -> Self {
344 Self {
345 id: 0,
346 seed: run.and_then(|run| run.seed),
347 run: run.and_then(|run| run.run),
348 worker: run.and_then(|run| run.worker),
349 new_coverage,
350 call_index,
351 sequence,
352 site: FuzzBranchFrontierSite {
353 address: cmp.address,
354 pc: cmp.pc,
355 opcode: cmp.opcode,
356 opcode_name: opcode_name(cmp.opcode),
357 },
358 operands: FuzzBranchFrontierOperands {
359 lhs: cmp.op1,
360 rhs: cmp.op2,
361 result,
362 operand_delta,
363 },
364 }
365 }
366}
367
368pub(in crate::executors) fn merge_frontiers(
377 limit: usize,
378 frontiers: impl IntoIterator<Item = FuzzBranchFrontier>,
379) -> Vec<FuzzBranchFrontier> {
380 if limit == 0 {
381 return Vec::new();
382 }
383
384 let mut merged = Vec::<FuzzBranchFrontier>::with_capacity(limit.min(32));
385 let mut indexes = HashMap::<FuzzBranchFrontierKey, usize>::default();
386 for frontier in frontiers {
387 match indexes.entry(frontier.key()) {
388 Entry::Occupied(entry) => {
389 let index = *entry.get();
390 let previous = &merged[index];
391 if frontier.operands.operand_delta < previous.operands.operand_delta
392 || (frontier.operands.operand_delta == previous.operands.operand_delta
393 && frontier.call_index < previous.call_index)
394 {
395 merged[index] = frontier;
396 }
397 }
398 Entry::Vacant(entry) => {
399 if merged.len() < limit {
400 entry.insert(merged.len());
401 merged.push(frontier);
402 }
403 }
404 }
405 }
406
407 merged
408}
409
410pub(in crate::executors) fn write_frontier_artifact(
411 dir: &Path,
412 artifact: &impl Serialize,
413) -> fs::Result<()> {
414 fs::create_dir_all(dir)?;
415 fs::write_json_file(&dir.join(FRONTIER_FILE), artifact)
416}
417
418fn serialize_sequence<S>(sequence: &Arc<[BasicTxDetails]>, serializer: S) -> Result<S::Ok, S::Error>
419where
420 S: Serializer,
421{
422 Serialize::serialize(sequence.as_ref(), serializer)
423}
424
425fn serialize_sequences<S>(
426 sequences: &[StatefulFuzzSequence],
427 serializer: S,
428) -> Result<S::Ok, S::Error>
429where
430 S: Serializer,
431{
432 serializer.collect_seq(sequences.iter().map(|sequence| &sequence.calls[..sequence.len]))
433}
434
435fn comparison_result(cmp: &CmpOperands) -> bool {
436 match cmp.opcode {
437 opcode::EQ => cmp.op1 == cmp.op2,
438 opcode::LT => cmp.op1 < cmp.op2,
439 opcode::GT => cmp.op1 > cmp.op2,
440 opcode::SLT => I256::from_raw(cmp.op1) < I256::from_raw(cmp.op2),
441 opcode::SGT => I256::from_raw(cmp.op1) > I256::from_raw(cmp.op2),
442 opcode::ISZERO => cmp.op1.is_zero(),
443 _ => false,
444 }
445}
446
447fn operand_delta(cmp: &CmpOperands) -> U256 {
448 match cmp.opcode {
449 opcode::SLT | opcode::SGT => signed_operand_delta(cmp.op1, cmp.op2),
450 _ => unsigned_operand_delta(cmp.op1, cmp.op2),
451 }
452}
453
454fn unsigned_operand_delta(left: U256, right: U256) -> U256 {
455 if left >= right { left - right } else { right - left }
456}
457
458fn signed_operand_delta(left: U256, right: U256) -> U256 {
459 let (left_negative, left_magnitude) = signed_magnitude(left);
460 let (right_negative, right_magnitude) = signed_magnitude(right);
461
462 if left_negative == right_negative {
463 unsigned_operand_delta(left_magnitude, right_magnitude)
464 } else {
465 left_magnitude + right_magnitude
466 }
467}
468
469fn signed_magnitude(value: U256) -> (bool, U256) {
470 let negative = I256::from_raw(value) < I256::ZERO;
471 let magnitude = if negative { U256::ZERO.wrapping_sub(value) } else { value };
472 (negative, magnitude)
473}
474
475const fn opcode_name(op: u8) -> &'static str {
476 match op {
477 opcode::EQ => "EQ",
478 opcode::LT => "LT",
479 opcode::GT => "GT",
480 opcode::SLT => "SLT",
481 opcode::SGT => "SGT",
482 opcode::ISZERO => "ISZERO",
483 _ => "UNKNOWN",
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use alloy_primitives::Bytes;
491 use foundry_evm_fuzz::CallDetails;
492
493 #[test]
494 fn stateless_frontier_preserves_concrete_transaction() {
495 let tx = BasicTxDetails {
496 warp: None,
497 roll: None,
498 sender: Address::with_last_byte(1),
499 call_details: CallDetails {
500 target: Address::with_last_byte(2),
501 calldata: Bytes::from_static(&[1, 2, 3, 4]),
502 value: Some(U256::from(7)),
503 },
504 };
505 let cmp = CmpOperands {
506 op1: U256::ZERO,
507 op2: U256::from(1),
508 pc: 1,
509 address: tx.call_details.target,
510 opcode: opcode::LT,
511 };
512 let mut recorder = FuzzFrontierRecorder::new(1);
513
514 recorder.capture_call(None, std::slice::from_ref(&tx), 0, &[cmp], Some(true));
515
516 let frontier = recorder.into_frontiers().pop().unwrap();
517 assert_eq!(frontier.sequence.len(), 1);
518 assert_eq!(frontier.call_index, 0);
519 let recorded = &frontier.sequence[0];
520 assert_eq!(recorded.sender, tx.sender);
521 assert_eq!(recorded.call_details.target, tx.call_details.target);
522 assert_eq!(recorded.call_details.calldata, tx.call_details.calldata);
523 assert_eq!(recorded.call_details.value, tx.call_details.value);
524 }
525
526 #[test]
527 fn operand_delta_uses_signed_distance_for_signed_comparisons() {
528 let unsigned = CmpOperands {
529 op1: U256::MAX,
530 op2: U256::from(1),
531 pc: 0,
532 address: Address::ZERO,
533 opcode: opcode::LT,
534 };
535 assert_eq!(operand_delta(&unsigned), U256::MAX - U256::from(1));
536
537 let signed = CmpOperands { opcode: opcode::SLT, ..unsigned };
538 assert_eq!(operand_delta(&signed), U256::from(2));
539 }
540
541 #[test]
542 fn signed_operand_delta_handles_full_int256_range() {
543 let min = I256::MIN.into_raw();
544 let max = I256::MAX.into_raw();
545
546 assert_eq!(signed_operand_delta(min, max), U256::MAX);
547 }
548
549 fn frontier(pc: usize, result: bool, operand_delta: u64) -> FuzzBranchFrontier {
550 frontier_at(Address::ZERO, pc, result, operand_delta)
551 }
552
553 fn frontier_at(
554 address: Address,
555 pc: usize,
556 result: bool,
557 operand_delta: u64,
558 ) -> FuzzBranchFrontier {
559 let cmp = CmpOperands { op1: U256::ZERO, op2: U256::ZERO, pc, address, opcode: opcode::LT };
560 FuzzBranchFrontier::new(
561 None,
562 Arc::from(Vec::<BasicTxDetails>::new().into_boxed_slice()),
563 cmp,
564 result,
565 U256::from(operand_delta),
566 None,
567 0,
568 )
569 }
570
571 #[test]
572 fn merge_frontiers_dedupes_across_workers_keeping_smallest_delta() {
573 let merged = merge_frontiers(
574 8,
575 [frontier(1, false, 30), frontier(1, false, 10), frontier(1, false, 20)],
576 );
577
578 assert_eq!(merged.len(), 1);
579 assert_eq!(merged[0].operands.operand_delta, U256::from(10));
580 }
581
582 #[test]
583 fn frontier_keeps_shortest_stateful_sequence_for_equal_delta() {
584 let tx = BasicTxDetails {
585 warp: None,
586 roll: None,
587 sender: Address::with_last_byte(1),
588 call_details: CallDetails {
589 target: Address::with_last_byte(2),
590 calldata: Bytes::from_static(&[1, 2, 3, 4]),
591 value: None,
592 },
593 };
594 let cmp = CmpOperands {
595 op1: U256::from(1),
596 op2: U256::from(2),
597 pc: 1,
598 address: tx.call_details.target,
599 opcode: opcode::LT,
600 };
601 let mut recorder = FuzzFrontierRecorder::new(1);
602
603 recorder.capture_call(None, &[tx.clone(), tx.clone()], 1, &[cmp], None);
604 recorder.capture_call(None, std::slice::from_ref(&tx), 0, &[cmp], None);
605
606 let frontier = recorder.into_frontiers().pop().unwrap();
607 assert_eq!(frontier.sequence.len(), 1);
608 assert_eq!(frontier.call_index, 0);
609 }
610
611 #[test]
612 fn stateful_artifact_stores_shared_sequence_once() {
613 let tx = BasicTxDetails {
614 warp: None,
615 roll: None,
616 sender: Address::with_last_byte(1),
617 call_details: CallDetails {
618 target: Address::with_last_byte(2),
619 calldata: Bytes::from_static(&[1, 2, 3, 4]),
620 value: None,
621 },
622 };
623 let first = CmpOperands {
624 op1: U256::ZERO,
625 op2: U256::from(1),
626 pc: 1,
627 address: tx.call_details.target,
628 opcode: opcode::LT,
629 };
630 let second = CmpOperands { pc: 2, ..first };
631 let mut recorder = FuzzFrontierRecorder::new(2);
632 recorder.capture_sequence(&[tx.clone(), tx], &[vec![first], vec![second]]);
633 let func: Function = serde_json::from_str(
634 r#"{"type":"function","name":"invariant_ok","inputs":[],"outputs":[],"stateMutability":"pure"}"#,
635 )
636 .unwrap();
637
638 let artifact = StatefulFuzzBranchFrontierArtifact::new(&func, 2, recorder.into_frontiers());
639
640 assert_eq!(artifact.sequences.len(), 1);
641 assert_eq!(artifact.sequences[0].len, 2);
642 assert_eq!(artifact.frontiers.len(), 2);
643 assert_eq!(artifact.frontiers[0].sequence_index, 0);
644 assert_eq!(artifact.frontiers[1].sequence_index, 0);
645 assert_eq!(artifact.frontiers[0].call_index, 0);
646 assert_eq!(artifact.frontiers[1].call_index, 1);
647 }
648
649 #[test]
650 fn merge_frontiers_keeps_records_with_distinct_result_keys() {
651 let merged = merge_frontiers(8, [frontier(1, false, 5), frontier(1, true, 5)]);
653
654 assert_eq!(merged.len(), 2);
655 }
656
657 #[test]
658 fn merge_frontiers_replaces_retained_key_after_limit_is_full() {
659 let merged = merge_frontiers(
662 2,
663 [
664 frontier(1, false, 30),
665 frontier(2, false, 30),
666 frontier(3, false, 5),
667 frontier(1, false, 10),
668 ],
669 );
670
671 assert_eq!(merged.len(), 2);
672 let retained = merged.iter().find(|f| f.site.pc == 1).unwrap();
673 assert_eq!(retained.operands.operand_delta, U256::from(10));
674 assert!(merged.iter().all(|f| f.site.pc != 3));
675 }
676
677 #[test]
678 fn merge_frontiers_does_not_spend_limit_on_duplicates() {
679 let merged = merge_frontiers(
682 2,
683 [frontier(1, false, 30), frontier(1, false, 10), frontier(2, false, 5)],
684 );
685
686 assert_eq!(merged.len(), 2);
687 assert_eq!(
688 merged.iter().find(|f| f.site.pc == 1).unwrap().operands.operand_delta,
689 U256::from(10)
690 );
691 assert!(merged.iter().any(|f| f.site.pc == 2));
692 }
693
694 #[test]
695 fn merge_frontiers_distinguishes_by_address() {
696 let other = Address::with_last_byte(1);
698 let merged = merge_frontiers(
699 8,
700 [frontier_at(Address::ZERO, 1, false, 5), frontier_at(other, 1, false, 5)],
701 );
702
703 assert_eq!(merged.len(), 2);
704 }
705
706 #[test]
707 fn merge_frontiers_with_zero_limit_is_empty() {
708 assert!(merge_frontiers(0, [frontier(1, false, 1)]).is_empty());
709 }
710}