Skip to main content

soldb_core/
lib.rs

1//! Shared vocabulary types for the SolDB workspace.
2//!
3//! Every other crate agrees on the types defined here: [`TransactionTrace`] and its
4//! [`TraceStep`]s are what an execution backend produces and what the debugger, the
5//! serializer, and the frontends consume. This crate deliberately has no soldb
6//! dependencies so that it can stay the single type boundary in the graph.
7//!
8//! These types are also a serialization contract. They round-trip through `serde` into
9//! trace files and into the web-facing JSON document, so adding a field is additive and
10//! needs `#[serde(default)]`, while renaming or removing one is a breaking change.
11//!
12//! [`TraceCapabilities`] describes what the selected backend was actually able to
13//! record. Frontends should branch on those flags rather than on the backend name, so a
14//! new backend does not require changes further up the stack.
15
16use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21#[derive(Debug, Error)]
22pub enum SoldbError {
23    /// A failure described by a human-readable message.
24    #[error("{0}")]
25    Message(String),
26    /// The failure was already rendered for the user, typically as a JSON error document
27    /// on a `--json` code path. Callers must exit non-zero without printing anything else.
28    ///
29    /// This exists so the exit path never has to inspect error *text* to decide whether a
30    /// message was already shown.
31    #[error("")]
32    AlreadyReported,
33}
34
35pub type SoldbResult<T> = Result<T, SoldbError>;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct TraceStep {
39    pub pc: u64,
40    pub op: String,
41    pub gas: u64,
42    pub gas_cost: u64,
43    pub depth: u64,
44    pub stack: Vec<String>,
45    pub memory: Option<String>,
46    pub storage: Option<BTreeMap<String, String>>,
47    pub error: Option<String>,
48    #[serde(default)]
49    pub snapshot: StepSnapshot,
50}
51
52impl TraceStep {
53    /// Borrows the machine state this step captured, without copying it.
54    ///
55    /// A step records the same data twice: the flat `stack`/`memory`/`storage` fields, and
56    /// a [`StepSnapshot`] that additionally carries the storage diff. Whichever is
57    /// populated, this returns a view of it.
58    ///
59    /// Prefer this over [`TraceStep::normalized_snapshot`] anywhere that walks a trace.
60    /// Traces routinely run to hundreds of thousands of steps and each one's `memory` can
61    /// be tens of kilobytes, so copying per step — or, as variable decoding does, per
62    /// variable per step — dominates the cost of reading a trace.
63    #[must_use]
64    pub fn snapshot_ref(&self) -> StepSnapshotRef<'_> {
65        static EMPTY_STORAGE: BTreeMap<String, String> = BTreeMap::new();
66        static EMPTY_STORAGE_DIFF: BTreeMap<String, StorageChange> = BTreeMap::new();
67
68        if !self.snapshot.is_empty() {
69            return StepSnapshotRef {
70                stack: &self.snapshot.stack,
71                memory: self.snapshot.memory.as_deref(),
72                storage: &self.snapshot.storage,
73                storage_diff: &self.snapshot.storage_diff,
74            };
75        }
76        StepSnapshotRef {
77            stack: &self.stack,
78            memory: self.memory.as_deref(),
79            storage: self.storage.as_ref().unwrap_or(&EMPTY_STORAGE),
80            storage_diff: &EMPTY_STORAGE_DIFF,
81        }
82    }
83
84    /// Copies the machine state this step captured into an owned [`StepSnapshot`].
85    ///
86    /// Use this only when ownership is genuinely required; [`TraceStep::snapshot_ref`] is
87    /// the same view without the copy.
88    #[must_use]
89    pub fn normalized_snapshot(&self) -> StepSnapshot {
90        self.snapshot_ref().to_owned_snapshot()
91    }
92}
93
94/// A borrowed view of one step's captured machine state.
95///
96/// Serializes identically to [`StepSnapshot`], so it can stand in wherever a snapshot is
97/// written out. Produced by [`TraceStep::snapshot_ref`].
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
99pub struct StepSnapshotRef<'a> {
100    pub stack: &'a [String],
101    pub memory: Option<&'a str>,
102    pub storage: &'a BTreeMap<String, String>,
103    pub storage_diff: &'a BTreeMap<String, StorageChange>,
104}
105
106impl StepSnapshotRef<'_> {
107    /// Copies this view into an owned [`StepSnapshot`].
108    #[must_use]
109    pub fn to_owned_snapshot(&self) -> StepSnapshot {
110        StepSnapshot {
111            stack: self.stack.to_vec(),
112            memory: self.memory.map(str::to_owned),
113            storage: self.storage.clone(),
114            storage_diff: self.storage_diff.clone(),
115        }
116    }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
120pub struct StepSnapshot {
121    #[serde(default)]
122    pub stack: Vec<String>,
123    #[serde(default)]
124    pub memory: Option<String>,
125    #[serde(default)]
126    pub storage: BTreeMap<String, String>,
127    #[serde(default)]
128    pub storage_diff: BTreeMap<String, StorageChange>,
129}
130
131impl StepSnapshot {
132    #[must_use]
133    pub fn is_empty(&self) -> bool {
134        self.stack.is_empty()
135            && self.memory.is_none()
136            && self.storage.is_empty()
137            && self.storage_diff.is_empty()
138    }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct StorageChange {
143    pub before: Option<String>,
144    pub after: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct TransactionTrace {
149    pub tx_hash: Option<String>,
150    pub from_addr: String,
151    pub to_addr: Option<String>,
152    pub value: String,
153    pub input_data: String,
154    pub gas_used: u64,
155    pub output: String,
156    pub success: bool,
157    pub error: Option<String>,
158    pub debug_trace_available: bool,
159    pub contract_address: Option<String>,
160    #[serde(default)]
161    pub backend: Option<String>,
162    #[serde(default)]
163    pub capabilities: TraceCapabilities,
164    #[serde(default)]
165    pub artifacts: TraceArtifacts,
166    pub steps: Vec<TraceStep>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
170pub struct TraceCapabilities {
171    #[serde(default)]
172    pub opcode_steps: bool,
173    #[serde(default)]
174    pub stack: bool,
175    #[serde(default)]
176    pub memory: bool,
177    #[serde(default)]
178    pub storage: bool,
179    #[serde(default)]
180    pub storage_diff: bool,
181    #[serde(default)]
182    pub call_trace: bool,
183    #[serde(default)]
184    pub contract_creation: bool,
185    #[serde(default)]
186    pub logs: bool,
187    #[serde(default)]
188    pub revert_data: bool,
189    #[serde(default)]
190    pub gas_details: bool,
191    #[serde(default)]
192    pub account_changes: bool,
193    #[serde(default)]
194    pub notes: Vec<String>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
198pub struct TraceArtifacts {
199    #[serde(default)]
200    pub calls: Vec<ExecutionCall>,
201    #[serde(default)]
202    pub creations: Vec<ContractCreation>,
203    #[serde(default)]
204    pub logs: Vec<ExecutionLog>,
205    #[serde(default)]
206    pub account_changes: Vec<AccountChange>,
207    #[serde(default)]
208    pub gas: Option<GasSummary>,
209    #[serde(default)]
210    pub revert_data: Option<String>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct ExecutionCall {
215    pub id: usize,
216    pub parent_id: Option<usize>,
217    pub depth: u64,
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub entry_step: Option<usize>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub exit_step: Option<usize>,
222    pub call_type: String,
223    pub from: String,
224    pub to: String,
225    pub bytecode_address: String,
226    pub value: String,
227    pub input: String,
228    pub gas_limit: u64,
229    pub gas_used: Option<u64>,
230    pub output: Option<String>,
231    pub success: Option<bool>,
232    pub error: Option<String>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236pub struct ContractCreation {
237    pub id: usize,
238    pub parent_id: Option<usize>,
239    pub depth: u64,
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub entry_step: Option<usize>,
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub exit_step: Option<usize>,
244    pub create_type: String,
245    pub caller: String,
246    pub address: Option<String>,
247    pub value: String,
248    pub init_code: String,
249    pub gas_limit: u64,
250    pub gas_used: Option<u64>,
251    pub output: Option<String>,
252    pub success: Option<bool>,
253    pub error: Option<String>,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct ExecutionLog {
258    pub index: usize,
259    pub depth: u64,
260    pub address: String,
261    pub topics: Vec<String>,
262    pub data: String,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct AccountChange {
267    pub depth: u64,
268    pub kind: String,
269    pub address: Option<String>,
270    pub from: Option<String>,
271    pub to: Option<String>,
272    pub value: Option<String>,
273    pub key: Option<String>,
274    pub previous_value: Option<String>,
275    pub previous_nonce: Option<u64>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct GasSummary {
280    pub used: u64,
281    pub spent: Option<u64>,
282    pub refunded: Option<u64>,
283    pub remaining: Option<u64>,
284    pub limit: Option<u64>,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288pub struct FunctionCall {
289    pub name: String,
290    pub selector: String,
291    pub entry_step: usize,
292    pub exit_step: Option<usize>,
293    pub gas_used: u64,
294    pub depth: u64,
295    pub call_type: String,
296    pub contract_address: Option<String>,
297}
298
299#[cfg(test)]
300mod tests {
301    use std::collections::BTreeMap;
302
303    use super::{
304        FunctionCall, StepSnapshot, StorageChange, TraceArtifacts, TraceCapabilities, TraceStep,
305        TransactionTrace,
306    };
307
308    fn step_with(snapshot: StepSnapshot) -> TraceStep {
309        TraceStep {
310            pc: 3,
311            op: "SSTORE".to_owned(),
312            gas: 100,
313            gas_cost: 5,
314            depth: 1,
315            stack: vec!["0x01".to_owned(), "0x02".to_owned()],
316            memory: Some("aabb".to_owned()),
317            storage: Some(BTreeMap::from([("0x00".to_owned(), "0x2a".to_owned())])),
318            error: None,
319            snapshot,
320        }
321    }
322
323    #[test]
324    fn borrowed_snapshot_matches_the_owned_one() {
325        // `snapshot_ref` is what every trace walk uses, so it has to be indistinguishable
326        // from `normalized_snapshot` in both shapes a step can take: snapshot populated,
327        // and only the flat fields populated. The serialized forms must match too, because
328        // the web JSON document writes a snapshot per step.
329        let flat = step_with(StepSnapshot::default());
330        let populated = step_with(StepSnapshot {
331            stack: vec!["0x09".to_owned()],
332            memory: Some("ccdd".to_owned()),
333            storage: BTreeMap::from([("0x01".to_owned(), "0x63".to_owned())]),
334            storage_diff: BTreeMap::from([(
335                "0x01".to_owned(),
336                StorageChange {
337                    before: None,
338                    after: Some("0x63".to_owned()),
339                },
340            )]),
341        });
342
343        for step in [&flat, &populated] {
344            assert_eq!(
345                step.snapshot_ref().to_owned_snapshot(),
346                step.normalized_snapshot()
347            );
348            assert_eq!(
349                serde_json::to_string(&step.snapshot_ref()).expect("borrowed snapshot"),
350                serde_json::to_string(&step.normalized_snapshot()).expect("owned snapshot"),
351            );
352        }
353
354        // The flat shape falls back to the top-level fields and reports no storage diff.
355        assert_eq!(flat.snapshot_ref().stack, ["0x01", "0x02"]);
356        assert_eq!(flat.snapshot_ref().memory, Some("aabb"));
357        assert!(flat.snapshot_ref().storage_diff.is_empty());
358
359        // A populated snapshot wins over the flat fields.
360        assert_eq!(populated.snapshot_ref().stack, ["0x09"]);
361        assert_eq!(populated.snapshot_ref().memory, Some("ccdd"));
362        assert_eq!(populated.snapshot_ref().storage_diff.len(), 1);
363    }
364
365    #[test]
366    fn core_models_are_serializable() {
367        let trace = TransactionTrace {
368            tx_hash: Some("0xabc".to_owned()),
369            from_addr: "0x1".to_owned(),
370            to_addr: Some("0x2".to_owned()),
371            value: "0".to_owned(),
372            input_data: "0x".to_owned(),
373            gas_used: 1,
374            output: "0x".to_owned(),
375            success: true,
376            error: None,
377            steps: vec![TraceStep {
378                pc: 0,
379                op: "STOP".to_owned(),
380                gas: 1,
381                gas_cost: 0,
382                depth: 0,
383                stack: Vec::new(),
384                memory: None,
385                storage: None,
386                error: None,
387                snapshot: StepSnapshot::default(),
388            }],
389            debug_trace_available: true,
390            contract_address: None,
391            backend: Some("debug-rpc".to_owned()),
392            capabilities: TraceCapabilities::default(),
393            artifacts: TraceArtifacts::default(),
394        };
395
396        let encoded = serde_json::to_string(&trace).expect("trace serializes");
397        assert!(encoded.contains("\"success\":true"));
398
399        let call = FunctionCall {
400            name: "runtime_dispatcher".to_owned(),
401            selector: String::new(),
402            entry_step: 0,
403            exit_step: Some(0),
404            gas_used: 1,
405            depth: 0,
406            call_type: "entry".to_owned(),
407            contract_address: Some("0x2".to_owned()),
408        };
409        assert_eq!(call.call_type, "entry");
410
411        let change = StorageChange {
412            before: None,
413            after: Some("0x2a".to_owned()),
414        };
415        assert_eq!(change.after.as_deref(), Some("0x2a"));
416    }
417}