1use alloy_primitives::hex::ToHexExt;
2use revm_inspectors::tracing::{
3 CallTraceArena,
4 types::{CallTraceNode, CallTraceStep, DecodedTraceStep, TraceMemberOrder},
5};
6
7#[derive(Default)]
9pub struct EvmFoldedStackTraceBuilder {
10 fst: FoldedStackTraceBuilder,
12}
13
14impl EvmFoldedStackTraceBuilder {
15 pub fn build(self) -> Vec<String> {
17 self.fst.build()
18 }
19
20 pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) {
23 let node = &nodes[idx];
24
25 let func_name = if node.trace.kind.is_any_create() {
26 let contract_name = node
27 .trace
28 .decoded
29 .as_ref()
30 .and_then(|dc| dc.label.as_deref())
31 .unwrap_or("Contract");
32 format!("new {contract_name}")
33 } else {
34 let selector = node
35 .selector()
36 .map(|selector| selector.encode_hex_with_prefix())
37 .unwrap_or_else(|| "fallback".to_string());
38 let signature = node
39 .trace
40 .decoded
41 .as_ref()
42 .and_then(|dc| dc.call_data.as_ref())
43 .map(|dc| &dc.signature)
44 .unwrap_or(&selector);
45
46 if let Some(label) = node.trace.decoded.as_ref().and_then(|dc| dc.label.as_ref()) {
47 format!("{label}.{signature}")
48 } else {
49 signature.clone()
50 }
51 };
52
53 self.fst.enter(func_name, node.trace.gas_used);
55
56 let mut step_exits = vec![];
58
59 for order in &node.ordering {
61 match order {
62 TraceMemberOrder::Call(child_idx) => {
63 let child_node_idx = node.children[*child_idx];
64 self.process_call_node(nodes, child_node_idx);
65 }
66 TraceMemberOrder::Step(step_idx) => {
67 self.exit_previous_steps(&mut step_exits, *step_idx);
68 self.process_step(&node.trace.steps, *step_idx, &mut step_exits)
69 }
70 TraceMemberOrder::Log(_) => {}
71 }
72 }
73
74 for _ in 0..step_exits.len() {
76 self.fst.exit();
77 }
78
79 self.fst.exit();
81 }
82
83 fn process_step(
87 &mut self,
88 steps: &[CallTraceStep],
89 step_idx: usize,
90 step_exits: &mut Vec<usize>,
91 ) {
92 let step = &steps[step_idx];
93 if let Some(decoded_step) = &step.decoded {
94 match decoded_step.as_ref() {
95 DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => {
96 let gas_used =
98 step.gas_remaining.saturating_sub(steps[*step_end_idx].gas_remaining);
99 self.fst.enter(decoded_internal_call.func_name.clone(), gas_used);
100 step_exits.push(*step_end_idx);
101 }
102 DecodedTraceStep::Line(_) => {}
103 }
104 }
105 }
106
107 fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) {
109 let initial_length = step_exits.len();
110 step_exits.retain(|&number| number > step_idx);
111
112 let num_exits = initial_length - step_exits.len();
113 for _ in 0..num_exits {
114 self.fst.exit();
115 }
116 }
117}
118
119#[derive(Debug, Default)]
146pub struct FoldedStackTraceBuilder {
147 traces: Vec<TraceEntry>,
149 exits: usize,
151}
152
153#[derive(Debug, Default)]
155struct TraceEntry {
156 names: Vec<String>,
158 gas: u64,
160}
161
162impl FoldedStackTraceBuilder {
163 pub fn enter(&mut self, label: String, gas: u64) {
165 let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default();
166
167 while self.exits > 0 {
168 names.pop();
169 self.exits -= 1;
170 }
171
172 names.push(label);
173 self.traces.push(TraceEntry { names, gas });
174 }
175
176 pub const fn exit(&mut self) {
178 self.exits += 1;
179 }
180
181 pub fn build(mut self) -> Vec<String> {
183 self.subtract_children();
184 self.traces.iter().map(|e| format!("{} {}", e.names.join(";"), e.gas)).collect()
185 }
186
187 pub fn build_without_subtraction(&self) -> Vec<String> {
190 self.traces.iter().map(|e| format!("{} {}", e.names.join(";"), e.gas)).collect()
191 }
192
193 fn subtract_children(&mut self) {
195 for i in 0..self.traces.len() {
197 let (left, right) = self.traces.split_at_mut(i);
198 let TraceEntry { names, gas } = &right[0];
199 if names.len() > 1 {
200 let parent_trace_to_match = &names[..names.len() - 1];
201 for parent in left.iter_mut().rev() {
202 if parent.names == parent_trace_to_match {
203 parent.gas = parent.gas.saturating_sub(*gas);
204 break;
205 }
206 }
207 }
208 }
209 }
210}
211
212pub fn build(arena: &CallTraceArena) -> Vec<String> {
214 let mut fst = EvmFoldedStackTraceBuilder::default();
215 if !arena.nodes().is_empty() {
216 fst.process_call_node(arena.nodes(), 0);
217 }
218 fst.build()
219}
220
221#[cfg(test)]
222mod tests {
223 use alloy_primitives::{Bytes, U256};
224 use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
225 use revm_inspectors::tracing::{
226 CallTraceArena,
227 types::{
228 CallTrace, CallTraceNode, CallTraceStep, DecodedInternalCall, DecodedTraceStep,
229 TraceMemberOrder,
230 },
231 };
232
233 fn trace_step(gas_remaining: u64) -> CallTraceStep {
234 CallTraceStep {
235 pc: 0,
236 op: OpCode::STOP,
237 stack: Some(Vec::<U256>::new().into_boxed_slice()),
238 push_stack: None,
239 memory: None,
240 returndata: Bytes::new(),
241 gas_remaining,
242 gas_refund_counter: 0,
243 gas_used: 0,
244 gas_cost: 0,
245 state_gas_cost: None,
246 state_gas_reservoir: None,
247 state_gas_spent: 0,
248 storage_change: None,
249 status: Some(InstructionResult::Stop),
250 immediate_bytes: None,
251 decoded: None,
252 }
253 }
254
255 #[test]
256 fn test_fst_1() {
257 let mut trace = super::FoldedStackTraceBuilder::default();
258 trace.enter("top".to_string(), 500);
259 trace.enter("child_a".to_string(), 100);
260 trace.exit();
261 trace.enter("child_b".to_string(), 200);
262
263 assert_eq!(
264 trace.build_without_subtraction(),
265 vec![
266 "top 500", "top;child_a 100",
268 "top;child_b 200",
269 ]
270 );
271 assert_eq!(
272 trace.build(),
273 vec![
274 "top 200", "top;child_a 100",
276 "top;child_b 200",
277 ]
278 );
279 }
280
281 #[test]
282 fn test_fst_2() {
283 let mut trace = super::FoldedStackTraceBuilder::default();
284 trace.enter("top".to_string(), 500);
285 trace.enter("child_a".to_string(), 300);
286 trace.enter("child_b".to_string(), 100);
287 trace.exit();
288 trace.exit();
289 trace.enter("child_c".to_string(), 100);
290
291 assert_eq!(
292 trace.build_without_subtraction(),
293 vec![
294 "top 500", "top;child_a 300",
296 "top;child_a;child_b 100",
297 "top;child_c 100",
298 ]
299 );
300
301 assert_eq!(
302 trace.build(),
303 vec![
304 "top 100", "top;child_a 200", "top;child_a;child_b 100",
307 "top;child_c 100",
308 ]
309 );
310 }
311
312 #[test]
313 fn test_fst_3() {
314 let mut trace = super::FoldedStackTraceBuilder::default();
315 trace.enter("top".to_string(), 1700);
316 trace.enter("child_a".to_string(), 500);
317 trace.exit();
318 trace.enter("child_b".to_string(), 500);
319 trace.enter("child_c".to_string(), 500);
320 trace.exit();
321 trace.exit();
322 trace.exit();
323 trace.enter("top2".to_string(), 1700);
324
325 assert_eq!(
326 trace.build_without_subtraction(),
327 vec![
328 "top 1700", "top;child_a 500",
330 "top;child_b 500",
331 "top;child_b;child_c 500",
332 "top2 1700",
333 ]
334 );
335
336 assert_eq!(
337 trace.build(),
338 vec![
339 "top 700", "top;child_a 500",
341 "top;child_b 0",
342 "top;child_b;child_c 500",
343 "top2 1700",
344 ]
345 );
346 }
347
348 #[test]
349 fn folded_stack_trace_saturates_parent_gas() {
350 let mut trace = super::FoldedStackTraceBuilder::default();
351 trace.enter("top".to_string(), 100);
352 trace.enter("child".to_string(), 150);
353
354 assert_eq!(trace.build(), vec!["top 0", "top;child 150"]);
355 }
356
357 #[test]
358 fn folded_stack_trace_keeps_precise_internal_function_names() {
359 let mut arena = CallTraceArena::default();
360 let root = &mut arena.nodes_mut()[0];
361 root.trace.gas_used = 100;
362 root.trace.gas_limit = 100;
363 root.trace.steps = vec![trace_step(100), trace_step(70)];
364 root.trace.steps[0].decoded = Some(Box::new(DecodedTraceStep::InternalCall(
365 DecodedInternalCall {
366 func_name: "DebugVarsTest::foo(uint256)".to_string(),
367 args: Some(vec!["42".to_string()]),
368 return_data: Some(vec!["43".to_string()]),
369 },
370 1,
371 )));
372 root.ordering = vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)];
373
374 assert_eq!(
375 super::build(&arena),
376 vec!["fallback 70", "fallback;DebugVarsTest::foo(uint256) 30",]
377 );
378 }
379
380 #[test]
381 fn folded_stack_trace_saturates_internal_call_gas_on_reset_metering() {
382 let mut arena = CallTraceArena::default();
384 let root = &mut arena.nodes_mut()[0];
385 root.trace.gas_used = 100;
386 root.trace.gas_limit = 100;
387 root.trace.steps = vec![trace_step(70), trace_step(100)];
388 root.trace.steps[0].decoded = Some(Box::new(DecodedTraceStep::InternalCall(
389 DecodedInternalCall {
390 func_name: "DebugVarsTest::resetsGas()".to_string(),
391 args: None,
392 return_data: None,
393 },
394 1,
395 )));
396 root.ordering = vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)];
397
398 assert_eq!(
399 super::build(&arena),
400 vec!["fallback 100", "fallback;DebugVarsTest::resetsGas() 0",]
401 );
402 }
403
404 #[test]
405 fn folded_stack_trace_does_not_add_refunds() {
406 let mut arena = CallTraceArena::default();
408 let root = &mut arena.nodes_mut()[0];
409 root.trace.gas_used = 100;
410 root.trace.gas_refund_counter = 40;
411 root.children = vec![1];
412 root.ordering = vec![TraceMemberOrder::Call(0)];
413 arena.nodes_mut().push(CallTraceNode {
414 parent: Some(0),
415 idx: 1,
416 trace: CallTrace {
417 depth: 1,
418 gas_used: 60,
419 gas_refund_counter: 40,
420 ..Default::default()
421 },
422 ..Default::default()
423 });
424
425 assert_eq!(super::build(&arena), vec!["fallback 40", "fallback;fallback 60"]);
426 }
427}