1use crate::{
4 Debugger, DebuggerLayout, debugger::DebuggerStats, node::flatten_call_trace_with_precompiles,
5};
6use alloy_primitives::{Address, map::AddressHashMap};
7use foundry_common::get_contract_name;
8use foundry_evm_core::Breakpoints;
9use foundry_evm_traces::{
10 CallTraceArena, CallTraceDecoder, Traces,
11 debug::{ContractSources, DebugTraceIdentifier},
12};
13
14#[derive(Debug, Default)]
16#[must_use = "builders do nothing unless you call `build` on them"]
17pub struct DebuggerBuilder {
18 trace_arenas: Vec<CallTraceArena>,
20 stats: DebuggerStats,
22 identified_contracts: AddressHashMap<String>,
24 precompile_labels: AddressHashMap<String>,
26 sources: ContractSources,
28 breakpoints: Breakpoints,
30 layout: DebuggerLayout,
32}
33
34impl DebuggerBuilder {
35 #[inline]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 #[inline]
43 pub fn traces(mut self, traces: Traces) -> Self {
44 for (_, arena) in traces {
45 self = self.trace_arena(arena.arena);
46 }
47 self
48 }
49
50 #[inline]
52 pub fn trace_arena(mut self, arena: CallTraceArena) -> Self {
53 if let Some(root) = arena.nodes().first() {
54 self.stats.session_trace_gas_used =
55 self.stats.session_trace_gas_used.saturating_add(root.trace.gas_used);
56 }
57 self.stats.session_subcalls =
58 self.stats.session_subcalls.saturating_add(arena.nodes().len().saturating_sub(1));
59 self.trace_arenas.push(arena);
60 self
61 }
62
63 #[inline]
65 pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
66 for decoder in decoders {
67 self = self.decoder(decoder);
68 }
69 self
70 }
71
72 #[inline]
74 pub fn decoder(mut self, decoder: &CallTraceDecoder) -> Self {
75 let c = decoder.contracts.iter().map(|(k, v)| (*k, get_contract_name(v).to_string()));
76 self.identified_contracts.extend(c);
77 self.precompile_labels.extend(decoder.precompile_labels());
78 self
79 }
80
81 #[inline]
83 pub fn identified_contracts(
84 mut self,
85 identified_contracts: impl IntoIterator<Item = (Address, String)>,
86 ) -> Self {
87 self.identified_contracts.extend(identified_contracts);
88 self
89 }
90
91 #[inline]
93 pub fn sources(mut self, sources: ContractSources) -> Self {
94 self.sources = sources;
95 self
96 }
97
98 #[inline]
100 pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
101 self.breakpoints = breakpoints;
102 self
103 }
104
105 #[inline]
107 pub const fn layout(mut self, layout: DebuggerLayout) -> Self {
108 self.layout = layout;
109 self
110 }
111
112 #[inline]
114 pub fn build(self) -> Debugger {
115 let Self {
116 mut trace_arenas,
117 stats,
118 identified_contracts,
119 precompile_labels,
120 sources,
121 breakpoints,
122 layout,
123 } = self;
124 identify_internal_calls(&mut trace_arenas, &identified_contracts, &sources);
125 let mut debug_arena = Vec::new();
126 for arena in trace_arenas {
127 flatten_call_trace_with_precompiles(arena, &mut debug_arena, &precompile_labels);
128 }
129 Debugger::new_with_stats(
130 debug_arena,
131 stats,
132 identified_contracts,
133 sources,
134 breakpoints,
135 layout,
136 )
137 }
138}
139
140fn identify_internal_calls(
141 trace_arenas: &mut [CallTraceArena],
142 identified_contracts: &AddressHashMap<String>,
143 sources: &ContractSources,
144) {
145 if sources.artifacts_by_name.is_empty() {
146 return;
147 }
148
149 for arena in trace_arenas {
150 for node in arena.nodes_mut() {
151 let Some(contract_name) = identified_contracts.get(&node.trace.address) else {
152 continue;
153 };
154 DebugTraceIdentifier::identify_node_steps_with_sources(node, sources, contract_name);
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use alloy_primitives::Bytes;
163 use foundry_evm_traces::{CallKind, CallTrace, CallTraceNode};
164 use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
165 use revm_inspectors::tracing::types::{CallTraceStep, TraceMemberOrder};
166
167 fn step() -> CallTraceStep {
168 CallTraceStep {
169 pc: 0,
170 op: OpCode::STOP,
171 stack: None,
172 push_stack: None,
173 memory: None,
174 returndata: Bytes::new(),
175 gas_remaining: 0,
176 gas_refund_counter: 0,
177 gas_used: 0,
178 gas_cost: 0,
179 storage_change: None,
180 status: Some(InstructionResult::Stop),
181 immediate_bytes: None,
182 decoded: None,
183 }
184 }
185
186 fn trace_arena(gas_used: u64, subcalls: usize) -> CallTraceArena {
187 let mut arena = CallTraceArena::default();
188
189 {
190 let root = &mut arena.nodes_mut()[0];
191 root.trace.steps.push(step());
192 root.trace.gas_limit = 1;
193 root.trace.gas_used = gas_used;
194 root.ordering.push(TraceMemberOrder::Step(0));
195
196 for idx in 1..=subcalls {
197 root.ordering.push(TraceMemberOrder::Call(idx - 1));
198 root.children.push(idx);
199 }
200 }
201
202 for idx in 1..=subcalls {
203 arena.nodes_mut().push(CallTraceNode {
204 parent: Some(0),
205 idx,
206 trace: CallTrace { depth: 1, kind: CallKind::Call, ..Default::default() },
207 ..Default::default()
208 });
209 }
210
211 arena
212 }
213
214 #[test]
215 fn trace_arena_accumulates_stats() {
216 let builder = DebuggerBuilder::new().trace_arena(trace_arena(100, 1));
217
218 assert_eq!(builder.stats.session_subcalls, 1);
219 assert_eq!(builder.stats.session_trace_gas_used, 100);
220 assert_eq!(builder.trace_arenas.len(), 1);
221 }
222
223 #[test]
224 fn trace_arena_accumulates_session_stats_across_multiple_arenas() {
225 let builder = DebuggerBuilder::new()
226 .trace_arena(trace_arena(100, 1))
227 .trace_arena(trace_arena(200, 2));
228
229 assert_eq!(builder.stats.session_subcalls, 3);
230 assert_eq!(builder.stats.session_trace_gas_used, 300);
231 assert_eq!(builder.trace_arenas.len(), 2);
232 }
233}