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