1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum StateSource {
35 Trace,
37 Chain,
39}
40
41pub trait ChainStorage {
47 fn word(&self, address: &str, slot: &Word) -> Option<Word>;
49
50 fn label(&self) -> &str;
53}
54
55pub type ChainRead = Box<dyn Fn(&str, &Word) -> Option<Word>>;
57
58pub 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 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#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct StateVariable {
116 pub name: String,
118 pub ty: String,
120 pub slot: String,
122 pub offset: u64,
124 pub value: DebugValue,
125 pub source: StateSource,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct StorageTape {
132 records: HashMap<(usize, Word), Vec<Record>>,
133 captured: bool,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140struct Record {
141 from: usize,
142 value: Option<Word>,
143 rollback: bool,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150enum Lookup {
151 Known(Word),
152 Untouched,
154 Reverted,
156}
157
158impl StorageTape {
159 #[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 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 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 #[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 #[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#[derive(Clone, Copy)]
268pub struct StorageWords<'a> {
269 tape: &'a StorageTape,
270 step: usize,
271 context: Option<usize>,
272 address: Option<&'a str>,
274 chain: Option<&'a dyn ChainStorage>,
275}
276
277impl<'a> StorageWords<'a> {
278 #[must_use]
280 pub fn with_chain(mut self, chain: Option<&'a dyn ChainStorage>) -> Self {
281 self.chain = chain;
282 self
283 }
284
285 #[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 #[must_use]
296 pub(crate) fn chain_word(&self, slot: &Word) -> Option<Word> {
297 self.chain?.word(self.address?, slot)
298 }
299
300 #[must_use]
302 pub fn chain_label(&self) -> Option<&str> {
303 Some(self.chain?.label())
304 }
305
306 #[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 #[must_use]
330 pub fn captured(&self) -> bool {
331 self.tape.captured
332 }
333
334 pub(crate) 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 Lookup::Known(_) | Lookup::Untouched => unread,
361 }
362 }
363}
364
365#[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
383pub 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 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
435fn 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#[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 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 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 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 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 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 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 assert_eq!(map.reverted_spans(), [(2, 4)]);
671
672 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 assert_eq!(show(&trace, 5, "counter"), "5");
684 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 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 ((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 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 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 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 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 assert!(super::ChainStorage::word(&chain, "0xaa", &slot).is_some());
803 assert_eq!(reads.get(), 1);
804 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}