1use alloy_primitives::{
2 Address, U256,
3 map::{DefaultHashBuilder, 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 pub fn edge_index(&mut self, edge: EdgeKey) -> usize {
120 if let Some(index) = self.edge_indices.get(&edge) {
121 *index
122 } else {
123 let index = self.next_index;
124 self.next_index += 1;
125 self.edge_indices.insert(edge, index);
126 index
127 }
128 }
129
130 pub const fn edge_count(&self) -> usize {
131 self.next_index
132 }
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub struct EdgeCovHit {
137 pub edge: EdgeKey,
138 pub count: u8,
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
142pub enum EdgeCoverage {
143 Hash(Vec<u8>),
144 CollisionFree(Vec<EdgeCovHit>),
145}
146
147impl EdgeCoverage {
148 pub fn is_empty(&self) -> bool {
149 match self {
150 Self::Hash(hitcount) => hitcount.iter().all(|&count| count == 0),
151 Self::CollisionFree(hits) => hits.is_empty(),
152 }
153 }
154}
155
156#[derive(Clone)]
162pub struct EdgeCovInspector {
163 hitcount: Vec<u8>,
165 config: EdgeCovConfig,
167 dense_hitcount: HashMap<EdgeKey, u8>,
169 hash_builder: DefaultHashBuilder,
170 cmp_log: Option<Vec<CmpOperands>>,
172 cmp_site_counts: HashMap<CmpSiteKey, u8>,
173}
174
175impl fmt::Debug for EdgeCovInspector {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 f.debug_struct("EdgeCovInspector")
178 .field("capacity", &self.hitcount.len())
179 .field("edges", &self.edge_count())
180 .field("config", &self.config)
181 .finish()
182 }
183}
184
185impl EdgeCovInspector {
186 pub fn new() -> Self {
188 Self::with_config(EdgeCovConfig::default())
189 }
190
191 pub fn with_cmp_log() -> Self {
193 let mut inspector = Self::new();
194 inspector.enable_cmp_log(true);
195 inspector
196 }
197
198 pub fn with_config(config: EdgeCovConfig) -> Self {
203 let hitcount = match config.kind {
204 EdgeCovKind::Hash => vec![0; MAX_EDGE_COUNT],
205 EdgeCovKind::CollisionFree => Vec::new(),
206 };
207 Self {
208 hitcount,
209 config,
210 dense_hitcount: HashMap::default(),
211 hash_builder: DefaultHashBuilder::default(),
212 cmp_log: None,
213 cmp_site_counts: HashMap::default(),
214 }
215 }
216
217 pub fn enable_cmp_log(&mut self, yes: bool) {
219 if yes {
220 self.cmp_log.get_or_insert_with(|| Vec::with_capacity(MAX_CMP_LOG_SITES));
221 } else {
222 self.cmp_log = None;
223 self.cmp_site_counts.clear();
224 }
225 }
226
227 pub fn reset(&mut self) {
229 match self.config.kind {
230 EdgeCovKind::CollisionFree => self.dense_hitcount.clear(),
231 EdgeCovKind::Hash => self.hitcount.fill(0),
232 }
233 if let Some(cmp_log) = &mut self.cmp_log {
234 cmp_log.clear();
235 }
236 self.cmp_site_counts.clear();
237 }
238
239 pub const fn get_cmp_log(&self) -> &[CmpOperands] {
241 match &self.cmp_log {
242 Some(cmp_log) => cmp_log.as_slice(),
243 None => &[],
244 }
245 }
246
247 pub fn into_parts(mut self) -> (EdgeCoverage, Vec<CmpOperands>) {
249 let cmp_log = self.cmp_log.take().unwrap_or_default();
250 (self.into(), cmp_log)
251 }
252
253 pub fn edge_count(&self) -> usize {
255 self.dense_hitcount.len()
256 }
257
258 fn store_hit(&mut self, address: Address, depth: usize, pc: usize, jump_dest: U256) {
260 let edge_id = match self.config.kind {
261 EdgeCovKind::CollisionFree => {
262 self.store_dense_hit(address, depth, pc, jump_dest);
263 return;
264 }
265 EdgeCovKind::Hash => self.hash_edge_id(address, depth, pc, jump_dest),
266 };
267 self.hitcount[edge_id] = self.hitcount[edge_id].wrapping_add(1).max(1);
268 }
269
270 fn store_dense_hit(&mut self, address: Address, depth: usize, pc: usize, jump_dest: U256) {
271 let key = EdgeKey::new(address, depth, pc, jump_dest, self.config.include_call_depth);
272 let count = self.dense_hitcount.entry(key).or_default();
273 *count = count.wrapping_add(1).max(1);
274 }
275
276 fn hash_edge_id(
277 &mut self,
278 address: Address,
279 depth: usize,
280 pc: usize,
281 jump_dest: U256,
282 ) -> usize {
283 let mut hasher = self.hash_builder.build_hasher();
284 address.hash(&mut hasher);
285 if self.config.include_call_depth {
286 depth.hash(&mut hasher);
287 }
288 pc.hash(&mut hasher);
289 jump_dest.hash(&mut hasher);
290 (hasher.finish() % self.hitcount.len() as u64) as usize
293 }
294
295 #[cfg(test)]
296 fn dense_hits(&self) -> Vec<EdgeCovHit> {
297 let mut hits = self
298 .dense_hitcount
299 .iter()
300 .map(|(&edge, &count)| EdgeCovHit { edge, count })
301 .collect::<Vec<_>>();
302 hits.sort_by_key(|hit| hit.edge);
303 hits
304 }
305
306 fn store_cmp(&mut self, cmp: CmpOperands) {
308 let Some(cmp_log) = &mut self.cmp_log else {
309 return;
310 };
311
312 let site = CmpSiteKey::new(&cmp);
313 if let Some(count) = self.cmp_site_counts.get_mut(&site) {
314 if *count >= MAX_CMP_OBSERVATIONS_PER_SITE {
315 return;
316 }
317 *count += 1;
318 cmp_log.push(cmp);
319 } else if self.cmp_site_counts.len() < MAX_CMP_LOG_SITES {
320 self.cmp_site_counts.insert(site, 1);
321 cmp_log.push(cmp);
322 }
323 }
324
325 #[cold]
326 fn do_step<CTX>(&mut self, interp: &mut Interpreter, context: &mut CTX)
327 where
328 CTX: ContextTr,
329 {
330 let address = interp.input.target_address();
331 let depth = context.journal_ref().depth();
332 let current_pc = interp.bytecode.pc();
333
334 match interp.bytecode.opcode() {
335 opcode::JUMP => {
336 if let Ok(jump_dest) = interp.stack.peek(0) {
338 self.store_hit(address, depth, current_pc, jump_dest);
339 }
340 }
341 opcode::JUMPI => {
342 if let Ok(stack_value) = interp.stack.peek(1) {
343 let jump_dest = if stack_value.is_zero() {
344 Ok(U256::from(current_pc + 1))
346 } else {
347 interp.stack.peek(0)
349 };
350
351 if let Ok(jump_dest) = jump_dest {
352 self.store_hit(address, depth, current_pc, jump_dest);
353 }
354 }
355 }
356 _ => {
357 }
359 }
360 }
361
362 #[cold]
363 fn do_cmp_step(&mut self, interp: &mut Interpreter) {
364 if self.cmp_log.is_none() {
365 return;
366 }
367
368 let address = interp.input.target_address();
369 let current_pc = interp.bytecode.pc();
370
371 match interp.bytecode.opcode() {
372 op @ (opcode::EQ | opcode::LT | opcode::SLT | opcode::GT | opcode::SGT) => {
373 if let (Ok(op1), Ok(op2)) = (interp.stack.peek(0), interp.stack.peek(1)) {
374 self.store_cmp(CmpOperands { op1, op2, pc: current_pc, address, opcode: op });
375 }
376 }
377 op @ opcode::ISZERO => {
378 if let Ok(op1) = interp.stack.peek(0) {
379 self.store_cmp(CmpOperands {
380 op1,
381 op2: U256::ZERO,
382 pc: current_pc,
383 address,
384 opcode: op,
385 });
386 }
387 }
388 _ => {}
389 }
390 }
391}
392
393impl Default for EdgeCovInspector {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399impl From<EdgeCovInspector> for EdgeCoverage {
400 fn from(inspector: EdgeCovInspector) -> Self {
401 let EdgeCovInspector { hitcount, config, dense_hitcount, .. } = inspector;
402 match config.kind {
403 EdgeCovKind::CollisionFree => Self::CollisionFree(
408 dense_hitcount
409 .into_iter()
410 .map(|(edge, count)| EdgeCovHit { edge, count })
411 .collect(),
412 ),
413 EdgeCovKind::Hash => Self::Hash(hitcount),
414 }
415 }
416}
417
418impl<CTX> Inspector<CTX> for EdgeCovInspector
419where
420 CTX: ContextTr,
421{
422 #[inline]
423 fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) {
424 let op = interp.bytecode.opcode();
425 if matches!(op, opcode::JUMP | opcode::JUMPI) {
426 self.do_step(interp, context);
427 }
428 if self.cmp_log.is_some()
429 && matches!(
430 op,
431 opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
432 )
433 {
434 self.do_cmp_step(interp);
435 }
436 }
437}
438
439#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
440struct CmpSiteKey {
441 address: Address,
442 pc: usize,
443 opcode: u8,
444}
445
446impl CmpSiteKey {
447 const fn new(cmp: &CmpOperands) -> Self {
448 Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode }
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn dense_counts(inspector: &EdgeCovInspector) -> Vec<u8> {
457 inspector.dense_hits().into_iter().map(|hit| hit.count).collect()
458 }
459
460 #[test]
461 fn cmp_operands_defaults_and_clones() {
462 let cmp = CmpOperands {
463 op1: U256::from(123),
464 op2: U256::from(456),
465 pc: 42,
466 address: Address::repeat_byte(0xaa),
467 opcode: opcode::EQ,
468 };
469
470 assert_eq!(cmp.op1, U256::from(123));
471 assert_eq!(cmp.op2, U256::from(456));
472 assert_eq!(cmp.pc, 42);
473
474 assert_eq!(CmpOperands::default(), CmpOperands::default());
475 let cloned = cmp;
476 assert_eq!(cloned, cmp);
477 }
478
479 #[test]
480 fn cmp_log_starts_empty_and_is_returned_by_into_parts() {
481 let inspector = EdgeCovInspector::new();
482
483 assert!(inspector.get_cmp_log().is_empty());
484
485 let (coverage, cmp_log) = inspector.into_parts();
486 assert_eq!(coverage, EdgeCoverage::CollisionFree(Vec::new()));
487 assert!(cmp_log.is_empty());
488 }
489
490 #[test]
491 fn collision_free_ids() {
492 let mut inspector = EdgeCovInspector::new();
493 let addr = Address::ZERO;
494
495 inspector.store_hit(addr, 0, 0, U256::from(10));
496 inspector.store_hit(addr, 0, 0, U256::from(20));
497 inspector.store_hit(addr, 0, 1, U256::from(10));
498
499 assert_eq!(inspector.edge_count(), 3);
500 assert_eq!(dense_counts(&inspector), [1, 1, 1]);
501 }
502
503 #[test]
504 fn same_edge_increments_same_dense_slot() {
505 let mut inspector = EdgeCovInspector::new();
506 let addr = Address::ZERO;
507
508 for _ in 0..5 {
509 inspector.store_hit(addr, 0, 42, U256::from(100));
510 }
511
512 assert_eq!(inspector.edge_count(), 1);
513 assert_eq!(dense_counts(&inspector), [5]);
514 }
515
516 #[test]
517 fn edge_index_map_keeps_indices_stable() {
518 let addr = Address::ZERO;
519 let mut indices = EdgeIndexMap::default();
520 let first = EdgeKey::new(addr, 0, 0, U256::from(10), false);
521 let second = EdgeKey::new(addr, 0, 0, U256::from(20), false);
522
523 assert_eq!(indices.edge_index(first), 0);
524 assert_eq!(indices.edge_index(second), 1);
525 assert_eq!(indices.edge_index(first), 0);
526 assert_eq!(indices.edge_count(), 2);
527 }
528
529 #[test]
530 fn hitcount_neverzero_on_wrap() {
531 let mut inspector = EdgeCovInspector::new();
532 let addr = Address::ZERO;
533
534 for _ in 0..256 {
535 inspector.store_hit(addr, 0, 0, U256::from(1));
536 }
537
538 assert_eq!(inspector.edge_count(), 1);
539 assert_eq!(dense_counts(&inspector), [1]);
540 }
541
542 #[test]
543 fn reset_clears_dense_hitcounts() {
544 let mut inspector = EdgeCovInspector::new();
545 let addr = Address::ZERO;
546
547 inspector.store_hit(addr, 0, 0, U256::from(1));
548 inspector.store_hit(addr, 0, 0, U256::from(2));
549 assert_eq!(inspector.edge_count(), 2);
550 assert_eq!(dense_counts(&inspector), [1, 1]);
551
552 inspector.reset();
553 assert_eq!(inspector.edge_count(), 0);
554 assert!(inspector.dense_hits().is_empty());
555
556 inspector.store_hit(addr, 0, 0, U256::from(1));
557 assert_eq!(inspector.edge_count(), 1);
558 assert_eq!(dense_counts(&inspector), [1]);
559 }
560
561 #[test]
562 fn legacy_hash_ids_match_old_calculation() {
563 let mut inspector = EdgeCovInspector::with_config(EdgeCovConfig::legacy_hash_ids());
564 let addr = Address::ZERO;
565 let pc = 42;
566 let jump_dest = U256::from(100);
567
568 let mut hasher = inspector.hash_builder.build_hasher();
569 addr.hash(&mut hasher);
570 pc.hash(&mut hasher);
571 jump_dest.hash(&mut hasher);
572 let expected_id = (hasher.finish() % MAX_EDGE_COUNT as u64) as usize;
573
574 inspector.store_hit(addr, 0, pc, jump_dest);
575
576 assert_eq!(inspector.hitcount[expected_id], 1);
577 assert_eq!(inspector.hitcount.iter().filter(|&&count| count != 0).count(), 1);
578 }
579
580 #[test]
581 fn call_depth_option_delineates_same_edge() {
582 let addr = Address::ZERO;
583
584 let mut without_depth = EdgeCovInspector::new();
585 without_depth.store_hit(addr, 0, 0, U256::from(1));
586 without_depth.store_hit(addr, 1, 0, U256::from(1));
587 assert_eq!(without_depth.edge_count(), 1);
588 assert_eq!(dense_counts(&without_depth), [2]);
589
590 let mut with_depth =
591 EdgeCovInspector::with_config(EdgeCovConfig::new(EdgeCovKind::CollisionFree, true));
592 with_depth.store_hit(addr, 0, 0, U256::from(1));
593 with_depth.store_hit(addr, 1, 0, U256::from(1));
594 assert_eq!(with_depth.edge_count(), 2);
595 assert_eq!(dense_counts(&with_depth), [1, 1]);
596 }
597
598 #[test]
599 fn reset_clears_hitcount_and_cmp_log() {
600 let mut inspector = EdgeCovInspector::with_cmp_log();
601
602 inspector.store_hit(Address::ZERO, 0, 0, U256::from(1));
603 inspector.store_cmp(CmpOperands {
604 op1: U256::from(123),
605 op2: U256::from(456),
606 pc: 42,
607 address: Address::ZERO,
608 opcode: opcode::EQ,
609 });
610
611 inspector.reset();
612
613 assert!(inspector.dense_hits().is_empty());
614 assert!(inspector.get_cmp_log().is_empty());
615 }
616
617 #[test]
618 fn cmp_log_is_capped_per_site() {
619 let mut inspector = EdgeCovInspector::with_cmp_log();
620
621 for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1 {
622 inspector.store_cmp(CmpOperands {
623 op1: U256::from(i),
624 op2: U256::from(i + 1),
625 pc: 42,
626 address: Address::ZERO,
627 opcode: opcode::EQ,
628 });
629 }
630
631 assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE));
632 }
633
634 #[test]
635 fn cmp_log_caps_sites_without_starving_later_observations() {
636 let mut inspector = EdgeCovInspector::with_cmp_log();
637
638 for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) * 2 {
639 inspector.store_cmp(CmpOperands {
640 op1: U256::from(i),
641 op2: U256::from(i + 1),
642 pc: 1,
643 address: Address::ZERO,
644 opcode: opcode::EQ,
645 });
646 }
647 inspector.store_cmp(CmpOperands {
648 op1: U256::from(123),
649 op2: U256::from(456),
650 pc: 2,
651 address: Address::ZERO,
652 opcode: opcode::EQ,
653 });
654
655 assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1);
656 assert_eq!(inspector.get_cmp_log().last().unwrap().pc, 2);
657 }
658}