1use alloy_primitives::{
2 Address, U256,
3 map::{DefaultHashBuilder, Entry, HashMap},
4};
5use core::{
6 fmt,
7 hash::{BuildHasher, Hash, Hasher},
8};
9use revm::{
10 Inspector,
11 bytecode::opcode,
12 context::{ContextTr, JournalTr},
13 interpreter::{
14 Interpreter,
15 interpreter_types::{InputsTr, Jumps},
16 },
17};
18
19pub(crate) const MAX_EDGE_COUNT: usize = 65536;
21
22const MAX_CMP_LOG_SITES: usize = 1024;
24
25const MAX_CMP_OBSERVATIONS_PER_SITE: u8 = 8;
28
29#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
31pub enum EdgeCovKind {
32 #[default]
34 CollisionFree,
35 Hash,
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct EdgeCovConfig {
42 pub kind: EdgeCovKind,
44 pub include_call_depth: bool,
46}
47
48impl EdgeCovConfig {
49 pub const fn new(kind: EdgeCovKind, include_call_depth: bool) -> Self {
51 Self { kind, include_call_depth }
52 }
53
54 pub const fn legacy_hash_ids() -> Self {
56 Self::new(EdgeCovKind::Hash, false)
57 }
58}
59
60impl Default for EdgeCovConfig {
61 fn default() -> Self {
62 Self::new(EdgeCovKind::CollisionFree, false)
63 }
64}
65
66impl From<&foundry_config::FuzzCorpusConfig> for EdgeCovConfig {
67 fn from(corpus: &foundry_config::FuzzCorpusConfig) -> Self {
68 let kind = if corpus.evm_edge_coverage_collision_free() {
69 EdgeCovKind::CollisionFree
70 } else {
71 EdgeCovKind::Hash
72 };
73 Self::new(kind, corpus.evm_edge_coverage_include_call_depth())
74 }
75}
76
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub struct CmpOperands {
80 pub op1: U256,
82 pub op2: U256,
84 pub pc: usize,
86 pub address: Address,
88 pub opcode: u8,
90}
91
92#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub struct EdgeKey {
94 pub address: Address,
95 pub depth: Option<usize>,
96 pub pc: usize,
97 pub jump_dest: U256,
98}
99
100impl EdgeKey {
101 fn new(
102 address: Address,
103 depth: usize,
104 pc: usize,
105 jump_dest: U256,
106 include_depth: bool,
107 ) -> Self {
108 Self { address, depth: include_depth.then_some(depth), pc, jump_dest }
109 }
110}
111
112#[derive(Clone, Debug, Default)]
113pub struct EdgeIndexMap {
114 edge_indices: HashMap<EdgeKey, usize>,
115 next_index: usize,
116}
117
118impl EdgeIndexMap {
119 #[inline]
120 pub fn edge_index(&mut self, edge: EdgeKey) -> usize {
121 match self.edge_indices.entry(edge) {
122 Entry::Occupied(entry) => *entry.get(),
123 Entry::Vacant(entry) => {
124 let index = self.next_index;
125 self.next_index += 1;
126 entry.insert(index);
127 index
128 }
129 }
130 }
131
132 pub const fn edge_count(&self) -> usize {
133 self.next_index
134 }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct EdgeCovHit {
139 pub edge: EdgeKey,
140 pub count: u8,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub enum EdgeCoverage {
145 Hash(Vec<u8>),
146 CollisionFree(Vec<EdgeCovHit>),
147}
148
149impl EdgeCoverage {
150 pub fn is_empty(&self) -> bool {
151 match self {
152 Self::Hash(hitcount) => hitcount.iter().all(|&count| count == 0),
153 Self::CollisionFree(hits) => hits.is_empty(),
154 }
155 }
156}
157
158#[derive(Clone)]
164pub struct EdgeCovInspector {
165 hitcount: Vec<u8>,
167 config: EdgeCovConfig,
169 collect_edges: bool,
172 dense_hitcount: HashMap<EdgeKey, u8>,
174 hash_builder: DefaultHashBuilder,
175 cmp_log: Option<Vec<CmpOperands>>,
177 cmp_site_counts: HashMap<CmpSiteKey, u8>,
178}
179
180impl fmt::Debug for EdgeCovInspector {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.debug_struct("EdgeCovInspector")
183 .field("capacity", &self.hitcount.len())
184 .field("edges", &self.edge_count())
185 .field("config", &self.config)
186 .field("collect_edges", &self.collect_edges)
187 .finish()
188 }
189}
190
191impl EdgeCovInspector {
192 pub fn new() -> Self {
194 Self::with_config(EdgeCovConfig::default())
195 }
196
197 pub fn with_cmp_log() -> Self {
199 let mut inspector = Self::new();
200 inspector.enable_cmp_log(true);
201 inspector
202 }
203
204 pub fn with_cmp_log_only() -> Self {
206 let mut inspector = Self::new();
207 inspector.collect_edges = false;
208 inspector.enable_cmp_log(true);
209 inspector
210 }
211
212 pub fn with_config(config: EdgeCovConfig) -> Self {
217 let hitcount = match config.kind {
218 EdgeCovKind::Hash => vec![0; MAX_EDGE_COUNT],
219 EdgeCovKind::CollisionFree => Vec::new(),
220 };
221 Self {
222 hitcount,
223 config,
224 collect_edges: true,
225 dense_hitcount: HashMap::default(),
226 hash_builder: DefaultHashBuilder::default(),
227 cmp_log: None,
228 cmp_site_counts: HashMap::default(),
229 }
230 }
231
232 pub fn enable_cmp_log(&mut self, yes: bool) {
234 if yes {
235 self.cmp_log.get_or_insert_with(|| Vec::with_capacity(MAX_CMP_LOG_SITES));
236 } else {
237 self.cmp_log = None;
238 self.cmp_site_counts.clear();
239 }
240 }
241
242 pub fn reset(&mut self) {
244 match self.config.kind {
245 EdgeCovKind::CollisionFree => self.dense_hitcount.clear(),
246 EdgeCovKind::Hash => self.hitcount.fill(0),
247 }
248 if let Some(cmp_log) = &mut self.cmp_log {
249 cmp_log.clear();
250 }
251 self.cmp_site_counts.clear();
252 }
253
254 pub const fn get_cmp_log(&self) -> &[CmpOperands] {
256 match &self.cmp_log {
257 Some(cmp_log) => cmp_log.as_slice(),
258 None => &[],
259 }
260 }
261
262 pub fn into_parts(mut self) -> (EdgeCoverage, Vec<CmpOperands>) {
264 let cmp_log = self.cmp_log.take().unwrap_or_default();
265 (self.into(), cmp_log)
266 }
267
268 pub fn edge_count(&self) -> usize {
270 self.dense_hitcount.len()
271 }
272
273 fn store_hit(&mut self, address: Address, depth: usize, pc: usize, jump_dest: U256) {
275 if !self.collect_edges {
276 return;
277 }
278
279 let edge_id = match self.config.kind {
280 EdgeCovKind::CollisionFree => {
281 self.store_dense_hit(address, depth, pc, jump_dest);
282 return;
283 }
284 EdgeCovKind::Hash => self.hash_edge_id(address, depth, pc, jump_dest),
285 };
286 self.hitcount[edge_id] = self.hitcount[edge_id].wrapping_add(1).max(1);
287 }
288
289 fn store_dense_hit(&mut self, address: Address, depth: usize, pc: usize, jump_dest: U256) {
290 let key = EdgeKey::new(address, depth, pc, jump_dest, self.config.include_call_depth);
291 let count = self.dense_hitcount.entry(key).or_default();
292 *count = count.wrapping_add(1).max(1);
293 }
294
295 fn hash_edge_id(
296 &mut self,
297 address: Address,
298 depth: usize,
299 pc: usize,
300 jump_dest: U256,
301 ) -> usize {
302 let mut hasher = self.hash_builder.build_hasher();
303 address.hash(&mut hasher);
304 if self.config.include_call_depth {
305 depth.hash(&mut hasher);
306 }
307 pc.hash(&mut hasher);
308 jump_dest.hash(&mut hasher);
309 (hasher.finish() % self.hitcount.len() as u64) as usize
312 }
313
314 #[cfg(test)]
315 fn dense_hits(&self) -> Vec<EdgeCovHit> {
316 let mut hits = self
317 .dense_hitcount
318 .iter()
319 .map(|(&edge, &count)| EdgeCovHit { edge, count })
320 .collect::<Vec<_>>();
321 hits.sort_by_key(|hit| hit.edge);
322 hits
323 }
324
325 fn store_cmp(&mut self, cmp: CmpOperands) {
327 let Some(cmp_log) = &mut self.cmp_log else {
328 return;
329 };
330
331 let site = CmpSiteKey::new(&cmp);
332 if let Some(count) = self.cmp_site_counts.get_mut(&site) {
333 if *count >= MAX_CMP_OBSERVATIONS_PER_SITE {
334 return;
335 }
336 *count += 1;
337 cmp_log.push(cmp);
338 } else if self.cmp_site_counts.len() < MAX_CMP_LOG_SITES {
339 self.cmp_site_counts.insert(site, 1);
340 cmp_log.push(cmp);
341 }
342 }
343
344 #[cold]
345 fn do_step<CTX>(&mut self, interp: &mut Interpreter, context: &mut CTX)
346 where
347 CTX: ContextTr,
348 {
349 let address = interp.input.target_address();
350 let depth = context.journal_ref().depth();
351 let current_pc = interp.bytecode.pc();
352
353 match interp.bytecode.opcode() {
354 opcode::JUMP => {
355 if let Ok(jump_dest) = interp.stack.peek(0) {
357 self.store_hit(address, depth, current_pc, jump_dest);
358 }
359 }
360 opcode::JUMPI => {
361 if let Ok(stack_value) = interp.stack.peek(1) {
362 let jump_dest = if stack_value.is_zero() {
363 Ok(U256::from(current_pc + 1))
365 } else {
366 interp.stack.peek(0)
368 };
369
370 if let Ok(jump_dest) = jump_dest {
371 self.store_hit(address, depth, current_pc, jump_dest);
372 }
373 }
374 }
375 _ => {
376 }
378 }
379 }
380
381 #[cold]
382 fn do_cmp_step(&mut self, interp: &mut Interpreter) {
383 if self.cmp_log.is_none() {
384 return;
385 }
386
387 let address = interp.input.target_address();
388 let current_pc = interp.bytecode.pc();
389
390 match interp.bytecode.opcode() {
391 op @ (opcode::EQ | opcode::LT | opcode::SLT | opcode::GT | opcode::SGT) => {
392 if let (Ok(op1), Ok(op2)) = (interp.stack.peek(0), interp.stack.peek(1)) {
393 self.store_cmp(CmpOperands { op1, op2, pc: current_pc, address, opcode: op });
394 }
395 }
396 op @ opcode::ISZERO => {
397 if let Ok(op1) = interp.stack.peek(0) {
398 self.store_cmp(CmpOperands {
399 op1,
400 op2: U256::ZERO,
401 pc: current_pc,
402 address,
403 opcode: op,
404 });
405 }
406 }
407 _ => {}
408 }
409 }
410}
411
412impl Default for EdgeCovInspector {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418impl From<EdgeCovInspector> for EdgeCoverage {
419 fn from(inspector: EdgeCovInspector) -> Self {
420 let EdgeCovInspector { hitcount, config, dense_hitcount, .. } = inspector;
421 match config.kind {
422 EdgeCovKind::CollisionFree => Self::CollisionFree(
427 dense_hitcount
428 .into_iter()
429 .map(|(edge, count)| EdgeCovHit { edge, count })
430 .collect(),
431 ),
432 EdgeCovKind::Hash => Self::Hash(hitcount),
433 }
434 }
435}
436
437impl<CTX> Inspector<CTX> for EdgeCovInspector
438where
439 CTX: ContextTr,
440{
441 #[inline]
442 fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) {
443 let op = interp.bytecode.opcode();
444 if self.collect_edges && matches!(op, opcode::JUMP | opcode::JUMPI) {
445 self.do_step(interp, context);
446 }
447 if self.cmp_log.is_some()
448 && matches!(
449 op,
450 opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
451 )
452 {
453 self.do_cmp_step(interp);
454 }
455 }
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
459struct CmpSiteKey {
460 address: Address,
461 pc: usize,
462 opcode: u8,
463}
464
465impl CmpSiteKey {
466 const fn new(cmp: &CmpOperands) -> Self {
467 Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 fn dense_counts(inspector: &EdgeCovInspector) -> Vec<u8> {
476 inspector.dense_hits().into_iter().map(|hit| hit.count).collect()
477 }
478
479 #[test]
480 fn cmp_operands_defaults_and_clones() {
481 let cmp = CmpOperands {
482 op1: U256::from(123),
483 op2: U256::from(456),
484 pc: 42,
485 address: Address::repeat_byte(0xaa),
486 opcode: opcode::EQ,
487 };
488
489 assert_eq!(cmp.op1, U256::from(123));
490 assert_eq!(cmp.op2, U256::from(456));
491 assert_eq!(cmp.pc, 42);
492
493 assert_eq!(CmpOperands::default(), CmpOperands::default());
494 let cloned = cmp;
495 assert_eq!(cloned, cmp);
496 }
497
498 #[test]
499 fn cmp_log_starts_empty_and_is_returned_by_into_parts() {
500 let inspector = EdgeCovInspector::new();
501
502 assert!(inspector.get_cmp_log().is_empty());
503
504 let (coverage, cmp_log) = inspector.into_parts();
505 assert_eq!(coverage, EdgeCoverage::CollisionFree(Vec::new()));
506 assert!(cmp_log.is_empty());
507 }
508
509 #[test]
510 fn cmp_log_only_does_not_collect_edges() {
511 let mut inspector = EdgeCovInspector::with_cmp_log_only();
512 let addr = Address::ZERO;
513
514 inspector.store_hit(addr, 0, 0, U256::from(1));
515 inspector.store_cmp(CmpOperands {
516 op1: U256::from(123),
517 op2: U256::from(456),
518 pc: 42,
519 address: addr,
520 opcode: opcode::EQ,
521 });
522
523 let (coverage, cmp_log) = inspector.into_parts();
524 assert_eq!(coverage, EdgeCoverage::CollisionFree(Vec::new()));
525 assert_eq!(cmp_log.len(), 1);
526 }
527
528 #[test]
529 fn collision_free_ids() {
530 let mut inspector = EdgeCovInspector::new();
531 let addr = Address::ZERO;
532
533 inspector.store_hit(addr, 0, 0, U256::from(10));
534 inspector.store_hit(addr, 0, 0, U256::from(20));
535 inspector.store_hit(addr, 0, 1, U256::from(10));
536
537 assert_eq!(inspector.edge_count(), 3);
538 assert_eq!(dense_counts(&inspector), [1, 1, 1]);
539 }
540
541 #[test]
542 fn same_edge_increments_same_dense_slot() {
543 let mut inspector = EdgeCovInspector::new();
544 let addr = Address::ZERO;
545
546 for _ in 0..5 {
547 inspector.store_hit(addr, 0, 42, U256::from(100));
548 }
549
550 assert_eq!(inspector.edge_count(), 1);
551 assert_eq!(dense_counts(&inspector), [5]);
552 }
553
554 #[test]
555 fn edge_index_map_keeps_indices_stable() {
556 let addr = Address::ZERO;
557 let mut indices = EdgeIndexMap::default();
558 let first = EdgeKey::new(addr, 0, 0, U256::from(10), false);
559 let second = EdgeKey::new(addr, 0, 0, U256::from(20), false);
560
561 assert_eq!(indices.edge_index(first), 0);
562 assert_eq!(indices.edge_index(second), 1);
563 assert_eq!(indices.edge_index(first), 0);
564 assert_eq!(indices.edge_count(), 2);
565 }
566
567 #[test]
568 fn hitcount_neverzero_on_wrap() {
569 let mut inspector = EdgeCovInspector::new();
570 let addr = Address::ZERO;
571
572 for _ in 0..256 {
573 inspector.store_hit(addr, 0, 0, U256::from(1));
574 }
575
576 assert_eq!(inspector.edge_count(), 1);
577 assert_eq!(dense_counts(&inspector), [1]);
578 }
579
580 #[test]
581 fn reset_clears_dense_hitcounts() {
582 let mut inspector = EdgeCovInspector::new();
583 let addr = Address::ZERO;
584
585 inspector.store_hit(addr, 0, 0, U256::from(1));
586 inspector.store_hit(addr, 0, 0, U256::from(2));
587 assert_eq!(inspector.edge_count(), 2);
588 assert_eq!(dense_counts(&inspector), [1, 1]);
589
590 inspector.reset();
591 assert_eq!(inspector.edge_count(), 0);
592 assert!(inspector.dense_hits().is_empty());
593
594 inspector.store_hit(addr, 0, 0, U256::from(1));
595 assert_eq!(inspector.edge_count(), 1);
596 assert_eq!(dense_counts(&inspector), [1]);
597 }
598
599 #[test]
600 fn legacy_hash_ids_match_old_calculation() {
601 let mut inspector = EdgeCovInspector::with_config(EdgeCovConfig::legacy_hash_ids());
602 let addr = Address::ZERO;
603 let pc = 42;
604 let jump_dest = U256::from(100);
605
606 let mut hasher = inspector.hash_builder.build_hasher();
607 addr.hash(&mut hasher);
608 pc.hash(&mut hasher);
609 jump_dest.hash(&mut hasher);
610 let expected_id = (hasher.finish() % MAX_EDGE_COUNT as u64) as usize;
611
612 inspector.store_hit(addr, 0, pc, jump_dest);
613
614 assert_eq!(inspector.hitcount[expected_id], 1);
615 assert_eq!(inspector.hitcount.iter().filter(|&&count| count != 0).count(), 1);
616 }
617
618 #[test]
619 fn call_depth_option_delineates_same_edge() {
620 let addr = Address::ZERO;
621
622 let mut without_depth = EdgeCovInspector::new();
623 without_depth.store_hit(addr, 0, 0, U256::from(1));
624 without_depth.store_hit(addr, 1, 0, U256::from(1));
625 assert_eq!(without_depth.edge_count(), 1);
626 assert_eq!(dense_counts(&without_depth), [2]);
627
628 let mut with_depth =
629 EdgeCovInspector::with_config(EdgeCovConfig::new(EdgeCovKind::CollisionFree, true));
630 with_depth.store_hit(addr, 0, 0, U256::from(1));
631 with_depth.store_hit(addr, 1, 0, U256::from(1));
632 assert_eq!(with_depth.edge_count(), 2);
633 assert_eq!(dense_counts(&with_depth), [1, 1]);
634 }
635
636 #[test]
637 fn reset_clears_hitcount_and_cmp_log() {
638 let mut inspector = EdgeCovInspector::with_cmp_log();
639
640 inspector.store_hit(Address::ZERO, 0, 0, U256::from(1));
641 inspector.store_cmp(CmpOperands {
642 op1: U256::from(123),
643 op2: U256::from(456),
644 pc: 42,
645 address: Address::ZERO,
646 opcode: opcode::EQ,
647 });
648
649 inspector.reset();
650
651 assert!(inspector.dense_hits().is_empty());
652 assert!(inspector.get_cmp_log().is_empty());
653 }
654
655 #[test]
656 fn cmp_log_is_capped_per_site() {
657 let mut inspector = EdgeCovInspector::with_cmp_log();
658
659 for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1 {
660 inspector.store_cmp(CmpOperands {
661 op1: U256::from(i),
662 op2: U256::from(i + 1),
663 pc: 42,
664 address: Address::ZERO,
665 opcode: opcode::EQ,
666 });
667 }
668
669 assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE));
670 }
671
672 #[test]
673 fn cmp_log_caps_sites_without_starving_later_observations() {
674 let mut inspector = EdgeCovInspector::with_cmp_log();
675
676 for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) * 2 {
677 inspector.store_cmp(CmpOperands {
678 op1: U256::from(i),
679 op2: U256::from(i + 1),
680 pc: 1,
681 address: Address::ZERO,
682 opcode: opcode::EQ,
683 });
684 }
685 inspector.store_cmp(CmpOperands {
686 op1: U256::from(123),
687 op2: U256::from(456),
688 pc: 2,
689 address: Address::ZERO,
690 opcode: opcode::EQ,
691 });
692
693 assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1);
694 assert_eq!(inspector.get_cmp_log().last().unwrap().pc, 2);
695 }
696}