1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum StateSource {
38 Trace,
40 Chain,
42}
43
44pub trait ChainStorage {
50 fn word(&self, address: &str, slot: &Word) -> Option<Word>;
52
53 fn label(&self) -> &str;
56}
57
58pub type ChainRead = Box<dyn Fn(&str, &Word) -> Option<Word>>;
60
61pub 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 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#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct StateVariable {
119 pub name: String,
121 pub ty: String,
123 pub slot: String,
125 pub offset: u64,
127 pub value: DebugValue,
128 pub source: StateSource,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct StorageTape {
135 records: HashMap<(usize, Word), Vec<Record>>,
136 captured: bool,
138 space: Space,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143enum Space {
144 Persistent,
145 Transient,
146}
147
148const ZERO_WORD: Word = [0_u8; 32];
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153struct Record {
154 from: usize,
155 value: Option<Word>,
156 rollback: bool,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163enum Lookup {
164 Known(Word),
165 Untouched,
167 Reverted,
169}
170
171impl StorageTape {
172 #[must_use]
175 pub fn new(trace: &TransactionTrace, map: &StepMap) -> Self {
176 Self::index(trace, map, Space::Persistent)
177 }
178
179 #[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 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 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 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 #[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 #[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 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 fn starts_at_zero(&self) -> bool {
300 self.space == Space::Transient && self.captured
301 }
302}
303
304fn 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#[derive(Clone, Copy)]
328pub struct StorageWords<'a> {
329 tape: &'a StorageTape,
330 step: usize,
331 context: Option<usize>,
332 address: Option<&'a str>,
334 chain: Option<&'a dyn ChainStorage>,
335}
336
337impl<'a> StorageWords<'a> {
338 #[must_use]
340 pub fn with_chain(mut self, chain: Option<&'a dyn ChainStorage>) -> Self {
341 self.chain = chain;
342 self
343 }
344
345 #[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 #[must_use]
356 pub(crate) fn chain_word(&self, slot: &Word) -> Option<Word> {
357 self.chain?.word(self.address?, slot)
358 }
359
360 #[must_use]
362 pub fn chain_label(&self) -> Option<&str> {
363 Some(self.chain?.label())
364 }
365
366 #[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 #[must_use]
390 pub fn captured(&self) -> bool {
391 self.tape.captured
392 }
393
394 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 Lookup::Known(_) | Lookup::Untouched => unread,
425 }
426 }
427}
428
429#[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
447pub 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 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
499fn 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#[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 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 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 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 assert_eq!(show_transient(&trace, 0, "count"), "0");
681 assert_eq!(show_transient(&trace, 0, "locked"), "false");
682 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 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 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 assert_eq!(show_transient(&trace, 5, "count"), "1");
723 assert_eq!(show_transient(&trace, 5, "locked"), "false");
724 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 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 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 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 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 assert_eq!(map.reverted_spans(), [(2, 4)]);
839
840 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 assert_eq!(show(&trace, 5, "counter"), "5");
852 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 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 ((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 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 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 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 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 assert!(super::ChainStorage::word(&chain, "0xaa", &slot).is_some());
971 assert_eq!(reads.get(), 1);
972 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}