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    /// Whether the call runs creation code.
56    #[must_use]
57    pub fn creation(&self, trace: &TransactionTrace) -> bool {
58        match self.record {
59            RootRecord::Trace => trace.to_addr.is_none() && trace.contract_address.is_some(),
60            RootRecord::Call(_) => false,
61            RootRecord::Creation(_) => true,
62        }
63    }
64
65    /// The calldata of a call, or the init code of a creation.
66    #[must_use]
67    pub fn input<'a>(&self, trace: &'a TransactionTrace) -> &'a str {
68        match self.record {
69            RootRecord::Trace => &trace.input_data,
70            RootRecord::Call(index) => trace
71                .artifacts
72                .calls
73                .get(index)
74                .map_or("", |call| call.input.as_str()),
75            RootRecord::Creation(index) => trace
76                .artifacts
77                .creations
78                .get(index)
79                .map_or("", |creation| creation.init_code.as_str()),
80        }
81    }
82}
83
84/// The top-level calls of `trace`, in execution order. Never empty.
85#[must_use]
86pub fn root_calls(trace: &TransactionTrace) -> Vec<RootCall> {
87    let mut roots = Vec::new();
88    for (index, call) in trace.artifacts.calls.iter().enumerate() {
89        if call.parent_id.is_some() {
90            continue;
91        }
92        let Some(entry_step) = root_entry(trace, call.entry_step, call.exit_step) else {
93            continue;
94        };
95        roots.push(RootCall {
96            entry_step,
97            record: RootRecord::Call(index),
98            kind: None,
99        });
100    }
101    for (index, creation) in trace.artifacts.creations.iter().enumerate() {
102        if creation.parent_id.is_some() {
103            continue;
104        }
105        let Some(entry_step) = root_entry(trace, creation.entry_step, creation.exit_step) else {
106            continue;
107        };
108        roots.push(RootCall {
109            entry_step,
110            record: RootRecord::Creation(index),
111            kind: None,
112        });
113    }
114    roots.sort_by_key(|root| root.entry_step);
115    // Without a record starting at the first step, the trace's own fields describe it.
116    if roots.first().is_none_or(|root| root.entry_step != 0) {
117        roots.insert(
118            0,
119            RootCall {
120                entry_step: 0,
121                record: RootRecord::Trace,
122                kind: None,
123            },
124        );
125    }
126    for root in &mut roots {
127        let kind = trace
128            .artifacts
129            .root_kinds
130            .iter()
131            .find(|kind| kind.entry_step == root.entry_step);
132        root.kind = kind.map(|kind| kind.kind.clone());
133    }
134    roots
135}
136
137/// The entry step of a record that starts a top-level call. A record that ran no steps,
138/// such as a call to an account without code, is not a frame in the trace, and one
139/// entered deeper than the first step is a nested call.
140fn root_entry(
141    trace: &TransactionTrace,
142    entry: Option<usize>,
143    exit: Option<usize>,
144) -> Option<usize> {
145    let entry = entry?;
146    if exit.is_some_and(|exit| exit <= entry) {
147        return None;
148    }
149    let root_depth = trace.steps.first().map_or(0, |step| step.depth());
150    let step = trace.steps.get(entry)?;
151    (step.depth() == root_depth).then_some(entry)
152}
153
154/// Fails when two top-level calls start at one step, which no execution produces.
155pub fn check_root_calls(trace: &TransactionTrace) -> SoldbResult<()> {
156    let roots = root_calls(trace);
157    let Some(pair) = roots
158        .windows(2)
159        .find(|pair| pair[0].entry_step == pair[1].entry_step)
160    else {
161        return Ok(());
162    };
163    Err(SoldbError::Message(format!(
164        "invalid trace: two top-level calls start at step {}\n\
165         the trace records more than one call or creation without a parent entered there",
166        pair[0].entry_step
167    )))
168}
169
170/// The index in `roots` of the top-level call `step` belongs to.
171#[must_use]
172pub fn root_index(roots: &[RootCall], step: usize) -> usize {
173    let position = roots.partition_point(|root| root.entry_step <= step);
174    position.saturating_sub(1)
175}
176
177#[cfg(test)]
178mod tests {
179    use soldb_core::{
180        ContractCreation, ExecutionCall, RootKind, StepSnapshot, TraceStep, TransactionTrace,
181    };
182
183    use super::{check_root_calls, root_calls, root_index, RootCall, RootRecord};
184
185    fn step(depth: u64) -> TraceStep {
186        TraceStep::new(0, "STOP".into(), 0, 0, depth, None, StepSnapshot::default())
187    }
188
189    fn trace(depths: &[u64]) -> TransactionTrace {
190        TransactionTrace {
191            tx_hash: None,
192            from_addr: "0x1".to_owned(),
193            to_addr: Some("0xaa".to_owned()),
194            value: "0x0".to_owned(),
195            input_data: "0x01".to_owned(),
196            gas_used: 0,
197            output: "0x".to_owned(),
198            success: true,
199            error: None,
200            debug_trace_available: true,
201            contract_address: None,
202            backend: None,
203            capabilities: Default::default(),
204            artifacts: Default::default(),
205            steps: depths.iter().map(|depth| step(*depth)).collect(),
206        }
207    }
208
209    fn call(parent_id: Option<usize>, entry: usize, exit: usize, address: &str) -> ExecutionCall {
210        ExecutionCall {
211            id: 0,
212            parent_id,
213            depth: 1,
214            entry_step: Some(entry),
215            exit_step: Some(exit),
216            call_type: "CALL".to_owned(),
217            from: "0x1".to_owned(),
218            to: address.to_owned(),
219            bytecode_address: address.to_owned(),
220            value: "0x0".to_owned(),
221            input: format!("0x{entry:02x}"),
222            gas_limit: 0,
223            gas_used: None,
224            output: None,
225            success: Some(true),
226            error: None,
227        }
228    }
229
230    #[test]
231    fn without_records_the_trace_fields_are_the_root() {
232        let trace = trace(&[1, 1]);
233        let roots = root_calls(&trace);
234        assert_eq!(
235            roots,
236            vec![RootCall {
237                entry_step: 0,
238                record: RootRecord::Trace,
239                kind: None,
240            }]
241        );
242        assert_eq!(roots[0].address(&trace), Some("0xaa"));
243        assert!(!roots[0].creation(&trace));
244        assert_eq!(roots[0].input(&trace), "0x01");
245    }
246
247    #[test]
248    fn top_level_records_are_roots_in_execution_order() {
249        let mut trace = trace(&[1, 1, 2, 1, 1]);
250        trace.artifacts.calls = vec![
251            call(None, 3, 5, "0xbb"),
252            call(None, 0, 3, "0xaa"),
253            // Nested under the first call.
254            call(Some(1), 2, 3, "0xcc"),
255            // No parent recorded, but entered deeper than the root.
256            call(None, 2, 3, "0xdd"),
257            // Ran no steps.
258            call(None, 5, 5, "0xee"),
259        ];
260        let roots = root_calls(&trace);
261        let entries = roots.iter().map(|root| root.entry_step).collect::<Vec<_>>();
262        assert_eq!(entries, vec![0, 3]);
263        assert_eq!(roots[root_index(&roots, 2)].address(&trace), Some("0xaa"));
264        assert_eq!(roots[root_index(&roots, 4)].input(&trace), "0x03");
265    }
266
267    #[test]
268    fn two_roots_starting_at_one_step_are_invalid() {
269        let mut trace = trace(&[1, 1, 1, 1]);
270        trace.artifacts.calls = vec![
271            call(None, 0, 1, "0xaa"),
272            call(None, 1, 2, "0xbb"),
273            call(None, 2, 4, "0xdd"),
274        ];
275        assert!(check_root_calls(&trace).is_ok());
276        trace.artifacts.calls.push(call(None, 1, 2, "0xcc"));
277        let error = check_root_calls(&trace).expect_err("two roots at step 1");
278        assert!(error
279            .to_string()
280            .starts_with("invalid trace: two top-level calls start at step 1\n"));
281    }
282
283    #[test]
284    fn a_later_root_creation_follows_the_trace_root() {
285        let mut trace = trace(&[1, 1]);
286        trace.artifacts.creations.push(ContractCreation {
287            id: 0,
288            parent_id: None,
289            depth: 1,
290            entry_step: Some(1),
291            exit_step: None,
292            create_type: "CREATE".to_owned(),
293            caller: "0x1".to_owned(),
294            address: Some("0xcc".to_owned()),
295            value: "0x0".to_owned(),
296            init_code: "0x6000".to_owned(),
297            gas_limit: 0,
298            gas_used: None,
299            output: None,
300            success: Some(true),
301            error: None,
302        });
303        let roots = root_calls(&trace);
304        assert_eq!(roots.len(), 2);
305        assert_eq!(roots[0].input(&trace), "0x01");
306        assert!(roots[1].creation(&trace));
307        assert_eq!(roots[1].address(&trace), Some("0xcc"));
308        assert_eq!(roots[1].input(&trace), "0x6000");
309    }
310
311    #[test]
312    fn roots_carry_the_kind_recorded_at_their_entry_step() {
313        let mut trace = trace(&[1, 1, 1]);
314        trace.artifacts.calls = vec![call(None, 0, 2, "0xaa"), call(None, 2, 3, "0xbb")];
315        trace.artifacts.root_kinds = vec![
316            RootKind {
317                entry_step: 2,
318                kind: "execution".to_owned(),
319            },
320            RootKind {
321                entry_step: 0,
322                kind: "setup".to_owned(),
323            },
324            // Not where any root starts.
325            RootKind {
326                entry_step: 1,
327                kind: "deployment".to_owned(),
328            },
329        ];
330        let kinds = root_calls(&trace)
331            .into_iter()
332            .map(|root| root.kind)
333            .collect::<Vec<_>>();
334        assert_eq!(
335            kinds,
336            vec![Some("setup".to_owned()), Some("execution".to_owned())]
337        );
338    }
339}