1use 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#[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<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 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#[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: FxHashMap<(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
150type Records = FxHashMap<(usize, Word), Vec<Record>>;
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156struct Record {
157 from: usize,
158 value: Option<Word>,
159 rollback: bool,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166enum Lookup {
167 Known(Word),
168 Untouched,
170 Reverted,
172}
173
174impl StorageTape {
175 #[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 #[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 #[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 fn finish(mut records: Records, map: &StepMap, captured: bool, space: Space) -> Self {
215 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 #[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 #[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 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 fn starts_at_zero(&self) -> bool {
296 self.space == Space::Transient && self.captured
297 }
298}
299
300fn index_records(
304 trace: &TransactionTrace,
305 map: &StepMap,
306 persistent: bool,
307 transient: bool,
308) -> (Records, Records) {
309 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
346fn 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#[derive(Clone, Copy)]
370pub struct StorageWords<'a> {
371 tape: &'a StorageTape,
372 step: usize,
373 context: Option<usize>,
374 address: Option<&'a str>,
376 chain: Option<&'a dyn ChainStorage>,
377}
378
379impl<'a> StorageWords<'a> {
380 #[must_use]
382 pub fn with_chain(mut self, chain: Option<&'a dyn ChainStorage>) -> Self {
383 self.chain = chain;
384 self
385 }
386
387 #[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 #[must_use]
398 pub(crate) fn chain_word(&self, slot: &Word) -> Option<Word> {
399 self.chain?.word(self.address?, slot)
400 }
401
402 #[must_use]
404 pub fn chain_label(&self) -> Option<&str> {
405 Some(self.chain?.label())
406 }
407
408 #[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 #[must_use]
432 pub fn captured(&self) -> bool {
433 self.tape.captured
434 }
435
436 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 Lookup::Known(_) | Lookup::Untouched => unread,
467 }
468 }
469}
470
471#[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
489pub 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 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
541fn 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#[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 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 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 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 assert_eq!(show_transient(&trace, 0, "count"), "0");
723 assert_eq!(show_transient(&trace, 0, "locked"), "false");
724 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 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 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 assert_eq!(show_transient(&trace, 5, "count"), "1");
765 assert_eq!(show_transient(&trace, 5, "locked"), "false");
766 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 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 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 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 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 assert_eq!(map.reverted_spans(), [(2, 4)]);
881
882 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 assert_eq!(show(&trace, 5, "counter"), "5");
894 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 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 ((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 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 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 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 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 assert!(super::ChainStorage::word(&chain, "0xaa", &slot).is_some());
1013 assert_eq!(reads.get(), 1);
1014 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}