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