1use crate::inspectors::CmpOperands;
2use alloy_json_abi::Function;
3use alloy_primitives::{
4 Address, Bytes, I256, U256,
5 map::{Entry, HashMap},
6};
7use foundry_common::fs;
8use foundry_evm_fuzz::{BasicTxDetails, CallDetails, 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";
18pub(super) const FRONTIER_FILE: &str = "branch-frontiers.json";
19
20#[derive(Debug, Serialize)]
21pub(super) struct FuzzBranchFrontierArtifact {
22 schema: &'static str,
24 version: u32,
26 generated_at: u64,
28 test: String,
30 limit: usize,
32 frontiers: Vec<FuzzBranchFrontier>,
34}
35
36impl FuzzBranchFrontierArtifact {
37 pub(super) fn new(
38 func: &Function,
39 limit: usize,
40 mut frontiers: Vec<FuzzBranchFrontier>,
41 ) -> Self {
42 for (id, frontier) in frontiers.iter_mut().enumerate() {
43 frontier.id = id as u64;
44 }
45
46 Self {
47 schema: FRONTIER_SCHEMA,
48 version: 1,
49 generated_at: SystemTime::now()
50 .duration_since(UNIX_EPOCH)
51 .expect("time went backwards")
52 .as_secs(),
53 test: func.signature(),
54 limit,
55 frontiers,
56 }
57 }
58}
59
60#[derive(Debug, Serialize)]
61pub(super) struct FuzzBranchFrontier {
62 id: u64,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 seed: Option<U256>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 run: Option<u32>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 worker: Option<u32>,
73 #[serde(skip_serializing_if = "Option::is_none")]
76 new_coverage: Option<bool>,
77 call_index: usize,
79 #[serde(serialize_with = "serialize_sequence")]
81 sequence: Arc<[BasicTxDetails]>,
82 site: FuzzBranchFrontierSite,
84 operands: FuzzBranchFrontierOperands,
86}
87
88#[derive(Clone, Copy, Debug, Serialize)]
89struct FuzzBranchFrontierSite {
90 address: Address,
91 pc: usize,
92 opcode: u8,
93 opcode_name: &'static str,
94}
95
96#[derive(Clone, Copy, Debug, Serialize)]
97struct FuzzBranchFrontierOperands {
98 lhs: U256,
99 rhs: U256,
100 result: bool,
102 operand_delta: U256,
104}
105
106#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
107struct FuzzBranchFrontierKey {
108 address: Address,
109 pc: usize,
110 opcode: u8,
111 result: bool,
112}
113
114impl FuzzBranchFrontierKey {
115 const fn new(cmp: &CmpOperands, result: bool) -> Self {
116 Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode, result }
117 }
118}
119
120#[derive(Debug, Default)]
121pub(super) struct FuzzFrontierRecorder {
122 limit: usize,
123 frontiers: Vec<FuzzBranchFrontier>,
124 indexes: HashMap<FuzzBranchFrontierKey, usize>,
125}
126
127impl FuzzFrontierRecorder {
128 pub(super) fn new(limit: usize) -> Self {
129 Self { limit, frontiers: Vec::with_capacity(limit.min(32)), indexes: HashMap::default() }
130 }
131
132 pub(super) fn capture_stateless_call(
133 &mut self,
134 run: Option<&FuzzRunMetadata>,
135 sender: Address,
136 target: Address,
137 calldata: &Bytes,
138 cmp_values: &[CmpOperands],
139 new_coverage: Option<bool>,
140 ) {
141 if self.limit == 0 || cmp_values.is_empty() {
142 return;
143 }
144
145 let mut sequence = None;
146 let mut new_frontier = |cmp: &CmpOperands, result, operand_delta| {
147 let sequence = sequence.get_or_insert_with(|| {
148 let call = BasicTxDetails {
149 warp: None,
150 roll: None,
151 sender,
152 call_details: CallDetails { target, calldata: calldata.clone(), value: None },
153 };
154 Arc::from(Vec::from([call]).into_boxed_slice())
155 });
156 FuzzBranchFrontier::new(
157 run,
158 Arc::clone(sequence),
159 *cmp,
160 result,
161 operand_delta,
162 new_coverage,
163 0,
164 )
165 };
166
167 for cmp in cmp_values {
168 let result = comparison_result(cmp);
169 let key = FuzzBranchFrontierKey::new(cmp, result);
170 let operand_delta = operand_delta(cmp);
171
172 match self.indexes.entry(key) {
173 Entry::Occupied(entry) => {
174 let index = *entry.get();
175 if operand_delta < self.frontiers[index].operands.operand_delta {
176 self.frontiers[index] = new_frontier(cmp, result, operand_delta);
177 }
178 }
179 Entry::Vacant(entry) => {
180 if self.frontiers.len() < self.limit {
181 entry.insert(self.frontiers.len());
182 let frontier = new_frontier(cmp, result, operand_delta);
183 self.frontiers.push(frontier);
184 }
185 }
186 }
187 }
188 }
189
190 pub(super) fn into_frontiers(self) -> Vec<FuzzBranchFrontier> {
191 self.frontiers
192 }
193}
194
195impl FuzzBranchFrontier {
196 const fn key(&self) -> FuzzBranchFrontierKey {
197 FuzzBranchFrontierKey {
198 address: self.site.address,
199 pc: self.site.pc,
200 opcode: self.site.opcode,
201 result: self.operands.result,
202 }
203 }
204
205 fn new(
206 run: Option<&FuzzRunMetadata>,
207 sequence: Arc<[BasicTxDetails]>,
208 cmp: CmpOperands,
209 result: bool,
210 operand_delta: U256,
211 new_coverage: Option<bool>,
212 call_index: usize,
213 ) -> Self {
214 Self {
215 id: 0,
216 seed: run.and_then(|run| run.seed),
217 run: run.and_then(|run| run.run),
218 worker: run.and_then(|run| run.worker),
219 new_coverage,
220 call_index,
221 sequence,
222 site: FuzzBranchFrontierSite {
223 address: cmp.address,
224 pc: cmp.pc,
225 opcode: cmp.opcode,
226 opcode_name: opcode_name(cmp.opcode),
227 },
228 operands: FuzzBranchFrontierOperands {
229 lhs: cmp.op1,
230 rhs: cmp.op2,
231 result,
232 operand_delta,
233 },
234 }
235 }
236}
237
238pub(super) fn merge_frontiers(
247 limit: usize,
248 frontiers: impl IntoIterator<Item = FuzzBranchFrontier>,
249) -> Vec<FuzzBranchFrontier> {
250 if limit == 0 {
251 return Vec::new();
252 }
253
254 let mut merged = Vec::<FuzzBranchFrontier>::with_capacity(limit.min(32));
255 let mut indexes = HashMap::<FuzzBranchFrontierKey, usize>::default();
256 for frontier in frontiers {
257 match indexes.entry(frontier.key()) {
258 Entry::Occupied(entry) => {
259 let index = *entry.get();
260 if frontier.operands.operand_delta < merged[index].operands.operand_delta {
261 merged[index] = frontier;
262 }
263 }
264 Entry::Vacant(entry) => {
265 if merged.len() < limit {
266 entry.insert(merged.len());
267 merged.push(frontier);
268 }
269 }
270 }
271 }
272
273 merged
274}
275
276pub(super) fn write_frontier_artifact(
277 dir: &Path,
278 artifact: &FuzzBranchFrontierArtifact,
279) -> fs::Result<()> {
280 fs::create_dir_all(dir)?;
281 fs::write_json_file(&dir.join(FRONTIER_FILE), artifact)
282}
283
284fn serialize_sequence<S>(sequence: &Arc<[BasicTxDetails]>, serializer: S) -> Result<S::Ok, S::Error>
285where
286 S: Serializer,
287{
288 Serialize::serialize(sequence.as_ref(), serializer)
289}
290
291fn comparison_result(cmp: &CmpOperands) -> bool {
292 match cmp.opcode {
293 opcode::EQ => cmp.op1 == cmp.op2,
294 opcode::LT => cmp.op1 < cmp.op2,
295 opcode::GT => cmp.op1 > cmp.op2,
296 opcode::SLT => I256::from_raw(cmp.op1) < I256::from_raw(cmp.op2),
297 opcode::SGT => I256::from_raw(cmp.op1) > I256::from_raw(cmp.op2),
298 opcode::ISZERO => cmp.op1.is_zero(),
299 _ => false,
300 }
301}
302
303fn operand_delta(cmp: &CmpOperands) -> U256 {
304 match cmp.opcode {
305 opcode::SLT | opcode::SGT => signed_operand_delta(cmp.op1, cmp.op2),
306 _ => unsigned_operand_delta(cmp.op1, cmp.op2),
307 }
308}
309
310fn unsigned_operand_delta(left: U256, right: U256) -> U256 {
311 if left >= right { left - right } else { right - left }
312}
313
314fn signed_operand_delta(left: U256, right: U256) -> U256 {
315 let (left_negative, left_magnitude) = signed_magnitude(left);
316 let (right_negative, right_magnitude) = signed_magnitude(right);
317
318 if left_negative == right_negative {
319 unsigned_operand_delta(left_magnitude, right_magnitude)
320 } else {
321 left_magnitude + right_magnitude
322 }
323}
324
325fn signed_magnitude(value: U256) -> (bool, U256) {
326 let negative = I256::from_raw(value) < I256::ZERO;
327 let magnitude = if negative { U256::ZERO.wrapping_sub(value) } else { value };
328 (negative, magnitude)
329}
330
331const fn opcode_name(op: u8) -> &'static str {
332 match op {
333 opcode::EQ => "EQ",
334 opcode::LT => "LT",
335 opcode::GT => "GT",
336 opcode::SLT => "SLT",
337 opcode::SGT => "SGT",
338 opcode::ISZERO => "ISZERO",
339 _ => "UNKNOWN",
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn operand_delta_uses_signed_distance_for_signed_comparisons() {
349 let unsigned = CmpOperands {
350 op1: U256::MAX,
351 op2: U256::from(1),
352 pc: 0,
353 address: Address::ZERO,
354 opcode: opcode::LT,
355 };
356 assert_eq!(operand_delta(&unsigned), U256::MAX - U256::from(1));
357
358 let signed = CmpOperands { opcode: opcode::SLT, ..unsigned };
359 assert_eq!(operand_delta(&signed), U256::from(2));
360 }
361
362 #[test]
363 fn signed_operand_delta_handles_full_int256_range() {
364 let min = I256::MIN.into_raw();
365 let max = I256::MAX.into_raw();
366
367 assert_eq!(signed_operand_delta(min, max), U256::MAX);
368 }
369
370 fn frontier(pc: usize, result: bool, operand_delta: u64) -> FuzzBranchFrontier {
371 frontier_at(Address::ZERO, pc, result, operand_delta)
372 }
373
374 fn frontier_at(
375 address: Address,
376 pc: usize,
377 result: bool,
378 operand_delta: u64,
379 ) -> FuzzBranchFrontier {
380 let cmp = CmpOperands { op1: U256::ZERO, op2: U256::ZERO, pc, address, opcode: opcode::LT };
381 FuzzBranchFrontier::new(
382 None,
383 Arc::from(Vec::<BasicTxDetails>::new().into_boxed_slice()),
384 cmp,
385 result,
386 U256::from(operand_delta),
387 None,
388 0,
389 )
390 }
391
392 #[test]
393 fn merge_frontiers_dedupes_across_workers_keeping_smallest_delta() {
394 let merged = merge_frontiers(
395 8,
396 [frontier(1, false, 30), frontier(1, false, 10), frontier(1, false, 20)],
397 );
398
399 assert_eq!(merged.len(), 1);
400 assert_eq!(merged[0].operands.operand_delta, U256::from(10));
401 }
402
403 #[test]
404 fn merge_frontiers_keeps_records_with_distinct_result_keys() {
405 let merged = merge_frontiers(8, [frontier(1, false, 5), frontier(1, true, 5)]);
407
408 assert_eq!(merged.len(), 2);
409 }
410
411 #[test]
412 fn merge_frontiers_replaces_retained_key_after_limit_is_full() {
413 let merged = merge_frontiers(
416 2,
417 [
418 frontier(1, false, 30),
419 frontier(2, false, 30),
420 frontier(3, false, 5),
421 frontier(1, false, 10),
422 ],
423 );
424
425 assert_eq!(merged.len(), 2);
426 let retained = merged.iter().find(|f| f.site.pc == 1).unwrap();
427 assert_eq!(retained.operands.operand_delta, U256::from(10));
428 assert!(merged.iter().all(|f| f.site.pc != 3));
429 }
430
431 #[test]
432 fn merge_frontiers_does_not_spend_limit_on_duplicates() {
433 let merged = merge_frontiers(
436 2,
437 [frontier(1, false, 30), frontier(1, false, 10), frontier(2, false, 5)],
438 );
439
440 assert_eq!(merged.len(), 2);
441 assert_eq!(
442 merged.iter().find(|f| f.site.pc == 1).unwrap().operands.operand_delta,
443 U256::from(10)
444 );
445 assert!(merged.iter().any(|f| f.site.pc == 2));
446 }
447
448 #[test]
449 fn merge_frontiers_distinguishes_by_address() {
450 let other = Address::with_last_byte(1);
452 let merged = merge_frontiers(
453 8,
454 [frontier_at(Address::ZERO, 1, false, 5), frontier_at(other, 1, false, 5)],
455 );
456
457 assert_eq!(merged.len(), 2);
458 }
459
460 #[test]
461 fn merge_frontiers_with_zero_limit_is_empty() {
462 assert!(merge_frontiers(0, [frontier(1, false, 1)]).is_empty());
463 }
464}