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