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