1use crate::{
4 Debugger, DebuggerLayout, debugger::DebuggerStats, node::flatten_call_trace_with_precompiles,
5};
6use alloy_primitives::{Address, map::AddressHashMap};
7use foundry_common::{ContractsByArtifact, get_contract_name, slot_identifier::SlotIdentifier};
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 contract_identifiers: AddressHashMap<String>,
26 known_contracts: ContractsByArtifact,
28 precompile_labels: AddressHashMap<String>,
30 sources: ContractSources,
32 breakpoints: Breakpoints,
34 layout: DebuggerLayout,
36}
37
38impl DebuggerBuilder {
39 #[inline]
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 #[inline]
47 pub fn traces(mut self, traces: Traces) -> Self {
48 for (_, arena) in traces {
49 self = self.trace_arena(arena.arena);
50 }
51 self
52 }
53
54 #[inline]
56 pub fn trace_arena(mut self, arena: CallTraceArena) -> Self {
57 if let Some(root) = arena.nodes().first() {
58 self.stats.session_trace_gas_used =
59 self.stats.session_trace_gas_used.saturating_add(root.trace.gas_used);
60 }
61 self.stats.session_subcalls =
62 self.stats.session_subcalls.saturating_add(arena.nodes().len().saturating_sub(1));
63 self.trace_arenas.push(arena);
64 self
65 }
66
67 #[inline]
69 pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
70 for decoder in decoders {
71 self = self.decoder(decoder);
72 }
73 self
74 }
75
76 #[inline]
78 pub fn decoder(mut self, decoder: &CallTraceDecoder) -> Self {
79 for (address, identifier) in &decoder.contracts {
80 self.identified_contracts.insert(*address, get_contract_name(identifier).to_string());
81 self.contract_identifiers.insert(*address, identifier.clone());
82 }
83 self.precompile_labels.extend(decoder.precompile_labels());
84 self
85 }
86
87 #[inline]
89 pub fn known_contracts(mut self, known_contracts: &ContractsByArtifact) -> Self {
90 self.known_contracts = known_contracts.clone();
91 self
92 }
93
94 #[inline]
96 pub fn identified_contracts(
97 mut self,
98 identified_contracts: impl IntoIterator<Item = (Address, String)>,
99 ) -> Self {
100 self.identified_contracts.extend(identified_contracts);
101 self
102 }
103
104 #[inline]
106 pub fn sources(mut self, sources: ContractSources) -> Self {
107 self.sources = sources;
108 self
109 }
110
111 #[inline]
113 pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
114 self.breakpoints = breakpoints;
115 self
116 }
117
118 #[inline]
120 pub const fn layout(mut self, layout: DebuggerLayout) -> Self {
121 self.layout = layout;
122 self
123 }
124
125 #[inline]
127 pub fn build(self) -> Debugger {
128 let Self {
129 mut trace_arenas,
130 stats,
131 identified_contracts,
132 contract_identifiers,
133 known_contracts,
134 precompile_labels,
135 sources,
136 breakpoints,
137 layout,
138 } = self;
139 let slot_identifiers = contract_identifiers
140 .into_iter()
141 .filter_map(|(address, identifier)| {
142 let (_, contract) =
143 known_contracts.find_by_name_or_identifier(&identifier).ok().flatten()?;
144 let layout = contract.storage_layout.clone()?;
145 Some((address, SlotIdentifier::new(layout)))
146 })
147 .collect();
148 identify_internal_calls(&mut trace_arenas, &identified_contracts, &sources);
149 let mut debug_arena = Vec::new();
150 for arena in trace_arenas {
151 flatten_call_trace_with_precompiles(arena, &mut debug_arena, &precompile_labels);
152 }
153 Debugger::new_with_stats(
154 debug_arena,
155 stats,
156 identified_contracts,
157 slot_identifiers,
158 sources,
159 breakpoints,
160 layout,
161 )
162 }
163}
164
165fn identify_internal_calls(
166 trace_arenas: &mut [CallTraceArena],
167 identified_contracts: &AddressHashMap<String>,
168 sources: &ContractSources,
169) {
170 if sources.artifacts_by_name.is_empty() {
171 return;
172 }
173
174 for arena in trace_arenas {
175 for node in arena.nodes_mut() {
176 let Some(contract_name) = identified_contracts.get(&node.trace.address) else {
177 continue;
178 };
179 DebugTraceIdentifier::identify_node_steps_with_sources(node, sources, contract_name);
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use alloy_primitives::Bytes;
188 use foundry_evm_traces::{CallKind, CallTrace, CallTraceNode};
189 use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
190 use revm_inspectors::tracing::types::{CallTraceStep, TraceMemberOrder};
191
192 fn step() -> CallTraceStep {
193 CallTraceStep {
194 pc: 0,
195 op: OpCode::STOP,
196 stack: None,
197 push_stack: None,
198 memory: None,
199 returndata: Bytes::new(),
200 gas_remaining: 0,
201 gas_refund_counter: 0,
202 gas_used: 0,
203 gas_cost: 0,
204 state_gas_cost: None,
205 state_gas_reservoir: None,
206 state_gas_spent: 0,
207 storage_change: None,
208 status: Some(InstructionResult::Stop),
209 immediate_bytes: None,
210 decoded: None,
211 }
212 }
213
214 fn trace_arena(gas_used: u64, subcalls: usize) -> CallTraceArena {
215 let mut arena = CallTraceArena::default();
216
217 {
218 let root = &mut arena.nodes_mut()[0];
219 root.trace.steps.push(step());
220 root.trace.gas_limit = 1;
221 root.trace.gas_used = gas_used;
222 root.ordering.push(TraceMemberOrder::Step(0));
223
224 for idx in 1..=subcalls {
225 root.ordering.push(TraceMemberOrder::Call(idx - 1));
226 root.children.push(idx);
227 }
228 }
229
230 for idx in 1..=subcalls {
231 arena.nodes_mut().push(CallTraceNode {
232 parent: Some(0),
233 idx,
234 trace: CallTrace { depth: 1, kind: CallKind::Call, ..Default::default() },
235 ..Default::default()
236 });
237 }
238
239 arena
240 }
241
242 #[test]
243 fn trace_arena_accumulates_stats() {
244 let builder = DebuggerBuilder::new().trace_arena(trace_arena(100, 1));
245
246 assert_eq!(builder.stats.session_subcalls, 1);
247 assert_eq!(builder.stats.session_trace_gas_used, 100);
248 assert_eq!(builder.trace_arenas.len(), 1);
249 }
250
251 #[test]
252 fn trace_arena_accumulates_session_stats_across_multiple_arenas() {
253 let builder = DebuggerBuilder::new()
254 .trace_arena(trace_arena(100, 1))
255 .trace_arena(trace_arena(200, 2));
256
257 assert_eq!(builder.stats.session_subcalls, 3);
258 assert_eq!(builder.stats.session_trace_gas_used, 300);
259 assert_eq!(builder.trace_arenas.len(), 2);
260 }
261}