Skip to main content

soldb_debugger/
roots.rs

1//! The top-level calls of a trace.
2//!
3//! A transaction has one root call. A test runner such as Foundry records several in one
4//! trace (the deployment, `setUp`, the test, an invariant sequence), one after another at
5//! the root depth. Each is a call or creation the backend recorded with no parent that
6//! starts at the root depth; a trace without such records has the one root its own fields
7//! describe.
8
9use soldb_core::{SoldbError, SoldbResult, TransactionTrace};
10
11/// One top-level call: where it starts, and the record that describes it.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct RootCall {
14    /// The step the call starts at.
15    pub entry_step: usize,
16    pub record: RootRecord,
17    /// What the call is, when the trace says, such as Foundry's `setup`.
18    pub kind: Option<String>,
19}
20
21/// Where a top-level call is recorded in its trace.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RootRecord {
24    /// The trace's own fields, for a trace without a record starting at its first step.
25    Trace,
26    /// An index into `artifacts.calls`.
27    Call(usize),
28    /// An index into `artifacts.creations`.
29    Creation(usize),
30}
31
32impl RootCall {
33    /// The address whose code the call runs; `None` for a creation whose address is
34    /// unknown.
35    #[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    /// The hash of the code the call runs, when the backend recorded it.
56    #[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    /// Whether the call runs creation code.
74    #[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    /// The calldata of a call, or the init code of a creation.
84    #[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/// The top-level calls of `trace`, in execution order. Never empty.
103#[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    // Without a record starting at the first step, the trace's own fields describe it.
134    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
155/// The entry step of a record that starts a top-level call. A record that ran no steps,
156/// such as a call to an account without code, is not a frame in the trace, and one
157/// entered deeper than the first step is a nested call.
158fn 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
172/// Fails when two top-level calls start at one step, which no execution produces.
173pub 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/// The index in `roots` of the top-level call `step` belongs to.
189#[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            // Nested under the first call.
273            call(Some(1), 2, 3, "0xcc"),
274            // No parent recorded, but entered deeper than the root.
275            call(None, 2, 3, "0xdd"),
276            // Ran no steps.
277            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            // Not where any root starts.
345            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}