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 interpreter::{
13 Interpreter,
14 interpreter_types::{InputsTr, Jumps},
15 },
16};
17
18const 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, PartialEq, Eq)]
31pub struct CmpOperands {
32 pub op1: U256,
34 pub op2: U256,
36 pub pc: usize,
38 pub address: Address,
40 pub opcode: u8,
42}
43
44#[derive(Clone)]
50pub struct EdgeCovInspector {
51 hitcount: Vec<u8>,
53 hash_builder: DefaultHashBuilder,
54 cmp_log: Option<Vec<CmpOperands>>,
56 cmp_site_counts: HashMap<CmpSiteKey, u8>,
57}
58
59impl fmt::Debug for EdgeCovInspector {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.debug_struct("EdgeCovInspector").finish_non_exhaustive()
62 }
63}
64
65impl EdgeCovInspector {
66 pub fn new() -> Self {
68 Self {
69 hitcount: vec![0; MAX_EDGE_COUNT],
70 hash_builder: DefaultHashBuilder::default(),
71 cmp_log: None,
72 cmp_site_counts: HashMap::default(),
73 }
74 }
75
76 pub fn with_cmp_log() -> Self {
78 let mut inspector = Self::new();
79 inspector.enable_cmp_log(true);
80 inspector
81 }
82
83 pub fn enable_cmp_log(&mut self, yes: bool) {
85 if yes {
86 self.cmp_log.get_or_insert_with(|| Vec::with_capacity(MAX_CMP_LOG_SITES));
87 } else {
88 self.cmp_log = None;
89 self.cmp_site_counts.clear();
90 }
91 }
92
93 pub fn reset(&mut self) {
95 self.hitcount.fill(0);
96 if let Some(cmp_log) = &mut self.cmp_log {
97 cmp_log.clear();
98 }
99 self.cmp_site_counts.clear();
100 }
101
102 pub const fn get_hitcount(&self) -> &[u8] {
104 self.hitcount.as_slice()
105 }
106
107 pub const fn get_cmp_log(&self) -> &[CmpOperands] {
109 match &self.cmp_log {
110 Some(cmp_log) => cmp_log.as_slice(),
111 None => &[],
112 }
113 }
114
115 pub fn into_hitcount(self) -> Vec<u8> {
117 self.hitcount
118 }
119
120 pub fn into_parts(self) -> (Vec<u8>, Vec<CmpOperands>) {
122 (self.hitcount, self.cmp_log.unwrap_or_default())
123 }
124
125 fn store_hit(&mut self, address: Address, pc: usize, jump_dest: U256) {
127 let mut hasher = self.hash_builder.build_hasher();
128 address.hash(&mut hasher);
129 pc.hash(&mut hasher);
130 jump_dest.hash(&mut hasher);
131 let edge_id = (hasher.finish() % MAX_EDGE_COUNT as u64) as usize;
134 self.hitcount[edge_id] = self.hitcount[edge_id].checked_add(1).unwrap_or(1);
135 }
136
137 fn store_cmp(&mut self, cmp: CmpOperands) {
139 let Some(cmp_log) = &mut self.cmp_log else {
140 return;
141 };
142
143 let site = CmpSiteKey::new(&cmp);
144 if let Some(count) = self.cmp_site_counts.get_mut(&site) {
145 if *count >= MAX_CMP_OBSERVATIONS_PER_SITE {
146 return;
147 }
148 *count += 1;
149 cmp_log.push(cmp);
150 } else if self.cmp_site_counts.len() < MAX_CMP_LOG_SITES {
151 self.cmp_site_counts.insert(site, 1);
152 cmp_log.push(cmp);
153 }
154 }
155
156 #[cold]
157 fn do_step(&mut self, interp: &mut Interpreter) {
158 let address = interp.input.target_address();
159 let current_pc = interp.bytecode.pc();
160
161 match interp.bytecode.opcode() {
162 opcode::JUMP => {
163 if let Ok(jump_dest) = interp.stack.peek(0) {
165 self.store_hit(address, current_pc, jump_dest);
166 }
167 }
168 opcode::JUMPI => {
169 if let Ok(stack_value) = interp.stack.peek(1) {
170 let jump_dest = if stack_value.is_zero() {
171 Ok(U256::from(current_pc + 1))
173 } else {
174 interp.stack.peek(0)
176 };
177
178 if let Ok(jump_dest) = jump_dest {
179 self.store_hit(address, current_pc, jump_dest);
180 }
181 }
182 }
183 _ => {
184 }
186 }
187 }
188
189 #[cold]
190 fn do_cmp_step(&mut self, interp: &mut Interpreter) {
191 if self.cmp_log.is_none() {
192 return;
193 }
194
195 let address = interp.input.target_address();
196 let current_pc = interp.bytecode.pc();
197
198 match interp.bytecode.opcode() {
199 op @ (opcode::EQ | opcode::LT | opcode::SLT | opcode::GT | opcode::SGT) => {
200 if let (Ok(op1), Ok(op2)) = (interp.stack.peek(0), interp.stack.peek(1)) {
201 self.store_cmp(CmpOperands { op1, op2, pc: current_pc, address, opcode: op });
202 }
203 }
204 op @ opcode::ISZERO => {
205 if let Ok(op1) = interp.stack.peek(0) {
206 self.store_cmp(CmpOperands {
207 op1,
208 op2: U256::ZERO,
209 pc: current_pc,
210 address,
211 opcode: op,
212 });
213 }
214 }
215 _ => {}
216 }
217 }
218}
219
220impl Default for EdgeCovInspector {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226impl<CTX> Inspector<CTX> for EdgeCovInspector {
227 #[inline]
228 fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
229 let op = interp.bytecode.opcode();
230 if matches!(op, opcode::JUMP | opcode::JUMPI) {
231 self.do_step(interp);
232 }
233 if self.cmp_log.is_some()
234 && matches!(
235 op,
236 opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
237 )
238 {
239 self.do_cmp_step(interp);
240 }
241 }
242}
243
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
245struct CmpSiteKey {
246 address: Address,
247 pc: usize,
248 opcode: u8,
249}
250
251impl CmpSiteKey {
252 const fn new(cmp: &CmpOperands) -> Self {
253 Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn cmp_operands_defaults_and_clones() {
263 let cmp = CmpOperands {
264 op1: U256::from(123),
265 op2: U256::from(456),
266 pc: 42,
267 address: Address::repeat_byte(0xaa),
268 opcode: opcode::EQ,
269 };
270
271 assert_eq!(cmp.op1, U256::from(123));
272 assert_eq!(cmp.op2, U256::from(456));
273 assert_eq!(cmp.pc, 42);
274
275 assert_eq!(CmpOperands::default(), CmpOperands::default());
276 let cloned = cmp;
277 assert_eq!(cloned, cmp);
278 }
279
280 #[test]
281 fn cmp_log_starts_empty_and_is_returned_by_into_parts() {
282 let inspector = EdgeCovInspector::new();
283
284 assert!(inspector.get_cmp_log().is_empty());
285 assert!(inspector.get_hitcount().iter().all(|&x| x == 0));
286
287 let (hitcount, cmp_log) = inspector.into_parts();
288 assert_eq!(hitcount.len(), MAX_EDGE_COUNT);
289 assert!(cmp_log.is_empty());
290 }
291
292 #[test]
293 fn reset_clears_hitcount_and_cmp_log() {
294 let mut inspector = EdgeCovInspector::with_cmp_log();
295
296 inspector.hitcount[0] = 1;
297 inspector.store_cmp(CmpOperands {
298 op1: U256::from(123),
299 op2: U256::from(456),
300 pc: 42,
301 address: Address::ZERO,
302 opcode: opcode::EQ,
303 });
304
305 inspector.reset();
306
307 assert!(inspector.get_hitcount().iter().all(|&x| x == 0));
308 assert!(inspector.get_cmp_log().is_empty());
309 }
310
311 #[test]
312 fn cmp_log_is_capped_per_site() {
313 let mut inspector = EdgeCovInspector::with_cmp_log();
314
315 for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1 {
316 inspector.store_cmp(CmpOperands {
317 op1: U256::from(i),
318 op2: U256::from(i + 1),
319 pc: 42,
320 address: Address::ZERO,
321 opcode: opcode::EQ,
322 });
323 }
324
325 assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE));
326 }
327
328 #[test]
329 fn cmp_log_caps_sites_without_starving_later_observations() {
330 let mut inspector = EdgeCovInspector::with_cmp_log();
331
332 for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) * 2 {
333 inspector.store_cmp(CmpOperands {
334 op1: U256::from(i),
335 op2: U256::from(i + 1),
336 pc: 1,
337 address: Address::ZERO,
338 opcode: opcode::EQ,
339 });
340 }
341 inspector.store_cmp(CmpOperands {
342 op1: U256::from(123),
343 op2: U256::from(456),
344 pc: 2,
345 address: Address::ZERO,
346 opcode: opcode::EQ,
347 });
348
349 assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1);
350 assert_eq!(inspector.get_cmp_log().last().unwrap().pc, 2);
351 }
352}