Skip to main content

soldb_debugger/
state.rs

1//! State variables at a step, read through the compiler's storage layout.
2//!
3//! `solc --storage-layout` says where every state variable lives; the trace says what
4//! the words there held. Putting the two together answers `print counter` or `print
5//! balances[0xabc]` without ETHDebug variable information, and does so from
6//! compiler-issued fact only: the slot arithmetic is the language's own rule, and the
7//! words are what the backend recorded.
8//!
9//! A backend records a slot at the `SLOAD` or `SSTORE` that touches it, not at every
10//! step, so the value at a step is the most recent record for that slot in the same
11//! storage context. A trace is a complete recording, so that is a lookup on the tape
12//! rather than a guess: [`StorageTape`] indexes every record once, keyed by the account
13//! whose storage it belongs to, and a `DELEGATECALL` writes the caller's. A frame that
14//! reverted did not keep its writes, so after it a slot holds what it held before the
15//! frame — and if that was never read, it is unknown again.
16//!
17//! A slot with no record yet is unknown here, because the trace does not say what is in
18//! it. A frontend that has a node can supply one through [`ChainStorage`], which is read
19//! only for slots the trace never recorded, and the value is then marked as coming from
20//! the chain rather than from the recording. Without one, unknown is reported as unknown
21//! and never as zero: this crate has no chain to ask, and it does not pretend to.
22//!
23//! Transient storage is a second tape, [`StorageTape::transient`], where a slot with no
24//! record is zero: the space starts empty in every transaction.
25
26use std::cell::{Cell, RefCell};
27use std::collections::HashMap;
28
29use soldb_core::TransactionTrace;
30use soldb_ethdebug::{parse_word, word_hex, StorageLayout, StorageRef, Word};
31
32use crate::stepping::StepMap;
33use crate::{DebugValue, DebugValueStatus};
34
35/// Where a value came from.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum StateSource {
38    /// A word the transaction itself read or wrote.
39    Trace,
40    /// A word read from the chain, for a slot the transaction never touched.
41    Chain,
42}
43
44/// A chain a frontend can read storage from, for slots the trace never recorded.
45///
46/// The debugger never opens a connection; the frontend that has a node implements this
47/// and answers, and says in [`ChainStorage::label`] where the answer came from, so the
48/// value can be shown as what it is rather than as part of the recording.
49pub trait ChainStorage {
50    /// The word at `slot` of `address`, or `None` when it could not be read.
51    fn word(&self, address: &str, slot: &Word) -> Option<Word>;
52
53    /// Where these words come from, as a user-facing phrase such as
54    /// `the chain at block 21000000`.
55    fn label(&self) -> &str;
56}
57
58/// The reading a frontend supplies: one storage word of one account, or nothing.
59pub type ChainRead = Box<dyn Fn(&str, &Word) -> Option<Word>>;
60
61/// A [`ChainStorage`] that reads each slot once, through a function the frontend gives.
62///
63/// The reading is the frontend's — this crate opens no connections — but the caching and
64/// the labelling are the same wherever the words come from, so they live here rather than
65/// once per frontend.
66pub struct CachedChain<F> {
67    read: F,
68    label: String,
69    words: RefCell<HashMap<(String, Word), Option<Word>>>,
70}
71
72impl<F> std::fmt::Debug for CachedChain<F> {
73    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        formatter
75            .debug_struct("CachedChain")
76            .field("label", &self.label)
77            .field("words", &self.words.borrow().len())
78            .finish()
79    }
80}
81
82impl<F> CachedChain<F>
83where
84    F: Fn(&str, &Word) -> Option<Word>,
85{
86    /// `label` says where the words come from, as a user-facing phrase such as
87    /// `the chain at block 21000000`.
88    pub fn new(label: impl Into<String>, read: F) -> Self {
89        Self {
90            read,
91            label: label.into(),
92            words: RefCell::new(HashMap::new()),
93        }
94    }
95}
96
97impl<F> ChainStorage for CachedChain<F>
98where
99    F: Fn(&str, &Word) -> Option<Word>,
100{
101    fn word(&self, address: &str, slot: &Word) -> Option<Word> {
102        let key = (address.to_ascii_lowercase(), *slot);
103        if let Some(cached) = self.words.borrow().get(&key) {
104            return *cached;
105        }
106        let word = (self.read)(&key.0, slot);
107        self.words.borrow_mut().insert(key, word);
108        word
109    }
110
111    fn label(&self) -> &str {
112        &self.label
113    }
114}
115
116/// One state variable, or one place inside one, with its value at a step.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct StateVariable {
119    /// The path as resolved, such as `counter` or `balances[0xabc]`.
120    pub name: String,
121    /// The Solidity type as the layout labels it.
122    pub ty: String,
123    /// The slot, as `0x`-prefixed hex without leading zeros.
124    pub slot: String,
125    /// The byte offset within the slot.
126    pub offset: u64,
127    pub value: DebugValue,
128    /// Whether the value came from the recording or from the chain.
129    pub source: StateSource,
130}
131
132/// Every storage word a trace recorded, by storage context and slot.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct StorageTape {
135    records: HashMap<(usize, Word), Vec<Record>>,
136    /// Whether the backend recorded per-step storage at all.
137    captured: bool,
138    space: Space,
139}
140
141/// Which storage a tape holds.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143enum Space {
144    Persistent,
145    Transient,
146}
147
148const ZERO_WORD: Word = [0_u8; 32];
149
150/// A value a slot holds from a step on. `None` when a reverted frame undid a write and
151/// the value before it was never recorded.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153struct Record {
154    from: usize,
155    value: Option<Word>,
156    /// True for a record a revert makes. At one step a read is the later word: the
157    /// reverting frame's last step and the caller's next are the same index.
158    rollback: bool,
159}
160
161/// What the tape knows about one slot at one step.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163enum Lookup {
164    Known(Word),
165    /// Nothing has read or written the slot by this step.
166    Untouched,
167    /// The last write was reverted and the value before it was never read.
168    Reverted,
169}
170
171impl StorageTape {
172    /// Indexes every storage word `trace` recorded, in the storage context `map`
173    /// computed for the step that recorded it.
174    #[must_use]
175    pub fn new(trace: &TransactionTrace, map: &StepMap) -> Self {
176        Self::index(trace, map, Space::Persistent)
177    }
178
179    /// Indexes every transient storage word `trace` recorded, the same way.
180    #[must_use]
181    pub fn transient(trace: &TransactionTrace, map: &StepMap) -> Self {
182        Self::index(trace, map, Space::Transient)
183    }
184
185    fn index(trace: &TransactionTrace, map: &StepMap, space: Space) -> Self {
186        let (write, captured) = match space {
187            Space::Persistent => ("SSTORE", trace.capabilities.storage),
188            Space::Transient => ("TSTORE", trace.capabilities.transient_storage),
189        };
190        let mut records = HashMap::<(usize, Word), Vec<Record>>::new();
191        // A trace without transient storage has nothing to index, so skip the walk.
192        let steps = if space == Space::Transient && !captured {
193            &[][..]
194        } else {
195            &trace.steps[..]
196        };
197        for (index, step) in steps.iter().enumerate() {
198            let snapshot = step.snapshot_ref();
199            let words = match space {
200                Space::Persistent if snapshot.storage_diff.is_empty() => continue,
201                Space::Transient if snapshot.transient_storage.is_empty() => continue,
202                Space::Persistent => snapshot
203                    .storage_diff
204                    .iter()
205                    .filter_map(|(slot, change)| Some((slot, change.after.as_ref()?)))
206                    .collect::<Vec<_>>(),
207                Space::Transient => snapshot.transient_storage.iter().collect(),
208            };
209            let Some(context) = map.storage_context_index(index) else {
210                continue;
211            };
212            // A write takes effect after its step; a read reports what is there at it.
213            let from = if &*step.op == write { index + 1 } else { index };
214            for (slot, value) in words {
215                push_record(&mut records, context, from, slot, value);
216            }
217        }
218
219        // A frame that reverted kept none of the writes made while it ran, its callees'
220        // included; every slot it touched goes back to what it held when it was entered.
221        for (entry, last) in map.reverted_spans() {
222            for entries in records.values_mut() {
223                let touched = entries
224                    .iter()
225                    .any(|record| record.from > *entry && record.from <= last + 1);
226                if !touched {
227                    continue;
228                }
229                let before = entries
230                    .iter()
231                    .filter(|record| record.from <= *entry)
232                    .max_by_key(|record| (record.from, !record.rollback))
233                    .and_then(|record| record.value);
234                entries.push(Record {
235                    from: last + 1,
236                    value: before,
237                    rollback: true,
238                });
239            }
240        }
241        for entries in records.values_mut() {
242            entries.sort_by_key(|record| (record.from, !record.rollback));
243        }
244        Self {
245            records,
246            captured,
247            space,
248        }
249    }
250
251    /// The words known at `step`, in storage context `context`.
252    #[must_use]
253    pub fn at(&self, step: usize, context: Option<usize>) -> StorageWords<'_> {
254        StorageWords {
255            tape: self,
256            step,
257            context,
258            address: None,
259            chain: None,
260        }
261    }
262
263    /// The words known at `step`, in the storage context that step runs in, with the
264    /// address of that context so a [`ChainStorage`] can be asked about its slots.
265    #[must_use]
266    pub fn at_step<'a>(&'a self, map: &'a StepMap, step: usize) -> StorageWords<'a> {
267        StorageWords {
268            tape: self,
269            step,
270            context: map.storage_context_index(step),
271            address: map.storage_address(step),
272            chain: None,
273        }
274    }
275
276    fn lookup(&self, context: usize, slot: &Word, step: usize) -> Lookup {
277        match self.recorded(context, slot, step) {
278            Lookup::Untouched | Lookup::Reverted if self.starts_at_zero() => {
279                Lookup::Known(ZERO_WORD)
280            }
281            found => found,
282        }
283    }
284
285    /// What the recording itself says, without the zero an untouched transient slot holds.
286    fn recorded(&self, context: usize, slot: &Word, step: usize) -> Lookup {
287        let Some(entries) = self.records.get(&(context, *slot)) else {
288            return Lookup::Untouched;
289        };
290        let count = entries.partition_point(|record| record.from <= step);
291        match count.checked_sub(1).map(|index| entries[index].value) {
292            Some(Some(value)) => Lookup::Known(value),
293            Some(None) => Lookup::Reverted,
294            None => Lookup::Untouched,
295        }
296    }
297
298    /// Whether a slot with no surviving write is zero, as transient storage is at the start.
299    fn starts_at_zero(&self) -> bool {
300        self.space == Space::Transient && self.captured
301    }
302}
303
304/// Records that `slot` of `context` holds `value` from step `from`, unless it already did.
305fn push_record(
306    records: &mut HashMap<(usize, Word), Vec<Record>>,
307    context: usize,
308    from: usize,
309    slot: &str,
310    value: &str,
311) {
312    let (Some(slot), Some(value)) = (parse_slot(slot), parse_slot(value)) else {
313        return;
314    };
315    let entries = records.entry((context, slot)).or_default();
316    if entries.last().is_some_and(|last| last.value == Some(value)) {
317        return;
318    }
319    entries.push(Record {
320        from,
321        value: Some(value),
322        rollback: false,
323    });
324}
325
326/// The storage words known at one step, in one storage context.
327#[derive(Clone, Copy)]
328pub struct StorageWords<'a> {
329    tape: &'a StorageTape,
330    step: usize,
331    context: Option<usize>,
332    /// The account whose storage this is, for reading from a chain.
333    address: Option<&'a str>,
334    chain: Option<&'a dyn ChainStorage>,
335}
336
337impl<'a> StorageWords<'a> {
338    /// Reads slots the trace never recorded from `chain`.
339    #[must_use]
340    pub fn with_chain(mut self, chain: Option<&'a dyn ChainStorage>) -> Self {
341        self.chain = chain;
342        self
343    }
344
345    /// The word at `slot`, when the trace recorded one that still holds.
346    #[must_use]
347    pub fn get(&self, slot: &Word) -> Option<Word> {
348        match self.tape.lookup(self.context?, slot, self.step) {
349            Lookup::Known(value) => Some(value),
350            Lookup::Untouched | Lookup::Reverted => None,
351        }
352    }
353
354    /// The word at `slot` from the chain, for a slot the trace did not record.
355    #[must_use]
356    pub(crate) fn chain_word(&self, slot: &Word) -> Option<Word> {
357        self.chain?.word(self.address?, slot)
358    }
359
360    /// How a chain-read value should be described, when a chain is attached.
361    #[must_use]
362    pub fn chain_label(&self) -> Option<&str> {
363        Some(self.chain?.label())
364    }
365
366    /// Every slot with a known value at the step, in slot order.
367    #[must_use]
368    pub fn known(&self) -> Vec<(Word, Word)> {
369        let Some(context) = self.context else {
370            return Vec::new();
371        };
372        let mut known = self
373            .tape
374            .records
375            .keys()
376            .filter(|(candidate, _)| *candidate == context)
377            .filter_map(
378                |(_, slot)| match self.tape.recorded(context, slot, self.step) {
379                    Lookup::Known(value) => Some((*slot, value)),
380                    Lookup::Untouched | Lookup::Reverted => None,
381                },
382            )
383            .collect::<Vec<_>>();
384        known.sort_unstable();
385        known
386    }
387
388    /// Whether the backend recorded storage at all.
389    #[must_use]
390    pub fn captured(&self) -> bool {
391        self.tape.captured
392    }
393
394    /// Why a word is missing, for the user.
395    pub(crate) fn unavailable(&self, slot: &Word) -> String {
396        let unread = if self.chain.is_some() {
397            format!(
398                "<unknown: slot {} was not touched by this transaction and the chain could not be read>",
399                short_hex(slot)
400            )
401        } else {
402            format!(
403                "<unknown: slot {} has not been read or written yet>",
404                short_hex(slot)
405            )
406        };
407        if !self.tape.captured {
408            return match self.tape.space {
409                Space::Persistent => "<unavailable: this backend recorded no storage>",
410                Space::Transient => "<unavailable: this trace carries no transient storage>",
411            }
412            .to_owned();
413        }
414        let Some(context) = self.context else {
415            return "<unavailable: the storage of this frame belongs to no known account>"
416                .to_owned();
417        };
418        match self.tape.lookup(context, slot, self.step) {
419            Lookup::Reverted => format!(
420                "<unknown: the write to slot {} was reverted and the value before it was never read>",
421                short_hex(slot)
422            ),
423            // `Known` cannot reach here: a decode only fails on a slot with no word.
424            Lookup::Known(_) | Lookup::Untouched => unread,
425        }
426    }
427}
428
429/// Every state variable in the layout, with its value at the step.
430#[must_use]
431pub fn state_variables(layout: &StorageLayout, words: &StorageWords<'_>) -> Vec<StateVariable> {
432    layout
433        .variables
434        .iter()
435        .map(|variable| {
436            let reference = StorageRef {
437                path: variable.label.clone(),
438                slot: variable.slot,
439                offset: variable.offset,
440                type_id: variable.type_id.clone(),
441            };
442            read(layout, words, &reference)
443        })
444        .collect()
445}
446
447/// The value at a storage path such as `owner`, `balances[0xabc]`, `items[2]`, or
448/// `config.limit`. `Err` says why the path does not name a place in storage.
449pub fn state_value(
450    layout: &StorageLayout,
451    words: &StorageWords<'_>,
452    path: &str,
453) -> Result<StateVariable, String> {
454    let reference = layout.resolve(path).map_err(|error| error.to_string())?;
455    Ok(read(layout, words, &reference))
456}
457
458fn read(layout: &StorageLayout, words: &StorageWords<'_>, reference: &StorageRef) -> StateVariable {
459    let ty = layout
460        .type_of(&reference.type_id)
461        .map_or_else(|| reference.type_id.clone(), |ty| ty.label.clone());
462    // The recording first; the chain only for a slot it never mentioned, and the value is
463    // marked as coming from there.
464    let from_chain = Cell::new(false);
465    let decoded = layout.decode(reference, &|slot| {
466        if let Some(word) = words.get(slot) {
467            return Some(word);
468        }
469        let word = words.chain_word(slot)?;
470        from_chain.set(true);
471        Some(word)
472    });
473    let value = match decoded {
474        Ok(decoded) => DebugValue {
475            display: decoded.display,
476            raw: decoded.raw,
477            status: DebugValueStatus::Decoded,
478        },
479        Err(missing) => DebugValue {
480            display: words.unavailable(&missing),
481            raw: None,
482            status: DebugValueStatus::Unavailable,
483        },
484    };
485    StateVariable {
486        name: reference.path.clone(),
487        ty,
488        slot: short_hex(&reference.slot),
489        offset: reference.offset,
490        value,
491        source: if from_chain.get() {
492            StateSource::Chain
493        } else {
494            StateSource::Trace
495        },
496    }
497}
498
499/// A recorded slot or word, however the backend spelled it: with or without `0x`, padded
500/// to 64 digits or not.
501fn parse_slot(text: &str) -> Option<Word> {
502    let digits = text
503        .strip_prefix("0x")
504        .or_else(|| text.strip_prefix("0X"))
505        .unwrap_or(text);
506    if digits.is_empty() {
507        return Some([0_u8; 32]);
508    }
509    parse_word(&format!("0x{digits}")).ok()
510}
511
512/// `0x`-prefixed hex without leading zeros; `0x0` for zero.
513#[must_use]
514pub fn short_hex(word: &Word) -> String {
515    let full = word_hex(word);
516    let trimmed = full[2..].trim_start_matches('0');
517    if trimmed.is_empty() {
518        "0x0".to_owned()
519    } else {
520        format!("0x{trimmed}")
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use std::collections::BTreeMap;
527
528    use serde_json::json;
529
530    use soldb_core::{StepSnapshot, StorageChange, TraceCapabilities, TraceStep, TransactionTrace};
531    use soldb_ethdebug::StorageLayout;
532
533    use super::{short_hex, state_value, state_variables, StorageTape, Word};
534    use crate::stepping::StepMap;
535    use crate::DebugValueStatus;
536
537    /// The slot of `balances[0xf39f…]`, as the layout resolves it.
538    fn mapping_entry_slot() -> Word {
539        layout()
540            .resolve("balances[0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266]")
541            .expect("entry")
542            .slot
543    }
544
545    fn layout() -> StorageLayout {
546        StorageLayout::parse(&json!({
547            "storage": [
548                {"astId": 1, "contract": "T.sol:T", "label": "counter", "offset": 0, "slot": "0", "type": "t_uint256"},
549                {"astId": 2, "contract": "T.sol:T", "label": "balances", "offset": 0, "slot": "1", "type": "t_mapping(t_address,t_uint256)"}
550            ],
551            "types": {
552                "t_uint256": {"encoding": "inplace", "label": "uint256", "numberOfBytes": "32"},
553                "t_address": {"encoding": "inplace", "label": "address", "numberOfBytes": "20"},
554                "t_mapping(t_address,t_uint256)": {"encoding": "mapping", "key": "t_address", "label": "mapping(address => uint256)", "numberOfBytes": "32", "value": "t_uint256"}
555            }
556        }))
557        .expect("layout")
558    }
559
560    /// One step, recording the slots it touched the way a backend does: at the `SLOAD` or
561    /// `SSTORE` itself, not cumulatively.
562    fn step(op: &str, depth: u64, stack: &[&str], touched: &[(&str, &str)]) -> TraceStep {
563        let storage = touched
564            .iter()
565            .map(|(slot, value)| ((*slot).to_owned(), (*value).to_owned()))
566            .collect::<BTreeMap<_, _>>();
567        let storage_diff = touched
568            .iter()
569            .map(|(slot, value)| {
570                (
571                    (*slot).to_owned(),
572                    StorageChange {
573                        before: None,
574                        after: Some((*value).to_owned()),
575                    },
576                )
577            })
578            .collect();
579        TraceStep {
580            pc: 0,
581            op: op.into(),
582            gas: 0,
583            gas_cost: 0,
584            depth,
585            stack: Vec::new(),
586            memory: None,
587            storage: None,
588            error: None,
589            snapshot: StepSnapshot::new(
590                stack
591                    .iter()
592                    .map(|word| soldb_core::Word::from(*word))
593                    .collect(),
594                None,
595                storage,
596                storage_diff,
597            ),
598        }
599    }
600
601    fn trace(steps: Vec<TraceStep>, captured: bool) -> TransactionTrace {
602        TransactionTrace {
603            tx_hash: None,
604            from_addr: "0x1".to_owned(),
605            to_addr: Some("0xAAaa000000000000000000000000000000000001".to_owned()),
606            value: "0x0".to_owned(),
607            input_data: "0x".to_owned(),
608            gas_used: 0,
609            output: "0x".to_owned(),
610            success: true,
611            error: None,
612            debug_trace_available: true,
613            contract_address: None,
614            backend: None,
615            capabilities: TraceCapabilities {
616                storage: captured,
617                ..Default::default()
618            },
619            artifacts: Default::default(),
620            steps,
621        }
622    }
623
624    fn show(trace: &TransactionTrace, at: usize, path: &str) -> String {
625        let map = StepMap::new(trace, Vec::new());
626        let tape = StorageTape::new(trace, &map);
627        state_value(&layout(), &tape.at_step(&map, at), path)
628            .expect(path)
629            .value
630            .display
631    }
632
633    /// One step recording the transient slots it touched.
634    fn transient_step(op: &str, depth: u64, touched: &[(&str, &str)]) -> TraceStep {
635        let mut step = step(op, depth, &["0x0"], &[]);
636        step.snapshot.transient_storage = touched
637            .iter()
638            .map(|(slot, value)| ((*slot).to_owned(), (*value).to_owned()))
639            .collect();
640        step
641    }
642
643    fn transient_layout() -> StorageLayout {
644        StorageLayout::parse(&json!({
645            "storage": [
646                {"astId": 1, "contract": "T.sol:T", "label": "count", "offset": 0, "slot": "0", "type": "t_uint256"},
647                {"astId": 2, "contract": "T.sol:T", "label": "locked", "offset": 0, "slot": "1", "type": "t_bool"}
648            ],
649            "types": {
650                "t_uint256": {"encoding": "inplace", "label": "uint256", "numberOfBytes": "32"},
651                "t_bool": {"encoding": "inplace", "label": "bool", "numberOfBytes": "1"}
652            }
653        }))
654        .expect("layout")
655    }
656
657    fn show_transient(trace: &TransactionTrace, at: usize, path: &str) -> String {
658        let map = StepMap::new(trace, Vec::new());
659        let tape = StorageTape::transient(trace, &map);
660        state_value(&transient_layout(), &tape.at_step(&map, at), path)
661            .expect(path)
662            .value
663            .display
664    }
665
666    #[test]
667    fn transient_slots_start_at_zero_and_follow_their_writes() {
668        let mut trace = trace(
669            vec![
670                transient_step("PUSH1", 1, &[]),
671                transient_step("TSTORE", 1, &[("0x0", "0x5")]),
672                transient_step("POP", 1, &[]),
673                transient_step("TLOAD", 1, &[("0x0", "0x5")]),
674                transient_step("STOP", 1, &[]),
675            ],
676            true,
677        );
678        trace.capabilities.transient_storage = true;
679        // Nothing has written yet, and transient storage starts empty in a transaction.
680        assert_eq!(show_transient(&trace, 0, "count"), "0");
681        assert_eq!(show_transient(&trace, 0, "locked"), "false");
682        // A write takes effect after its step.
683        assert_eq!(show_transient(&trace, 1, "count"), "0");
684        assert_eq!(show_transient(&trace, 2, "count"), "5");
685        assert_eq!(show_transient(&trace, 4, "count"), "5");
686        // Persistent storage is a separate space: the same slot number is untouched there.
687        let map = StepMap::new(&trace, Vec::new());
688        let tape = StorageTape::new(&trace, &map);
689        assert!(tape.at_step(&map, 4).known().is_empty());
690    }
691
692    #[test]
693    fn a_reverted_frame_undoes_its_transient_writes() {
694        // The contract calls itself, so both frames share one transient storage.
695        let own = "0xaaaa000000000000000000000000000000000001";
696        let mut call = transient_step("CALL", 1, &[]);
697        call.snapshot.stack = ["0x0", "0x0", "0x0", "0x0", "0x0", own, "0x0"]
698            .into_iter()
699            .map(soldb_core::Word::from)
700            .collect();
701        let mut trace = trace(
702            vec![
703                transient_step("TSTORE", 1, &[("0x0", "0x1")]),
704                call,
705                transient_step("TSTORE", 2, &[("0x0", "0x2")]),
706                transient_step("TSTORE", 2, &[("0x1", "0x1")]),
707                transient_step("REVERT", 2, &[]),
708                transient_step("POP", 1, &[]),
709                transient_step("STOP", 1, &[]),
710            ],
711            true,
712        );
713        trace.capabilities.transient_storage = true;
714        let map = StepMap::new(&trace, Vec::new());
715        assert_eq!(map.reverted_spans(), [(2, 4)]);
716        assert_eq!(map.storage_context_index(2), map.storage_context_index(0));
717
718        assert_eq!(show_transient(&trace, 4, "count"), "2");
719        assert_eq!(show_transient(&trace, 4, "locked"), "true");
720        // After the revert: the caller's write again, and zero for a slot only the
721        // reverted frame wrote.
722        assert_eq!(show_transient(&trace, 5, "count"), "1");
723        assert_eq!(show_transient(&trace, 5, "locked"), "false");
724        // Only the caller's write survives to be listed; the undone one is not a record.
725        let tape = StorageTape::transient(&trace, &map);
726        let known = tape.at_step(&map, 5).known();
727        assert_eq!(known.len(), 1);
728        assert_eq!(short_hex(&known[0].1), "0x1");
729    }
730
731    #[test]
732    fn transient_storage_without_the_capability_is_unknown_not_zero() {
733        let trace = trace(vec![transient_step("STOP", 1, &[])], true);
734        assert!(show_transient(&trace, 0, "count").contains("carries no transient storage"));
735    }
736
737    #[test]
738    fn a_slot_holds_its_last_recorded_word_until_the_next_write() {
739        let padded = format!("{:0>64}", "7");
740        let trace = trace(
741            vec![
742                step("PUSH1", 1, &[], &[]),
743                step("SLOAD", 1, &[], &[(&padded, &format!("{:0>64}", "3"))]),
744                step("SLOAD", 1, &[], &[("0x0", "0x2a")]),
745                step("ADD", 1, &[], &[]),
746                step("SSTORE", 1, &[], &[("0x0", "0x2b")]),
747                step("POP", 1, &[], &[]),
748                step("STOP", 1, &[], &[]),
749            ],
750            true,
751        );
752        assert!(show(&trace, 0, "counter").contains("has not been read or written yet"));
753        assert!(show(&trace, 1, "counter").contains("has not been read or written yet"));
754        assert_eq!(show(&trace, 2, "counter"), "42");
755        assert_eq!(show(&trace, 3, "counter"), "42");
756        // The write takes effect after the SSTORE step, not at it.
757        assert_eq!(show(&trace, 4, "counter"), "42");
758        assert_eq!(show(&trace, 5, "counter"), "43");
759        assert_eq!(show(&trace, 6, "counter"), "43");
760
761        let map = StepMap::new(&trace, Vec::new());
762        let tape = StorageTape::new(&trace, &map);
763        let words = tape.at_step(&map, 6);
764        let variables = state_variables(&layout(), &words);
765        assert_eq!(variables.len(), 2);
766        assert_eq!(
767            (
768                variables[0].name.as_str(),
769                variables[0].ty.as_str(),
770                variables[0].slot.as_str()
771            ),
772            ("counter", "uint256", "0x0")
773        );
774        assert_eq!(variables[0].value.status, DebugValueStatus::Decoded);
775        assert_eq!(
776            variables[0].value.raw.as_deref(),
777            Some(&format!("0x{:0>64}", "2b")[..])
778        );
779        assert_eq!(variables[1].value.display, "<mapping; index it with [key]>");
780        assert_eq!(variables[1].ty, "mapping(address => uint256)");
781        // Slots the trace recorded, however they were spelled, in slot order.
782        let known = words.known();
783        assert_eq!(known.len(), 2);
784        assert_eq!(short_hex(&known[0].1), "0x2b");
785        assert_eq!(short_hex(&known[1].0), "0x7");
786
787        let entry = state_value(
788            &layout(),
789            &words,
790            "balances[0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266]",
791        )
792        .expect("entry");
793        assert_eq!(entry.ty, "uint256");
794        assert_eq!(entry.value.status, DebugValueStatus::Unavailable);
795        let error = state_value(&layout(), &words, "nothing").expect_err("unknown variable");
796        assert!(
797            error.contains("no state variable named `nothing`"),
798            "{error}"
799        );
800    }
801
802    #[test]
803    fn a_reverted_call_keeps_none_of_its_writes() {
804        let callee = "0xbbbb000000000000000000000000000000000002";
805        let trace = trace(
806            vec![
807                step("SLOAD", 1, &[], &[("0x0", "0x5")]),
808                step(
809                    "CALL",
810                    1,
811                    &["0x0", "0x0", "0x0", "0x0", "0x0", callee, "0x0"],
812                    &[],
813                ),
814                // The callee writes its own storage, then reverts.
815                step("SSTORE", 2, &[], &[("0x0", "0x9")]),
816                step("POP", 2, &[], &[]),
817                step("REVERT", 2, &[], &[]),
818                step("POP", 1, &[], &[]),
819                step(
820                    "DELEGATECALL",
821                    1,
822                    &["0x0", "0x0", "0x0", "0x0", callee, "0x0"],
823                    &[],
824                ),
825                // A delegate call writes the caller's storage, and commits.
826                step("SSTORE", 2, &[], &[("0x0", "0x6")]),
827                step("RETURN", 2, &[], &[]),
828                step("STOP", 1, &[], &[]),
829            ],
830            true,
831        );
832        let map = StepMap::new(&trace, Vec::new());
833        let tape = StorageTape::new(&trace, &map);
834        let caller = map.storage_context_index(0).expect("root context");
835        let callee_context = map.storage_context_index(2).expect("callee context");
836        assert_ne!(caller, callee_context);
837        // The frame is entered at its first step, not at the caller's `CALL`.
838        assert_eq!(map.reverted_spans(), [(2, 4)]);
839
840        // Inside the callee its write holds; after the revert the slot is unknown again,
841        // because nothing read it before the write.
842        assert_eq!(show(&trace, 3, "counter"), "9");
843        let after = tape.at(5, Some(callee_context));
844        assert!(after.get(&[0_u8; 32]).is_none());
845        assert!(state_value(&layout(), &after, "counter")
846            .expect("counter")
847            .value
848            .display
849            .contains("was reverted"));
850        // The caller's own slot was never touched by the reverted frame.
851        assert_eq!(show(&trace, 5, "counter"), "5");
852        // The delegate call runs against the caller's storage, and its write survives.
853        assert_eq!(map.storage_context_index(7), Some(caller));
854        assert_eq!(show(&trace, 7, "counter"), "5");
855        assert_eq!(show(&trace, 8, "counter"), "6");
856        assert_eq!(show(&trace, 9, "counter"), "6");
857    }
858
859    /// A chain a frontend would read through a node, answering from a fixed table.
860    struct FakeChain {
861        words: BTreeMap<(String, Word), Word>,
862        label: String,
863    }
864
865    impl super::ChainStorage for FakeChain {
866        fn word(&self, address: &str, slot: &Word) -> Option<Word> {
867            self.words
868                .get(&(address.to_ascii_lowercase(), *slot))
869                .copied()
870        }
871
872        fn label(&self) -> &str {
873            &self.label
874        }
875    }
876
877    #[test]
878    fn a_slot_the_trace_never_touched_can_come_from_the_chain() {
879        let trace = trace(
880            vec![
881                step("SLOAD", 1, &[], &[]),
882                step("SSTORE", 1, &[], &[("0x0", "0x2a")]),
883                step("STOP", 1, &[], &[]),
884            ],
885            true,
886        );
887        let map = StepMap::new(&trace, Vec::new());
888        let tape = StorageTape::new(&trace, &map);
889        let address = map.storage_address(0).expect("address").to_owned();
890        let entry_slot = mapping_entry_slot();
891        let chain = FakeChain {
892            words: BTreeMap::from([
893                // `counter` before the transaction, and one mapping entry.
894                ((address.clone(), [0_u8; 32]), {
895                    let mut word = [0_u8; 32];
896                    word[31] = 9;
897                    word
898                }),
899                ((address.clone(), entry_slot), {
900                    let mut word = [0_u8; 32];
901                    word[31] = 4;
902                    word
903                }),
904            ]),
905            label: "the chain at block 7".to_owned(),
906        };
907        let layout = layout();
908        let words = tape.at_step(&map, 0).with_chain(Some(&chain));
909        assert_eq!(words.chain_label(), Some("the chain at block 7"));
910
911        // Before the transaction writes it, the slot reads from the chain and says so.
912        let counter = state_value(&layout, &words, "counter").expect("counter");
913        assert_eq!(counter.value.display, "9");
914        assert_eq!(counter.source, super::StateSource::Chain);
915        assert_eq!(counter.value.status, DebugValueStatus::Decoded);
916
917        // A mapping entry the transaction never touched, read by key.
918        let entry = state_value(
919            &layout,
920            &words,
921            "balances[0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266]",
922        )
923        .expect("entry");
924        assert_eq!(entry.value.display, "4");
925        assert_eq!(entry.source, super::StateSource::Chain);
926
927        // After the transaction writes it, the recording wins over the chain.
928        let words = tape.at_step(&map, 2).with_chain(Some(&chain));
929        let counter = state_value(&layout, &words, "counter").expect("counter");
930        assert_eq!(counter.value.display, "42");
931        assert_eq!(counter.source, super::StateSource::Trace);
932
933        // A slot the chain cannot answer for stays unknown, and says the chain was asked.
934        let empty = FakeChain {
935            words: BTreeMap::new(),
936            label: "the chain at block 7".to_owned(),
937        };
938        let words = tape.at_step(&map, 0).with_chain(Some(&empty));
939        let counter = state_value(&layout, &words, "counter").expect("counter");
940        assert_eq!(counter.source, super::StateSource::Trace);
941        assert!(
942            counter
943                .value
944                .display
945                .contains("the chain could not be read"),
946            "{}",
947            counter.value.display
948        );
949    }
950
951    #[test]
952    fn a_cached_chain_reads_each_slot_once() {
953        use std::cell::Cell;
954
955        let reads = Cell::new(0_u32);
956        let chain =
957            super::CachedChain::new("the chain at block 7", |_address: &str, slot: &Word| {
958                reads.set(reads.get() + 1);
959                (slot[31] == 1).then(|| {
960                    let mut word = [0_u8; 32];
961                    word[31] = 9;
962                    word
963                })
964            });
965        let mut slot = [0_u8; 32];
966        slot[31] = 1;
967        assert_eq!(super::ChainStorage::label(&chain), "the chain at block 7");
968        assert!(super::ChainStorage::word(&chain, "0xAA", &slot).is_some());
969        // The same slot, and the same account however it is spelled, is not read again.
970        assert!(super::ChainStorage::word(&chain, "0xaa", &slot).is_some());
971        assert_eq!(reads.get(), 1);
972        // A slot the chain has no answer for is remembered as unanswered, not retried.
973        let missing = [0_u8; 32];
974        assert!(super::ChainStorage::word(&chain, "0xaa", &missing).is_none());
975        assert!(super::ChainStorage::word(&chain, "0xaa", &missing).is_none());
976        assert_eq!(reads.get(), 2);
977    }
978
979    #[test]
980    fn says_when_the_backend_recorded_no_storage() {
981        let trace = trace(vec![step("STOP", 1, &[], &[])], false);
982        let map = StepMap::new(&trace, Vec::new());
983        let tape = StorageTape::new(&trace, &map);
984        let words = tape.at_step(&map, 0);
985        assert!(!words.captured());
986        assert!(words.known().is_empty());
987        let counter = state_value(&layout(), &words, "counter").expect("counter");
988        assert_eq!(
989            counter.value.display,
990            "<unavailable: this backend recorded no storage>"
991        );
992    }
993
994    #[test]
995    fn short_hex_trims_leading_zeros() {
996        let mut word = [0_u8; 32];
997        assert_eq!(short_hex(&word), "0x0");
998        word[31] = 0x1f;
999        assert_eq!(short_hex(&word), "0x1f");
1000        word[0] = 0xab;
1001        assert!(short_hex(&word).starts_with("0xab00"));
1002    }
1003}