1use soldb_core::{SoldbError, SoldbResult, TransactionTrace};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct RootCall {
14 pub entry_step: usize,
16 pub record: RootRecord,
17 pub kind: Option<String>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RootRecord {
24 Trace,
26 Call(usize),
28 Creation(usize),
30}
31
32impl RootCall {
33 #[must_use]
36 pub fn address<'a>(&self, trace: &'a TransactionTrace) -> Option<&'a str> {
37 match self.record {
38 RootRecord::Trace => trace
39 .to_addr
40 .as_deref()
41 .or(trace.contract_address.as_deref()),
42 RootRecord::Call(index) => trace
43 .artifacts
44 .calls
45 .get(index)
46 .map(|call| call.bytecode_address.as_str()),
47 RootRecord::Creation(index) => trace
48 .artifacts
49 .creations
50 .get(index)
51 .and_then(|creation| creation.address.as_deref()),
52 }
53 }
54
55 #[must_use]
57 pub fn code_hash<'a>(&self, trace: &'a TransactionTrace) -> Option<&'a str> {
58 match self.record {
59 RootRecord::Trace => None,
60 RootRecord::Call(index) => trace
61 .artifacts
62 .calls
63 .get(index)
64 .and_then(|call| call.code_hash.as_deref()),
65 RootRecord::Creation(index) => trace
66 .artifacts
67 .creations
68 .get(index)
69 .and_then(|creation| creation.code_hash.as_deref()),
70 }
71 }
72
73 #[must_use]
75 pub fn creation(&self, trace: &TransactionTrace) -> bool {
76 match self.record {
77 RootRecord::Trace => trace.to_addr.is_none() && trace.contract_address.is_some(),
78 RootRecord::Call(_) => false,
79 RootRecord::Creation(_) => true,
80 }
81 }
82
83 #[must_use]
85 pub fn input<'a>(&self, trace: &'a TransactionTrace) -> &'a str {
86 match self.record {
87 RootRecord::Trace => &trace.input_data,
88 RootRecord::Call(index) => trace
89 .artifacts
90 .calls
91 .get(index)
92 .map_or("", |call| call.input.as_str()),
93 RootRecord::Creation(index) => trace
94 .artifacts
95 .creations
96 .get(index)
97 .map_or("", |creation| creation.init_code.as_str()),
98 }
99 }
100}
101
102#[must_use]
104pub fn root_calls(trace: &TransactionTrace) -> Vec<RootCall> {
105 let mut roots = Vec::new();
106 for (index, call) in trace.artifacts.calls.iter().enumerate() {
107 if call.parent_id.is_some() {
108 continue;
109 }
110 let Some(entry_step) = root_entry(trace, call.entry_step, call.exit_step) else {
111 continue;
112 };
113 roots.push(RootCall {
114 entry_step,
115 record: RootRecord::Call(index),
116 kind: None,
117 });
118 }
119 for (index, creation) in trace.artifacts.creations.iter().enumerate() {
120 if creation.parent_id.is_some() {
121 continue;
122 }
123 let Some(entry_step) = root_entry(trace, creation.entry_step, creation.exit_step) else {
124 continue;
125 };
126 roots.push(RootCall {
127 entry_step,
128 record: RootRecord::Creation(index),
129 kind: None,
130 });
131 }
132 roots.sort_by_key(|root| root.entry_step);
133 if roots.first().is_none_or(|root| root.entry_step != 0) {
135 roots.insert(
136 0,
137 RootCall {
138 entry_step: 0,
139 record: RootRecord::Trace,
140 kind: None,
141 },
142 );
143 }
144 for root in &mut roots {
145 let kind = trace
146 .artifacts
147 .root_kinds
148 .iter()
149 .find(|kind| kind.entry_step == root.entry_step);
150 root.kind = kind.map(|kind| kind.kind.clone());
151 }
152 roots
153}
154
155fn root_entry(
159 trace: &TransactionTrace,
160 entry: Option<usize>,
161 exit: Option<usize>,
162) -> Option<usize> {
163 let entry = entry?;
164 if exit.is_some_and(|exit| exit <= entry) {
165 return None;
166 }
167 let root_depth = trace.steps.first().map_or(0, |step| step.depth());
168 let step = trace.steps.get(entry)?;
169 (step.depth() == root_depth).then_some(entry)
170}
171
172pub fn check_root_calls(trace: &TransactionTrace) -> SoldbResult<()> {
174 let roots = root_calls(trace);
175 let Some(pair) = roots
176 .windows(2)
177 .find(|pair| pair[0].entry_step == pair[1].entry_step)
178 else {
179 return Ok(());
180 };
181 Err(SoldbError::Message(format!(
182 "invalid trace: two top-level calls start at step {}\n\
183 the trace records more than one call or creation without a parent entered there",
184 pair[0].entry_step
185 )))
186}
187
188#[must_use]
190pub fn root_index(roots: &[RootCall], step: usize) -> usize {
191 let position = roots.partition_point(|root| root.entry_step <= step);
192 position.saturating_sub(1)
193}
194
195#[cfg(test)]
196mod tests {
197 use soldb_core::{
198 ContractCreation, ExecutionCall, RootKind, StepSnapshot, TraceStep, TransactionTrace,
199 };
200
201 use super::{check_root_calls, root_calls, root_index, RootCall, RootRecord};
202
203 fn step(depth: u64) -> TraceStep {
204 TraceStep::new(0, "STOP".into(), 0, 0, depth, None, StepSnapshot::default())
205 }
206
207 fn trace(depths: &[u64]) -> TransactionTrace {
208 TransactionTrace {
209 tx_hash: None,
210 from_addr: "0x1".to_owned(),
211 to_addr: Some("0xaa".to_owned()),
212 value: "0x0".to_owned(),
213 input_data: "0x01".to_owned(),
214 gas_used: 0,
215 output: "0x".to_owned(),
216 success: true,
217 error: None,
218 debug_trace_available: true,
219 contract_address: None,
220 backend: None,
221 capabilities: Default::default(),
222 artifacts: Default::default(),
223 steps: depths.iter().map(|depth| step(*depth)).collect(),
224 }
225 }
226
227 fn call(parent_id: Option<usize>, entry: usize, exit: usize, address: &str) -> ExecutionCall {
228 ExecutionCall {
229 id: 0,
230 parent_id,
231 depth: 1,
232 entry_step: Some(entry),
233 exit_step: Some(exit),
234 call_type: "CALL".to_owned(),
235 from: "0x1".to_owned(),
236 to: address.to_owned(),
237 bytecode_address: address.to_owned(),
238 value: "0x0".to_owned(),
239 input: format!("0x{entry:02x}"),
240 gas_limit: 0,
241 gas_used: None,
242 output: None,
243 success: Some(true),
244 error: None,
245 code_hash: None,
246 }
247 }
248
249 #[test]
250 fn without_records_the_trace_fields_are_the_root() {
251 let trace = trace(&[1, 1]);
252 let roots = root_calls(&trace);
253 assert_eq!(
254 roots,
255 vec![RootCall {
256 entry_step: 0,
257 record: RootRecord::Trace,
258 kind: None,
259 }]
260 );
261 assert_eq!(roots[0].address(&trace), Some("0xaa"));
262 assert!(!roots[0].creation(&trace));
263 assert_eq!(roots[0].input(&trace), "0x01");
264 }
265
266 #[test]
267 fn top_level_records_are_roots_in_execution_order() {
268 let mut trace = trace(&[1, 1, 2, 1, 1]);
269 trace.artifacts.calls = vec![
270 call(None, 3, 5, "0xbb"),
271 call(None, 0, 3, "0xaa"),
272 call(Some(1), 2, 3, "0xcc"),
274 call(None, 2, 3, "0xdd"),
276 call(None, 5, 5, "0xee"),
278 ];
279 let roots = root_calls(&trace);
280 let entries = roots.iter().map(|root| root.entry_step).collect::<Vec<_>>();
281 assert_eq!(entries, vec![0, 3]);
282 assert_eq!(roots[root_index(&roots, 2)].address(&trace), Some("0xaa"));
283 assert_eq!(roots[root_index(&roots, 4)].input(&trace), "0x03");
284 }
285
286 #[test]
287 fn two_roots_starting_at_one_step_are_invalid() {
288 let mut trace = trace(&[1, 1, 1, 1]);
289 trace.artifacts.calls = vec![
290 call(None, 0, 1, "0xaa"),
291 call(None, 1, 2, "0xbb"),
292 call(None, 2, 4, "0xdd"),
293 ];
294 assert!(check_root_calls(&trace).is_ok());
295 trace.artifacts.calls.push(call(None, 1, 2, "0xcc"));
296 let error = check_root_calls(&trace).expect_err("two roots at step 1");
297 assert!(error
298 .to_string()
299 .starts_with("invalid trace: two top-level calls start at step 1\n"));
300 }
301
302 #[test]
303 fn a_later_root_creation_follows_the_trace_root() {
304 let mut trace = trace(&[1, 1]);
305 trace.artifacts.creations.push(ContractCreation {
306 id: 0,
307 parent_id: None,
308 depth: 1,
309 entry_step: Some(1),
310 exit_step: None,
311 create_type: "CREATE".to_owned(),
312 caller: "0x1".to_owned(),
313 address: Some("0xcc".to_owned()),
314 value: "0x0".to_owned(),
315 init_code: "0x6000".to_owned(),
316 gas_limit: 0,
317 gas_used: None,
318 output: None,
319 success: Some(true),
320 error: None,
321 code_hash: None,
322 });
323 let roots = root_calls(&trace);
324 assert_eq!(roots.len(), 2);
325 assert_eq!(roots[0].input(&trace), "0x01");
326 assert!(roots[1].creation(&trace));
327 assert_eq!(roots[1].address(&trace), Some("0xcc"));
328 assert_eq!(roots[1].input(&trace), "0x6000");
329 }
330
331 #[test]
332 fn roots_carry_the_kind_recorded_at_their_entry_step() {
333 let mut trace = trace(&[1, 1, 1]);
334 trace.artifacts.calls = vec![call(None, 0, 2, "0xaa"), call(None, 2, 3, "0xbb")];
335 trace.artifacts.root_kinds = vec![
336 RootKind {
337 entry_step: 2,
338 kind: "execution".to_owned(),
339 },
340 RootKind {
341 entry_step: 0,
342 kind: "setup".to_owned(),
343 },
344 RootKind {
346 entry_step: 1,
347 kind: "deployment".to_owned(),
348 },
349 ];
350 let kinds = root_calls(&trace)
351 .into_iter()
352 .map(|root| root.kind)
353 .collect::<Vec<_>>();
354 assert_eq!(
355 kinds,
356 vec![Some("setup".to_owned()), Some("execution".to_owned())]
357 );
358 }
359}