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";
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 tx: &BasicTxDetails,
136 cmp_values: &[CmpOperands],
137 new_coverage: Option<bool>,
138 ) {
139 if self.limit == 0 || cmp_values.is_empty() {
140 return;
141 }
142
143 let mut sequence = None;
144 let mut new_frontier = |cmp: &CmpOperands, result, operand_delta| {
145 let sequence = sequence
146 .get_or_insert_with(|| Arc::from(Vec::from([tx.clone()]).into_boxed_slice()));
147 FuzzBranchFrontier::new(
148 run,
149 Arc::clone(sequence),
150 *cmp,
151 result,
152 operand_delta,
153 new_coverage,
154 0,
155 )
156 };
157
158 for cmp in cmp_values {
159 let result = comparison_result(cmp);
160 let key = FuzzBranchFrontierKey::new(cmp, result);
161 let operand_delta = operand_delta(cmp);
162
163 match self.indexes.entry(key) {
164 Entry::Occupied(entry) => {
165 let index = *entry.get();
166 if operand_delta < self.frontiers[index].operands.operand_delta {
167 self.frontiers[index] = new_frontier(cmp, result, operand_delta);
168 }
169 }
170 Entry::Vacant(entry) => {
171 if self.frontiers.len() < self.limit {
172 entry.insert(self.frontiers.len());
173 let frontier = new_frontier(cmp, result, operand_delta);
174 self.frontiers.push(frontier);
175 }
176 }
177 }
178 }
179 }
180
181 pub(super) fn into_frontiers(self) -> Vec<FuzzBranchFrontier> {
182 self.frontiers
183 }
184}
185
186impl FuzzBranchFrontier {
187 const fn key(&self) -> FuzzBranchFrontierKey {
188 FuzzBranchFrontierKey {
189 address: self.site.address,
190 pc: self.site.pc,
191 opcode: self.site.opcode,
192 result: self.operands.result,
193 }
194 }
195
196 fn new(
197 run: Option<&FuzzRunMetadata>,
198 sequence: Arc<[BasicTxDetails]>,
199 cmp: CmpOperands,
200 result: bool,
201 operand_delta: U256,
202 new_coverage: Option<bool>,
203 call_index: usize,
204 ) -> Self {
205 Self {
206 id: 0,
207 seed: run.and_then(|run| run.seed),
208 run: run.and_then(|run| run.run),
209 worker: run.and_then(|run| run.worker),
210 new_coverage,
211 call_index,
212 sequence,
213 site: FuzzBranchFrontierSite {
214 address: cmp.address,
215 pc: cmp.pc,
216 opcode: cmp.opcode,
217 opcode_name: opcode_name(cmp.opcode),
218 },
219 operands: FuzzBranchFrontierOperands {
220 lhs: cmp.op1,
221 rhs: cmp.op2,
222 result,
223 operand_delta,
224 },
225 }
226 }
227}
228
229pub(super) fn merge_frontiers(
238 limit: usize,
239 frontiers: impl IntoIterator<Item = FuzzBranchFrontier>,
240) -> Vec<FuzzBranchFrontier> {
241 if limit == 0 {
242 return Vec::new();
243 }
244
245 let mut merged = Vec::<FuzzBranchFrontier>::with_capacity(limit.min(32));
246 let mut indexes = HashMap::<FuzzBranchFrontierKey, usize>::default();
247 for frontier in frontiers {
248 match indexes.entry(frontier.key()) {
249 Entry::Occupied(entry) => {
250 let index = *entry.get();
251 if frontier.operands.operand_delta < merged[index].operands.operand_delta {
252 merged[index] = frontier;
253 }
254 }
255 Entry::Vacant(entry) => {
256 if merged.len() < limit {
257 entry.insert(merged.len());
258 merged.push(frontier);
259 }
260 }
261 }
262 }
263
264 merged
265}
266
267pub(super) fn write_frontier_artifact(
268 dir: &Path,
269 artifact: &FuzzBranchFrontierArtifact,
270) -> fs::Result<()> {
271 fs::create_dir_all(dir)?;
272 fs::write_json_file(&dir.join(FRONTIER_FILE), artifact)
273}
274
275fn serialize_sequence<S>(sequence: &Arc<[BasicTxDetails]>, serializer: S) -> Result<S::Ok, S::Error>
276where
277 S: Serializer,
278{
279 Serialize::serialize(sequence.as_ref(), serializer)
280}
281
282fn comparison_result(cmp: &CmpOperands) -> bool {
283 match cmp.opcode {
284 opcode::EQ => cmp.op1 == cmp.op2,
285 opcode::LT => cmp.op1 < cmp.op2,
286 opcode::GT => cmp.op1 > cmp.op2,
287 opcode::SLT => I256::from_raw(cmp.op1) < I256::from_raw(cmp.op2),
288 opcode::SGT => I256::from_raw(cmp.op1) > I256::from_raw(cmp.op2),
289 opcode::ISZERO => cmp.op1.is_zero(),
290 _ => false,
291 }
292}
293
294fn operand_delta(cmp: &CmpOperands) -> U256 {
295 match cmp.opcode {
296 opcode::SLT | opcode::SGT => signed_operand_delta(cmp.op1, cmp.op2),
297 _ => unsigned_operand_delta(cmp.op1, cmp.op2),
298 }
299}
300
301fn unsigned_operand_delta(left: U256, right: U256) -> U256 {
302 if left >= right { left - right } else { right - left }
303}
304
305fn signed_operand_delta(left: U256, right: U256) -> U256 {
306 let (left_negative, left_magnitude) = signed_magnitude(left);
307 let (right_negative, right_magnitude) = signed_magnitude(right);
308
309 if left_negative == right_negative {
310 unsigned_operand_delta(left_magnitude, right_magnitude)
311 } else {
312 left_magnitude + right_magnitude
313 }
314}
315
316fn signed_magnitude(value: U256) -> (bool, U256) {
317 let negative = I256::from_raw(value) < I256::ZERO;
318 let magnitude = if negative { U256::ZERO.wrapping_sub(value) } else { value };
319 (negative, magnitude)
320}
321
322const fn opcode_name(op: u8) -> &'static str {
323 match op {
324 opcode::EQ => "EQ",
325 opcode::LT => "LT",
326 opcode::GT => "GT",
327 opcode::SLT => "SLT",
328 opcode::SGT => "SGT",
329 opcode::ISZERO => "ISZERO",
330 _ => "UNKNOWN",
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use alloy_primitives::Bytes;
338 use foundry_evm_fuzz::CallDetails;
339
340 #[test]
341 fn stateless_frontier_preserves_concrete_transaction() {
342 let tx = BasicTxDetails {
343 warp: None,
344 roll: None,
345 sender: Address::with_last_byte(1),
346 call_details: CallDetails {
347 target: Address::with_last_byte(2),
348 calldata: Bytes::from_static(&[1, 2, 3, 4]),
349 value: Some(U256::from(7)),
350 },
351 };
352 let cmp = CmpOperands {
353 op1: U256::ZERO,
354 op2: U256::from(1),
355 pc: 1,
356 address: tx.call_details.target,
357 opcode: opcode::LT,
358 };
359 let mut recorder = FuzzFrontierRecorder::new(1);
360
361 recorder.capture_stateless_call(None, &tx, &[cmp], Some(true));
362
363 let frontier = recorder.into_frontiers().pop().unwrap();
364 assert_eq!(frontier.sequence.len(), 1);
365 let recorded = &frontier.sequence[0];
366 assert_eq!(recorded.sender, tx.sender);
367 assert_eq!(recorded.call_details.target, tx.call_details.target);
368 assert_eq!(recorded.call_details.calldata, tx.call_details.calldata);
369 assert_eq!(recorded.call_details.value, tx.call_details.value);
370 }
371
372 #[test]
373 fn operand_delta_uses_signed_distance_for_signed_comparisons() {
374 let unsigned = CmpOperands {
375 op1: U256::MAX,
376 op2: U256::from(1),
377 pc: 0,
378 address: Address::ZERO,
379 opcode: opcode::LT,
380 };
381 assert_eq!(operand_delta(&unsigned), U256::MAX - U256::from(1));
382
383 let signed = CmpOperands { opcode: opcode::SLT, ..unsigned };
384 assert_eq!(operand_delta(&signed), U256::from(2));
385 }
386
387 #[test]
388 fn signed_operand_delta_handles_full_int256_range() {
389 let min = I256::MIN.into_raw();
390 let max = I256::MAX.into_raw();
391
392 assert_eq!(signed_operand_delta(min, max), U256::MAX);
393 }
394
395 fn frontier(pc: usize, result: bool, operand_delta: u64) -> FuzzBranchFrontier {
396 frontier_at(Address::ZERO, pc, result, operand_delta)
397 }
398
399 fn frontier_at(
400 address: Address,
401 pc: usize,
402 result: bool,
403 operand_delta: u64,
404 ) -> FuzzBranchFrontier {
405 let cmp = CmpOperands { op1: U256::ZERO, op2: U256::ZERO, pc, address, opcode: opcode::LT };
406 FuzzBranchFrontier::new(
407 None,
408 Arc::from(Vec::<BasicTxDetails>::new().into_boxed_slice()),
409 cmp,
410 result,
411 U256::from(operand_delta),
412 None,
413 0,
414 )
415 }
416
417 #[test]
418 fn merge_frontiers_dedupes_across_workers_keeping_smallest_delta() {
419 let merged = merge_frontiers(
420 8,
421 [frontier(1, false, 30), frontier(1, false, 10), frontier(1, false, 20)],
422 );
423
424 assert_eq!(merged.len(), 1);
425 assert_eq!(merged[0].operands.operand_delta, U256::from(10));
426 }
427
428 #[test]
429 fn merge_frontiers_keeps_records_with_distinct_result_keys() {
430 let merged = merge_frontiers(8, [frontier(1, false, 5), frontier(1, true, 5)]);
432
433 assert_eq!(merged.len(), 2);
434 }
435
436 #[test]
437 fn merge_frontiers_replaces_retained_key_after_limit_is_full() {
438 let merged = merge_frontiers(
441 2,
442 [
443 frontier(1, false, 30),
444 frontier(2, false, 30),
445 frontier(3, false, 5),
446 frontier(1, false, 10),
447 ],
448 );
449
450 assert_eq!(merged.len(), 2);
451 let retained = merged.iter().find(|f| f.site.pc == 1).unwrap();
452 assert_eq!(retained.operands.operand_delta, U256::from(10));
453 assert!(merged.iter().all(|f| f.site.pc != 3));
454 }
455
456 #[test]
457 fn merge_frontiers_does_not_spend_limit_on_duplicates() {
458 let merged = merge_frontiers(
461 2,
462 [frontier(1, false, 30), frontier(1, false, 10), frontier(2, false, 5)],
463 );
464
465 assert_eq!(merged.len(), 2);
466 assert_eq!(
467 merged.iter().find(|f| f.site.pc == 1).unwrap().operands.operand_delta,
468 U256::from(10)
469 );
470 assert!(merged.iter().any(|f| f.site.pc == 2));
471 }
472
473 #[test]
474 fn merge_frontiers_distinguishes_by_address() {
475 let other = Address::with_last_byte(1);
477 let merged = merge_frontiers(
478 8,
479 [frontier_at(Address::ZERO, 1, false, 5), frontier_at(other, 1, false, 5)],
480 );
481
482 assert_eq!(merged.len(), 2);
483 }
484
485 #[test]
486 fn merge_frontiers_with_zero_limit_is_empty() {
487 assert!(merge_frontiers(0, [frontier(1, false, 1)]).is_empty());
488 }
489}