1use std::collections::BTreeMap;
33use std::num::NonZeroU32;
34use std::sync::Arc;
35
36use rustc_hash::FxHashMap;
37use soldb_core::{opcode, EvmWord, StepView, TransactionTrace};
38use soldb_ethdebug::{
39 function_selector, parse_path, CodeGenerator, EthdebugInfo, FunctionExit, Instruction,
40 PathSegment, SourceLocation, StorageLayout,
41};
42
43use crate::condition::Value;
44use crate::decode::{Place, ValueReader};
45use crate::roots::{root_calls, root_index, RootCall};
46use crate::state::StorageWords;
47use crate::types::SourceTypes;
48use crate::{
49 decode_arguments, is_value_type, parse_source_functions, readable_parameter, ArgumentLayout,
50 ArgumentOrder, ByteRange, DebugLocation, DebugValue, DebugValueStatus, DebugVariable,
51 FrameArgument, FrameState, SourceFunction, SourceParam,
52};
53
54pub const INFERRED_LOCALS_WARNING: &str = "local variables are inferred from the legacy source \
56map and the stack layout of solc's legacy code generator, not from compiler-reported \
57variable locations";
58
59pub const INFERRED_LOCALS_NOTE: &str = "values can be wrong under the optimizer, and a \
61variable whose frame could not be placed shows as unavailable; ETHDebug variable \
62information will replace this once compilers emit it";
63
64#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ContractDebugInfo {
69 pub address: Option<String>,
71 pub name: String,
72 pub info: EthdebugInfo,
73 pub source_contents: BTreeMap<u64, String>,
74 pub functions: Vec<SourceFunction>,
75 pub types: SourceTypes,
78 pub storage_layout: Option<StorageLayout>,
81 pub transient_storage_layout: Option<StorageLayout>,
83 pub code_generator: Option<CodeGenerator>,
87 line_starts: BTreeMap<u64, Vec<usize>>,
89 pc_index: PcIndex,
91 facts: Vec<InstructionFacts>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99struct InstructionFacts {
100 location: Option<SourceLocation>,
102 line: Option<u64>,
104 function: Option<usize>,
106 entry: Option<usize>,
109 marker: JumpMarker,
111 jumpdest: bool,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
117struct PcIndex {
118 dense: Vec<Option<NonZeroU32>>,
121 sparse: FxHashMap<u64, usize>,
123}
124
125impl PcIndex {
126 fn new(instructions: &[Instruction]) -> Self {
129 let bound = instructions.len().saturating_mul(33);
133 let dense_len = instructions
134 .iter()
135 .filter_map(|instruction| usize::try_from(instruction.offset).ok())
136 .filter(|offset| *offset < bound)
137 .max()
138 .map_or(0, |offset| offset + 1);
139 let mut dense = vec![None; dense_len];
140 let mut sparse = FxHashMap::default();
141 for (index, instruction) in instructions.iter().enumerate() {
142 let slot = usize::try_from(instruction.offset)
143 .ok()
144 .and_then(|offset| dense.get_mut(offset));
145 let entry = u32::try_from(index + 1).ok().and_then(NonZeroU32::new);
146 match (slot, entry) {
147 (Some(slot), Some(entry)) => *slot = Some(entry),
148 (slot, _) => {
149 if let Some(slot) = slot {
150 *slot = None;
151 }
152 sparse.insert(instruction.offset, index);
153 }
154 }
155 }
156 Self { dense, sparse }
157 }
158
159 fn get(&self, pc: u64) -> Option<usize> {
160 let dense = usize::try_from(pc)
161 .ok()
162 .and_then(|pc| self.dense.get(pc).copied().flatten());
163 match dense {
164 Some(entry) => Some(entry.get() as usize - 1),
165 None => self.sparse.get(&pc).copied(),
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173enum CodeEnvironment {
174 Create,
175 Call,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum JumpMarker {
181 None,
182 Call,
184 Return,
186}
187
188#[derive(Debug, Default)]
193pub struct SourceCache {
194 sources: FxHashMap<u64, Vec<Arc<ReadSource>>>,
197}
198
199#[derive(Debug)]
200struct ReadSource {
201 text: String,
202 functions: Vec<SourceFunction>,
203 types: SourceTypes,
204 line_starts: Vec<usize>,
205}
206
207impl SourceCache {
208 fn read(&mut self, source_id: u64, text: &str) -> Arc<ReadSource> {
210 let read = self.sources.entry(source_id).or_default();
211 if let Some(source) = read.iter().find(|source| source.text == text) {
212 return Arc::clone(source);
213 }
214 let mut types = SourceTypes::default();
215 types.add_source(source_id, text);
216 let source = Arc::new(ReadSource {
217 text: text.to_owned(),
218 functions: parse_source_functions(source_id, text),
219 types,
220 line_starts: line_starts(text),
221 });
222 read.push(Arc::clone(&source));
223 source
224 }
225}
226
227impl ContractDebugInfo {
228 #[must_use]
232 pub fn new(
233 address: Option<&str>,
234 name: &str,
235 info: EthdebugInfo,
236 source_contents: BTreeMap<u64, String>,
237 ) -> Self {
238 Self::with_source_cache(
239 address,
240 name,
241 info,
242 source_contents,
243 &mut SourceCache::default(),
244 )
245 }
246
247 #[must_use]
250 pub fn with_source_cache(
251 address: Option<&str>,
252 name: &str,
253 info: EthdebugInfo,
254 source_contents: BTreeMap<u64, String>,
255 cache: &mut SourceCache,
256 ) -> Self {
257 let read = source_contents
258 .iter()
259 .map(|(source_id, text)| (*source_id, cache.read(*source_id, text)))
260 .collect::<Vec<_>>();
261 let functions = read
262 .iter()
263 .flat_map(|(_, source)| source.functions.iter().cloned())
264 .collect::<Vec<SourceFunction>>();
265 let mut types = SourceTypes::default();
266 for (_, source) in &read {
267 types.extend_from(&source.types);
268 }
269 let line_starts = read
270 .iter()
271 .map(|(source_id, source)| (*source_id, source.line_starts.clone()))
272 .collect();
273 let pc_index = PcIndex::new(&info.instructions);
274 let locations = info
275 .instructions
276 .iter()
277 .map(Instruction::source_location)
278 .collect::<Vec<_>>();
279 let jumpdests = info
280 .instructions
281 .iter()
282 .map(|instruction| instruction.mnemonic() == Some("JUMPDEST"))
283 .collect::<Vec<_>>();
284 let mut earliest_jumpdests = FxHashMap::<(u64, u64), u64>::default();
287 for (index, instruction) in info.instructions.iter().enumerate() {
288 if !jumpdests[index] {
289 continue;
290 }
291 let Some(location) = &locations[index] else {
292 continue;
293 };
294 let earliest = earliest_jumpdests
295 .entry((location.source_id, location.offset))
296 .or_insert(instruction.offset);
297 *earliest = (*earliest).min(instruction.offset);
298 }
299 let function_entries = functions
301 .iter()
302 .enumerate()
303 .filter_map(|(function_index, function)| {
304 earliest_jumpdests
305 .get(&(function.source_id, function.declaration_start))
306 .map(|pc| (*pc, function_index))
307 })
308 .collect::<FxHashMap<_, _>>();
309 let mut contract = Self {
310 address: address.map(normalize_address),
311 name: name.to_owned(),
312 info,
313 source_contents,
314 functions,
315 types,
316 storage_layout: None,
317 transient_storage_layout: None,
318 code_generator: None,
319 line_starts,
320 pc_index,
321 facts: Vec::new(),
322 };
323 contract.facts = contract.instruction_facts(locations, &jumpdests, &function_entries);
324 contract
325 }
326
327 fn instruction_facts(
330 &self,
331 locations: Vec<Option<SourceLocation>>,
332 jumpdests: &[bool],
333 function_entries: &FxHashMap<u64, usize>,
334 ) -> Vec<InstructionFacts> {
335 let mut by_source = FxHashMap::<u64, Vec<usize>>::default();
337 for (index, function) in self.functions.iter().enumerate() {
338 by_source.entry(function.source_id).or_default().push(index);
339 }
340 let mut found = FxHashMap::<(u64, u64), (Option<u64>, Option<usize>)>::default();
343 let mut line_and_function = |location: &SourceLocation| {
344 *found
345 .entry((location.source_id, location.offset))
346 .or_insert_with(|| {
347 let function = by_source.get(&location.source_id).and_then(|candidates| {
348 narrowest_function(&self.functions, candidates, location.offset)
349 });
350 (self.line_of(location.source_id, location.offset), function)
351 })
352 };
353 self.info
354 .instructions
355 .iter()
356 .zip(locations)
357 .zip(jumpdests)
358 .map(|((instruction, location), jumpdest)| {
359 let (line, function) = location
360 .as_ref()
361 .map_or((None, None), &mut line_and_function);
362 InstructionFacts {
363 line,
364 function,
365 entry: function_entries.get(&instruction.offset).copied(),
366 marker: jump_marker(instruction),
367 jumpdest: *jumpdest,
368 location,
369 }
370 })
371 .collect()
372 }
373
374 fn facts_at_pc(&self, pc: u64) -> Option<&InstructionFacts> {
376 self.facts.get(self.pc_index.get(pc)?)
377 }
378
379 #[must_use]
381 pub const fn with_code_generator(mut self, code_generator: Option<CodeGenerator>) -> Self {
382 self.code_generator = code_generator;
383 self
384 }
385
386 #[must_use]
388 pub fn with_storage_layout(mut self, storage_layout: Option<StorageLayout>) -> Self {
389 self.storage_layout = self.with_enum_variants(storage_layout);
390 self
391 }
392
393 #[must_use]
395 pub fn with_transient_storage_layout(mut self, layout: Option<StorageLayout>) -> Self {
396 self.transient_storage_layout = self.with_enum_variants(layout);
397 self
398 }
399
400 fn with_enum_variants(&self, layout: Option<StorageLayout>) -> Option<StorageLayout> {
402 layout.map(|mut layout| {
403 layout.enum_variants = self.types.enum_variants_by_name();
404 layout
405 })
406 }
407
408 fn code_environment(&self) -> Option<CodeEnvironment> {
411 match self.info.environment.as_str() {
412 "create" => Some(CodeEnvironment::Create),
413 "call" => Some(CodeEnvironment::Call),
414 _ => None,
415 }
416 }
417
418 #[must_use]
420 pub fn function_entry_at_pc(&self, pc: u64) -> Option<usize> {
421 self.facts_at_pc(pc).and_then(|facts| facts.entry)
422 }
423
424 #[must_use]
426 pub fn is_jumpdest(&self, pc: u64) -> bool {
427 self.facts_at_pc(pc).is_some_and(|facts| facts.jumpdest)
428 }
429
430 #[must_use]
432 pub fn jump_marker_at_pc(&self, pc: u64) -> JumpMarker {
433 self.facts_at_pc(pc)
434 .map_or(JumpMarker::None, |facts| facts.marker)
435 }
436
437 #[must_use]
438 pub fn source_path(&self, source_id: u64) -> Option<&str> {
439 self.info.sources.get(&source_id).map(String::as_str)
440 }
441
442 #[must_use]
444 pub fn line_of(&self, source_id: u64, offset: u64) -> Option<u64> {
445 let starts = self.line_starts.get(&source_id)?;
446 let offset = usize::try_from(offset).ok()?;
447 Some(starts.partition_point(|start| *start <= offset) as u64)
448 }
449
450 #[must_use]
451 pub fn column_of(&self, source_id: u64, offset: u64) -> Option<u64> {
452 let starts = self.line_starts.get(&source_id)?;
453 let line = self.line_of(source_id, offset)?;
454 let line_start = *starts.get(usize::try_from(line).ok()?.checked_sub(1)?)?;
455 Some(offset.saturating_sub(line_start as u64) + 1)
456 }
457
458 #[must_use]
459 pub fn line_count(&self, source_id: u64) -> Option<u64> {
460 self.line_starts
461 .get(&source_id)
462 .map(|starts| starts.len() as u64)
463 }
464
465 #[must_use]
467 pub fn line_text(&self, source_id: u64, line: u64) -> Option<&str> {
468 let source = self.source_contents.get(&source_id)?;
469 let index = usize::try_from(line).ok()?.checked_sub(1)?;
470 source.lines().nth(index)
471 }
472
473 #[must_use]
475 pub fn location_at_pc(&self, pc: u64) -> Option<SourceLocation> {
476 self.facts_at_pc(pc)?.location.clone()
477 }
478
479 #[must_use]
481 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
482 self.info.instructions.get(self.pc_index.get(pc)?)
483 }
484
485 #[must_use]
487 pub fn modifier_depth_at_pc(&self, pc: u64) -> Option<i64> {
488 self.instruction_at_pc(pc)?.modifier_depth()
489 }
490
491 #[must_use]
493 pub fn function_for_location(&self, location: &SourceLocation) -> Option<usize> {
494 let candidates = (0..self.functions.len())
495 .filter(|index| self.functions[*index].source_id == location.source_id)
496 .collect::<Vec<_>>();
497 narrowest_function(&self.functions, &candidates, location.offset)
498 }
499
500 #[must_use]
501 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
502 self.functions.get(self.facts_at_pc(pc)?.function?)
503 }
504
505 #[must_use]
512 pub fn effective_line(&self, source_id: u64, line: u64) -> Option<u64> {
513 if line == 0 || line > self.line_count(source_id)? {
514 return None;
515 }
516 let mut narrowest = None::<(u64, u64)>;
517 for instruction in &self.info.instructions {
518 let Some(location) = instruction.source_location() else {
519 continue;
520 };
521 if location.source_id != source_id {
522 continue;
523 }
524 let start = self.line_of(source_id, location.offset)?;
525 let end = self.line_of(
526 source_id,
527 location
528 .offset
529 .saturating_add(location.length.saturating_sub(1)),
530 )?;
531 if start > line || line > end {
532 continue;
533 }
534 if start == line {
535 return Some(line);
536 }
537 if narrowest.is_none_or(|(length, _)| location.length < length) {
538 narrowest = Some((location.length, start));
539 }
540 }
541 narrowest.map(|(_, start)| start)
542 }
543}
544
545#[must_use]
548pub fn normalize_address(value: &str) -> String {
549 let hex = value.trim_start_matches("0x").trim_start_matches("0X");
550 format!("0x{}", hex.to_ascii_lowercase())
551}
552
553#[must_use]
557pub fn source_path_matches(source_path: &str, requested: &str) -> bool {
558 fn ends_with_path(longer: &str, shorter: &str) -> bool {
559 longer.ends_with(shorter)
560 && longer
561 .as_bytes()
562 .get(longer.len() - shorter.len() - 1)
563 .is_none_or(|byte| *byte == b'/')
564 }
565 fn file_name(path: &str) -> &str {
566 path.rsplit('/').next().unwrap_or(path)
567 }
568 source_path == requested
569 || ends_with_path(source_path, requested)
570 || ends_with_path(requested, source_path)
571 || (!requested.is_empty() && file_name(source_path) == file_name(requested))
572}
573
574fn narrowest_function(
577 functions: &[SourceFunction],
578 candidates: &[usize],
579 offset: u64,
580) -> Option<usize> {
581 candidates
582 .iter()
583 .copied()
584 .filter(|index| {
585 let function = &functions[*index];
586 function.declaration_start <= offset && offset <= function.body_end
587 })
588 .min_by_key(|index| {
589 let function = &functions[*index];
590 function.body_end.saturating_sub(function.declaration_start)
591 })
592}
593
594fn line_starts(source: &str) -> Vec<usize> {
595 let mut starts = vec![0];
596 starts.extend(
597 source
598 .bytes()
599 .enumerate()
600 .filter(|(_, byte)| *byte == b'\n')
601 .map(|(index, _)| index + 1),
602 );
603 starts
604}
605
606fn jump_marker(instruction: &Instruction) -> JumpMarker {
608 if !instruction.function_invocations().is_empty() {
609 JumpMarker::Call
610 } else if instruction.function_exit() == Some(FunctionExit::Return) {
611 JumpMarker::Return
612 } else {
613 JumpMarker::None
614 }
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
619pub struct LineKey {
620 pub contract: usize,
621 pub source_id: u64,
622 pub line: u64,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
627pub struct FunctionId {
628 pub contract: usize,
629 pub function: usize,
630}
631
632#[derive(Debug, Clone, PartialEq, Eq)]
634pub struct StepLocation {
635 pub key: LineKey,
636 pub generated: bool,
639 pub contract_name: String,
640 pub path: String,
641 pub offset: u64,
642 pub length: u64,
643 pub line: u64,
644 pub column: u64,
645 pub function: Option<FunctionId>,
646 pub function_name: Option<String>,
647}
648
649#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct ResolvedLine {
652 pub key: LineKey,
653 pub path: String,
654 pub requested_line: u64,
656}
657
658#[derive(Debug, Clone, PartialEq, Eq)]
660pub struct ResolvedFunction {
661 pub id: FunctionId,
662 pub name: String,
663 pub contract_name: String,
664 pub path: String,
665 pub line: u64,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq)]
670pub struct Frame {
671 pub depth: u32,
673 pub external: bool,
676 pub address: Option<String>,
677 pub contract_name: Option<String>,
678 pub function_name: Option<String>,
679 pub entry_step: usize,
681 pub step: usize,
684 pub pc: u64,
685 pub location: Option<StepLocation>,
686 pub arguments: Vec<FrameArgument>,
690 pub kind: Option<String>,
692}
693
694#[derive(Debug, Clone, PartialEq, Eq)]
696pub struct SourceListing {
697 pub path: String,
698 pub current_line: u64,
699 pub lines: Vec<(u64, String)>,
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
704struct LocationRef {
705 key: LineKey,
706 offset: u64,
707 length: u64,
708 function: Option<usize>,
709}
710
711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
712struct StepInfo {
713 location: Option<LocationRef>,
716 generated: bool,
718 key: Option<LineKey>,
721 contract: Option<usize>,
723 address: Option<usize>,
725 storage: Option<usize>,
728 evm_depth: u32,
730 starts_root: bool,
732 frame_depth: u32,
734 frame_entry: bool,
736 line_start: bool,
739}
740
741#[derive(Debug)]
746struct NearestLines<T> {
747 lines: Vec<(u32, T)>,
749}
750
751impl<T> Default for NearestLines<T> {
752 fn default() -> Self {
753 Self { lines: Vec::new() }
754 }
755}
756
757impl<T: Copy> NearestLines<T> {
758 fn visit(&mut self, depth: u32, key: Option<T>) -> Option<T> {
761 while self.lines.last().is_some_and(|(level, _)| *level > depth) {
762 self.lines.pop();
763 }
764 let nearest = match self.lines.last() {
765 Some((level, line)) if *level == depth => Some(*line),
766 _ => None,
767 };
768 if let Some(key) = key {
769 if nearest.is_some() {
770 self.lines.pop();
771 }
772 self.lines.push((depth, key));
773 }
774 nearest
775 }
776
777 fn clear(&mut self) {
779 self.lines.clear();
780 }
781}
782
783#[derive(Debug, Clone, PartialEq, Eq)]
785pub struct StepMap {
786 contracts: Vec<ContractDebugInfo>,
787 steps: Vec<StepInfo>,
788 addresses: Vec<String>,
789 pcs: Vec<u64>,
790 reverted: Vec<(usize, usize)>,
793 argument_layouts: Vec<Evidence>,
796 locals_support: Vec<Result<(), &'static str>>,
798 variable_layouts: Vec<Vec<InferredVariable>>,
801 step_variables: Vec<u32>,
803 roots: Vec<RootCall>,
805}
806
807const NO_LAYOUT: u32 = u32::MAX;
808const INLINED_LAYOUT: u32 = u32::MAX - 1;
810
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum VariableKind {
814 Parameter,
815 Return,
816 Local,
817}
818
819#[derive(Debug, Clone, PartialEq, Eq)]
821pub struct InferredVariable {
822 pub name: String,
823 pub ty: String,
825 pub kind: VariableKind,
826 pub slot: Option<usize>,
829 pub words: usize,
832}
833
834#[derive(Debug, Clone, Copy, PartialEq, Eq)]
836pub enum LocalsStatus<'a> {
837 Inferred(&'a [InferredVariable]),
839 Unavailable(&'static str),
841}
842
843#[derive(Debug, Clone, Copy, PartialEq, Eq)]
847enum Evidence {
848 Unknown,
850 Contradicted,
852 TopWords,
854 Ordered(ArgumentOrder),
856}
857
858impl Evidence {
859 fn merge(self, other: Self) -> Self {
862 match (self, other) {
863 (Self::Contradicted, _) | (_, Self::Contradicted) => Self::Contradicted,
864 (Self::Ordered(order), _) | (_, Self::Ordered(order)) => Self::Ordered(order),
865 (Self::TopWords, _) | (_, Self::TopWords) => Self::TopWords,
866 _ => Self::Unknown,
867 }
868 }
869
870 fn layout(self) -> Option<ArgumentLayout> {
871 match self {
872 Self::Ordered(order) => Some(ArgumentLayout::Ordered(order)),
873 Self::TopWords => Some(ArgumentLayout::TopWords),
874 Self::Unknown | Self::Contradicted => None,
875 }
876 }
877}
878
879#[derive(Debug, Clone, Copy)]
881struct FramedStep {
882 evm_depth: u32,
883 frame_id: u32,
884 root: usize,
886 contract: Option<usize>,
887 address: Option<usize>,
888 storage: Option<usize>,
889 location: Option<LocationRef>,
890 entry: Option<usize>,
892 marker: JumpMarker,
894}
895
896struct EvmFrame {
897 id: u32,
898 contract: Option<usize>,
899 address: Option<usize>,
900 storage: Option<usize>,
902 entry_step: usize,
904}
905
906#[derive(Clone)]
908struct FrameEntered {
909 address: Option<String>,
912 environment: CodeEnvironment,
913 delegated: bool,
915}
916
917impl FrameEntered {
918 fn recorded(trace: &TransactionTrace) -> FxHashMap<usize, Self> {
924 let entered = |entry: Option<usize>, exit: Option<usize>| {
925 entry.filter(|entry| exit.is_none_or(|exit| exit > *entry))
926 };
927 let artifacts = &trace.artifacts;
928 let mut recorded = FxHashMap::default();
929 for call in &artifacts.calls {
930 if let Some(step) = entered(call.entry_step, call.exit_step) {
931 recorded.entry(step).or_insert_with(|| Self {
932 address: Some(call.bytecode_address.clone()),
933 environment: CodeEnvironment::Call,
934 delegated: matches!(call.call_type.as_str(), "DELEGATECALL" | "CALLCODE"),
935 });
936 }
937 }
938 for creation in &artifacts.creations {
939 if let Some(step) = entered(creation.entry_step, creation.exit_step) {
940 recorded.entry(step).or_insert_with(|| Self {
941 address: creation.address.clone(),
942 environment: CodeEnvironment::Create,
943 delegated: false,
944 });
945 }
946 }
947 recorded
948 }
949
950 fn from_call(trace: &TransactionTrace, caller: Option<usize>) -> Self {
952 let op = caller.map(|caller| trace.steps.step(caller).op());
953 Self {
954 address: caller.and_then(|caller| call_target(trace.steps.step(caller))),
955 environment: if matches!(op, Some(opcode::CREATE | opcode::CREATE2)) {
956 CodeEnvironment::Create
957 } else {
958 CodeEnvironment::Call
959 },
960 delegated: matches!(op, Some(opcode::DELEGATECALL | opcode::CALLCODE)),
961 }
962 }
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
969struct FrameEntry {
970 function: Option<usize>,
971 return_pc: Option<u64>,
976 variables: Option<FrameVariables>,
979}
980
981#[derive(Debug, Clone, PartialEq, Eq)]
997struct FrameVariables {
998 params_base: Option<usize>,
1000 returns: Vec<Option<usize>>,
1002 locals_base: Option<usize>,
1004 body_seen: bool,
1006 layout: Option<u32>,
1009 modifier_slots: Option<usize>,
1013 inlined: bool,
1018 layout_inputs: LayoutInputs,
1021 param_slots: usize,
1024 last_span: Option<ByteRange>,
1026 last_height: usize,
1027}
1028
1029#[derive(Debug, Clone, Default, PartialEq, Eq)]
1032struct LayoutInputs {
1033 params_base: Option<usize>,
1034 returns: Vec<Option<usize>>,
1035 locals_base: Option<usize>,
1036 in_scope: Vec<bool>,
1038}
1039
1040impl FrameVariables {
1041 fn new(
1042 function: &SourceFunction,
1043 params_base: Option<usize>,
1044 modifier_slots: Option<usize>,
1045 ) -> Self {
1046 Self {
1047 params_base,
1048 returns: vec![None; function.returns.len()],
1049 locals_base: None,
1050 body_seen: false,
1051 layout: None,
1052 modifier_slots,
1053 inlined: false,
1054 layout_inputs: LayoutInputs::default(),
1055 param_slots: parameter_slots(&function.params),
1056 last_span: None,
1057 last_height: 0,
1058 }
1059 }
1060
1061 fn place_from_parameters(&mut self, function: &SourceFunction) {
1064 let Some(base) = self.params_base else {
1065 return;
1066 };
1067 let returns_base = base + self.param_slots;
1068 for (index, slot) in self.returns.iter_mut().enumerate() {
1069 slot.get_or_insert(returns_base + index);
1070 }
1071 if self.locals_base.is_none() {
1072 if let Some(modifier_slots) = self.modifier_slots {
1073 self.locals_base = Some(returns_base + function.returns.len() + modifier_slots);
1074 }
1075 }
1076 }
1077
1078 fn track(
1081 &mut self,
1082 function: &SourceFunction,
1083 span: ByteRange,
1084 height: usize,
1085 layouts: &mut Vec<Vec<InferredVariable>>,
1086 ) -> u32 {
1087 if !self.inlined
1092 && self.last_span == Some(span)
1093 && (self.locals_base.is_some() || self.last_height == height)
1094 {
1095 if let Some(id) = self.layout {
1096 let inputs = &self.layout_inputs;
1097 if inputs.params_base == self.params_base
1098 && inputs.locals_base == self.locals_base
1099 && inputs.returns == self.returns
1100 {
1101 self.last_height = height;
1102 return id;
1103 }
1104 }
1105 }
1106 let id = self.track_span(function, span, height, layouts);
1107 self.last_span = Some(span);
1108 self.last_height = height;
1109 id
1110 }
1111
1112 fn track_span(
1114 &mut self,
1115 function: &SourceFunction,
1116 span: ByteRange,
1117 height: usize,
1118 layouts: &mut Vec<Vec<InferredVariable>>,
1119 ) -> u32 {
1120 if self.inlined {
1121 self.layout = Some(INLINED_LAYOUT);
1122 return INLINED_LAYOUT;
1123 }
1124 let length = span.end - span.start;
1125 let param_slots = self.param_slots;
1126 self.place_from_parameters(function);
1128 let at_beginning = function
1133 .placeholder
1134 .is_none_or(|placeholder| span.start < placeholder);
1135 if !self.body_seen
1136 && at_beginning
1137 && span.start >= function.body_start
1138 && span.start < function.body_end
1139 {
1140 self.body_seen = true;
1141 if self.locals_base.is_none() {
1142 self.locals_base = Some(height);
1143 }
1144 if self.params_base.is_none() && !function.has_modifiers {
1145 self.params_base = height.checked_sub(param_slots + function.returns.len());
1146 }
1147 }
1148 for (index, parameter) in function.returns.iter().enumerate() {
1151 if self.returns[index].is_none() && parameter.declaration.covers(span.start, length) {
1152 self.returns[index] = Some(height);
1153 if index == 0 && self.params_base.is_none() {
1154 self.params_base = height.checked_sub(param_slots);
1155 }
1156 }
1157 }
1158 if self.locals_base.is_none() {
1161 if let Some((index, _)) = function
1162 .locals
1163 .iter()
1164 .enumerate()
1165 .find(|(_, local)| local.statement.covers(span.start, length))
1166 {
1167 self.locals_base = height.checked_sub(live_index(function, index, span.start));
1168 }
1169 }
1170 self.place_from_parameters(function);
1172
1173 let inputs = &self.layout_inputs;
1176 if let Some(id) = self.layout {
1177 if inputs.params_base == self.params_base
1178 && inputs.locals_base == self.locals_base
1179 && inputs.returns == self.returns
1180 && inputs.in_scope.len() == function.locals.len()
1181 && function
1182 .locals
1183 .iter()
1184 .zip(&inputs.in_scope)
1185 .all(|(local, in_scope)| local.scope.contains(span.start) == *in_scope)
1186 {
1187 debug_assert_eq!(
1188 layouts[id as usize],
1189 self.layout_at(function, span.start),
1190 "a cache hit must give the layout its inputs compute"
1191 );
1192 return id;
1193 }
1194 }
1195
1196 let layout = self.layout_at(function, span.start);
1197 let id = match self.layout {
1198 Some(id) if layouts[id as usize] == layout => id,
1199 _ => {
1200 layouts.push(layout);
1201 (layouts.len() - 1) as u32
1202 }
1203 };
1204 self.layout = Some(id);
1205 let inputs = &mut self.layout_inputs;
1206 inputs.params_base = self.params_base;
1207 inputs.returns.clone_from(&self.returns);
1208 inputs.locals_base = self.locals_base;
1209 inputs.in_scope.clear();
1210 inputs.in_scope.extend(
1211 function
1212 .locals
1213 .iter()
1214 .map(|local| local.scope.contains(span.start)),
1215 );
1216 id
1217 }
1218
1219 fn layout_at(&self, function: &SourceFunction, offset: u64) -> Vec<InferredVariable> {
1222 let mut layout = Vec::new();
1223 if let Some(base) = self.params_base {
1224 let mut slot = base;
1225 for parameter in &function.params {
1226 let size = parameter_stack_slots(parameter);
1227 layout.push(InferredVariable {
1228 name: parameter.name.clone(),
1229 ty: match ¶meter.location {
1230 Some(location) => format!("{} {location}", parameter.ty),
1231 None => parameter.ty.clone(),
1232 },
1233 kind: VariableKind::Parameter,
1234 slot: Some(slot),
1235 words: size,
1236 });
1237 slot += size;
1238 }
1239 }
1240 for (index, parameter) in function.returns.iter().enumerate() {
1241 layout.push(InferredVariable {
1242 name: parameter.name.clone(),
1243 ty: parameter.declared_type(),
1244 kind: VariableKind::Return,
1245 slot: self.returns[index],
1246 words: 1,
1247 });
1248 }
1249 for (index, local) in function.locals.iter().enumerate() {
1250 if local.scope.contains(offset) {
1251 layout.push(InferredVariable {
1252 name: local.name.clone(),
1253 ty: local.declared_type(),
1254 kind: VariableKind::Local,
1255 slot: self
1256 .locals_base
1257 .map(|base| base + live_index(function, index, offset)),
1258 words: 1,
1259 });
1260 }
1261 }
1262 layout
1263 }
1264}
1265
1266fn live_index(function: &SourceFunction, index: usize, offset: u64) -> usize {
1269 let declaration = function.locals[index].declaration.start;
1270 function
1271 .locals
1272 .iter()
1273 .filter(|other| other.scope.contains(offset) && other.declaration.start < declaration)
1274 .count()
1275}
1276
1277pub(crate) fn calldata_for_step<'a>(
1281 trace: &'a TransactionTrace,
1282 roots: &[RootCall],
1283 step: usize,
1284) -> &'a str {
1285 let root_input = roots
1286 .get(root_index(roots, step))
1287 .map_or("", |root| root.input(trace));
1288 frame_calldata(trace, step, root_input)
1289}
1290
1291fn frame_calldata<'a>(trace: &'a TransactionTrace, step: usize, root_input: &'a str) -> &'a str {
1293 let root_depth = trace.steps.first().map_or(0, |first| first.depth());
1294 let depth = trace
1295 .steps
1296 .get(step)
1297 .map_or(root_depth, |current| current.depth());
1298 if depth == root_depth {
1299 return root_input;
1300 }
1301 trace
1302 .artifacts
1303 .calls
1304 .iter()
1305 .filter(|call| {
1306 call.entry_step.is_some_and(|entry| entry <= step)
1307 && call.exit_step.is_none_or(|exit| step < exit)
1308 })
1309 .max_by_key(|call| call.depth)
1310 .map_or("", |call| call.input.as_str())
1311}
1312
1313fn parameter_stack_slots(parameter: &SourceParam) -> usize {
1317 let dynamic =
1318 parameter.ty == "bytes" || parameter.ty == "string" || parameter.ty.ends_with("[]");
1319 if parameter.location.as_deref() == Some("calldata") && dynamic
1320 || parameter.ty.starts_with("function")
1321 {
1322 2
1323 } else {
1324 1
1325 }
1326}
1327
1328fn parameter_slots(parameters: &[SourceParam]) -> usize {
1329 parameters.iter().map(parameter_stack_slots).sum()
1330}
1331
1332fn modifier_slots(
1337 contract: &ContractDebugInfo,
1338 function: &SourceFunction,
1339 count: usize,
1340) -> Option<usize> {
1341 let mut slots = 0;
1342 for name in function.modifiers.iter().take(count) {
1343 let modifier = contract.functions.iter().find(|candidate| {
1344 candidate.placeholder.is_some()
1345 && candidate.name == *name
1346 && candidate.source_id == function.source_id
1347 });
1348 let modifier = modifier.or_else(|| {
1349 contract
1350 .functions
1351 .iter()
1352 .find(|candidate| candidate.placeholder.is_some() && candidate.name == *name)
1353 })?;
1354 let placeholder = modifier.placeholder?;
1355 let live = modifier
1356 .locals
1357 .iter()
1358 .filter(|local| {
1359 local.declaration.start < placeholder && local.scope.contains(placeholder)
1360 })
1361 .count();
1362 slots += parameter_slots(&modifier.params) + live;
1363 }
1364 Some(slots)
1365}
1366
1367fn locals_support(contract: &ContractDebugInfo, evidence: Evidence) -> Result<(), &'static str> {
1369 match (contract.code_generator, evidence.layout()) {
1370 (_, Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))) => Err(
1371 "this trace shows the via-IR calling convention, whose stack layout cannot be \
1372 recovered without compiler-reported variable locations",
1373 ),
1374 (Some(CodeGenerator::ViaIr), _) => Err(
1375 "this contract was compiled through the via-IR pipeline, whose stack layout cannot \
1376 be recovered without compiler-reported variable locations",
1377 ),
1378 (Some(CodeGenerator::Legacy), _) | (None, Some(ArgumentLayout::Ordered(_))) => Ok(()),
1379 (None, _) => Err(
1380 "the code generator is not known; only solc's legacy pipeline keeps variables at \
1381 fixed stack slots",
1382 ),
1383 }
1384}
1385
1386#[derive(Default)]
1388struct InternalFrame {
1389 functions: Vec<FrameEntry>,
1391 named: usize,
1393 named_below: usize,
1396 statement: Option<LocationRef>,
1398 remembered: FxHashMap<usize, FrameVariables>,
1401}
1402
1403impl InternalFrame {
1404 fn active(&self) -> Option<usize> {
1406 self.functions.iter().rev().find_map(|entry| entry.function)
1407 }
1408
1409 fn is_active(&self, function: usize) -> bool {
1410 self.active() == Some(function)
1411 }
1412
1413 fn push_function(
1414 &mut self,
1415 function: usize,
1416 return_pc: Option<u64>,
1417 variables: Option<FrameVariables>,
1418 ) {
1419 self.functions.push(FrameEntry {
1420 function: Some(function),
1421 return_pc,
1422 variables,
1423 });
1424 self.named += 1;
1425 }
1426
1427 fn pop(&mut self) {
1429 if self
1430 .functions
1431 .pop()
1432 .is_some_and(|entry| entry.function.is_some())
1433 {
1434 self.named -= 1;
1435 }
1436 }
1437
1438 fn clear(&mut self) {
1440 self.functions.clear();
1441 self.named = 0;
1442 }
1443
1444 fn depth(&self) -> usize {
1446 self.named_below + self.named
1447 }
1448
1449 fn push_placeholder(&mut self) {
1450 self.functions.push(FrameEntry {
1451 function: None,
1452 return_pc: None,
1453 variables: None,
1454 });
1455 }
1456
1457 fn returns_to(&self, pc: u64) -> bool {
1459 self.functions
1460 .last()
1461 .is_some_and(|entry| entry.return_pc == Some(pc))
1462 }
1463}
1464
1465#[allow(clippy::too_many_arguments)]
1474fn place_at_entry_landing(
1475 frame: &mut InternalFrame,
1476 contract: &ContractDebugInfo,
1477 function: usize,
1478 trace: &TransactionTrace,
1479 root_input: &str,
1480 index: usize,
1481 own: LocationRef,
1482 height: usize,
1483 from_call: bool,
1484) {
1485 let Some(declared) = contract.functions.get(function) else {
1486 return;
1487 };
1488 let at_declaration = contract.is_jumpdest(trace.steps.step(index).pc())
1489 && own.offset == declared.declaration_start;
1490 let from_dispatcher =
1491 !from_call || (index > 0 && framed_declaration(trace, contract, index - 1, declared));
1492 if !at_declaration || !from_dispatcher {
1493 return;
1494 }
1495 let entry = frame
1496 .functions
1497 .iter_mut()
1498 .rev()
1499 .find(|entry| entry.function == Some(function));
1500 let Some(variables) = entry.and_then(|entry| entry.variables.as_mut()) else {
1501 return;
1502 };
1503 if variables.params_base.is_some() || variables.body_seen {
1504 return;
1505 }
1506 let stack = trace.steps.step(index).stack().to_vec();
1507 let calldata = frame_calldata(trace, index, root_input);
1508 if argument_evidence(declared, calldata, &stack) == Evidence::Contradicted {
1509 return;
1510 }
1511 variables.params_base = height.checked_sub(parameter_slots(&declared.params));
1512}
1513
1514fn framed_declaration(
1517 trace: &TransactionTrace,
1518 contract: &ContractDebugInfo,
1519 index: usize,
1520 function: &SourceFunction,
1521) -> bool {
1522 let step = trace.steps.step(index);
1523 if step.op() != opcode::JUMP {
1524 return false;
1525 }
1526 contract.location_at_pc(step.pc()).is_some_and(|location| {
1527 location.source_id == function.source_id && location.offset == function.declaration_start
1528 })
1529}
1530
1531fn modifier_base(
1535 frame: &InternalFrame,
1536 contract: &ContractDebugInfo,
1537 function: usize,
1538) -> Option<usize> {
1539 let modifier = contract.functions.get(function)?;
1540 modifier.placeholder?;
1541 let parent = frame
1542 .functions
1543 .iter()
1544 .rev()
1545 .find(|entry| entry.function.is_some())?;
1546 let parent_function = contract.functions.get(parent.function?)?;
1547 let position = parent_function
1548 .modifiers
1549 .iter()
1550 .position(|name| *name == modifier.name)?;
1551 let parent_base = parent.variables.as_ref()?.params_base?;
1552 let before = modifier_slots(contract, parent_function, position)?;
1553 Some(
1554 parent_base
1555 + parameter_slots(&parent_function.params)
1556 + parent_function.returns.len()
1557 + before,
1558 )
1559}
1560
1561impl StepMap {
1562 #[must_use]
1571 pub fn new(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1572 Self::build(trace, contracts, true)
1573 }
1574
1575 #[must_use]
1580 pub fn for_debug_diff(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1581 Self::build(trace, contracts, false)
1582 }
1583
1584 fn build(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>, smooth: bool) -> Self {
1585 let mut addresses = Vec::<String>::new();
1586 let mut address_index = FxHashMap::<String, usize>::default();
1587 let mut intern = |address: &str| -> usize {
1588 let address = normalize_address(address);
1589 *address_index.entry(address.clone()).or_insert_with(|| {
1590 addresses.push(address);
1591 addresses.len() - 1
1592 })
1593 };
1594 let roots = root_calls(trace);
1600 let single_root = roots.len() == 1;
1601 let contract_for =
1602 |address: Option<&str>, environment: CodeEnvironment, root: bool| -> Option<usize> {
1603 let address = address.map(normalize_address);
1604 if let Some(address) = &address {
1605 let mut at_address = contracts
1606 .iter()
1607 .enumerate()
1608 .filter(|(_, contract)| contract.address.as_deref() == Some(address));
1609 let exact = at_address.clone().find(|(_, contract)| {
1610 contract
1611 .code_environment()
1612 .is_none_or(|candidate| candidate == environment)
1613 });
1614 if let Some((index, _)) = exact.or_else(|| at_address.next()) {
1615 return Some(index);
1616 }
1617 }
1618 (root && single_root && contracts.len() == 1).then_some(0)
1619 };
1620
1621 let root_depth = trace.steps.first().map_or(0, |step| step.depth());
1623 let mut next_frame_id = 0_u32;
1624 let mut evm_frames = Vec::<EvmFrame>::new();
1625 let mut upcoming_roots = roots.iter().enumerate().peekable();
1626 let mut current_root = 0;
1627 let mut reverted = Vec::new();
1628 let mut framed = Vec::with_capacity(trace.steps.len());
1629 let recorded = FrameEntered::recorded(trace);
1630 for (index, step) in trace.steps.iter().enumerate() {
1631 let evm_depth = step.depth().saturating_sub(root_depth) as usize;
1632 if let Some((position, root)) =
1633 upcoming_roots.next_if(|(_, root)| root.entry_step == index)
1634 {
1635 current_root = position;
1636 let environment = if root.creation(trace) {
1638 CodeEnvironment::Create
1639 } else {
1640 CodeEnvironment::Call
1641 };
1642 let root_address = root.address(trace);
1643 let address = root_address.map(&mut intern);
1644 evm_frames.clear();
1645 evm_frames.push(EvmFrame {
1646 id: next_frame_id,
1647 contract: contract_for(root_address, environment, true),
1648 address,
1649 storage: address,
1650 entry_step: index,
1651 });
1652 next_frame_id += 1;
1653 }
1654 evm_frames.truncate(evm_depth + 1);
1655 while evm_frames.len() <= evm_depth {
1656 let call = (evm_frames.len() == evm_depth)
1661 .then(|| index.checked_sub(1))
1662 .flatten();
1663 let entered = recorded
1664 .get(&index)
1665 .filter(|_| evm_frames.len() == evm_depth)
1666 .cloned()
1667 .unwrap_or_else(|| FrameEntered::from_call(trace, call));
1668 let address = entered.address.as_deref().map(&mut intern);
1669 let storage = if entered.delegated {
1672 evm_frames.last().and_then(|frame| frame.storage)
1673 } else {
1674 address
1675 };
1676 evm_frames.push(EvmFrame {
1677 id: next_frame_id,
1678 contract: contract_for(entered.address.as_deref(), entered.environment, false),
1679 address,
1680 storage,
1681 entry_step: index,
1682 });
1683 next_frame_id += 1;
1684 }
1685 if step.op() == opcode::REVERT || step.error().is_some() {
1686 let entry_step = evm_frames
1687 .last()
1688 .expect("the root frame is never popped")
1689 .entry_step;
1690 reverted.push((entry_step, index));
1691 }
1692 let frame = evm_frames.last().expect("the root frame is never popped");
1693 let facts = frame
1694 .contract
1695 .and_then(|index| contracts[index].facts_at_pc(step.pc()));
1696 let location = frame.contract.zip(facts).and_then(|(contract, facts)| {
1697 let location = facts.location.as_ref()?;
1698 Some(LocationRef {
1699 key: LineKey {
1700 contract,
1701 source_id: location.source_id,
1702 line: facts.line?,
1703 },
1704 offset: location.offset,
1705 length: location.length,
1706 function: facts.function,
1707 })
1708 });
1709 framed.push(FramedStep {
1710 evm_depth: evm_depth as u32,
1711 frame_id: frame.id,
1712 root: current_root,
1713 contract: frame.contract,
1714 address: frame.address,
1715 storage: frame.storage,
1716 location,
1717 entry: facts.and_then(|facts| facts.entry),
1718 marker: facts.map_or(JumpMarker::None, |facts| facts.marker),
1719 });
1720 }
1721
1722 let structured = |contract: Option<usize>| {
1726 contract.is_some_and(|index| !contracts[index].functions.is_empty())
1727 };
1728 let mut function_follows = vec![false; next_frame_id as usize];
1730 let mut epilogue = vec![false; framed.len()];
1731 for (index, step) in framed.iter().enumerate().rev() {
1732 match step.location.map(|location| location.function) {
1733 Some(Some(_)) => {
1734 function_follows[step.frame_id as usize] = true;
1735 }
1736 Some(None) if structured(step.contract) => {
1737 epilogue[index] = !function_follows[step.frame_id as usize];
1738 }
1739 _ => {}
1740 }
1741 }
1742
1743 let argument_layouts = prove_argument_layouts(trace, &contracts, &framed);
1744 let locals_support = contracts
1745 .iter()
1746 .zip(&argument_layouts)
1747 .map(|(contract, evidence)| locals_support(contract, *evidence))
1748 .collect::<Vec<_>>();
1749
1750 let mut internal = Vec::<InternalFrame>::new();
1753 let mut steps = Vec::with_capacity(framed.len());
1754 let mut pcs = Vec::with_capacity(framed.len());
1755 let mut variable_layouts = Vec::<Vec<InferredVariable>>::new();
1756 let mut step_variables = Vec::with_capacity(framed.len());
1757 for (index, step) in framed.iter().enumerate() {
1758 let height = trace.steps.step(index).stack().len();
1759 let frame_variables = |function: usize| -> Option<FrameVariables> {
1764 let contract_index = step.contract?;
1765 locals_support[contract_index].ok()?;
1766 let contract = &contracts[contract_index];
1767 let function = contract.functions.get(function)?;
1768 let landed_from_function = index > 0
1769 && framed[index - 1].frame_id == step.frame_id
1770 && trace.steps.step(index - 1).op() == opcode::JUMP
1771 && framed[index - 1]
1772 .location
1773 .is_some_and(|location| location.function.is_some());
1774 let params_base = landed_from_function
1775 .then(|| height.checked_sub(parameter_slots(&function.params)))
1776 .flatten();
1777 Some(FrameVariables::new(
1778 function,
1779 params_base,
1780 modifier_slots(contract, function, function.modifiers.len()),
1781 ))
1782 };
1783 let contract_info = step.contract.map(|index| &contracts[index]);
1784 let evm_depth = step.evm_depth as usize;
1785 let mut frame_entry = false;
1786 let new_root = index > 0 && framed[index - 1].root != step.root;
1788 let root_input = roots[step.root].input(trace);
1789 internal.truncate(if new_root { evm_depth } else { evm_depth + 1 });
1790 while internal.len() <= evm_depth {
1791 let named_below = internal.last().map_or(0, InternalFrame::depth);
1792 internal.push(InternalFrame {
1793 named_below,
1794 ..InternalFrame::default()
1795 });
1796 frame_entry = true;
1797 }
1798 let frame = internal.last_mut().expect("the root frame is never popped");
1799
1800 let landed_by_jump = index > 0
1803 && framed[index - 1].frame_id == step.frame_id
1804 && trace.steps.step(index - 1).op() == opcode::JUMP;
1805 let from_function_code = index > 0
1808 && framed[index - 1].frame_id == step.frame_id
1809 && framed[index - 1].location.is_some_and(|location| {
1810 location.function.is_some()
1811 && location.function != step.location.and_then(|own| own.function)
1812 });
1813 let previous_marker = if landed_by_jump {
1814 framed[index - 1].marker
1815 } else {
1816 JumpMarker::None
1817 };
1818 let structured = structured(step.contract);
1819 let mut generated = false;
1820 let mut pending_call = false;
1824 if landed_by_jump {
1825 let pc = trace.steps.step(index).pc();
1826 match previous_marker {
1827 JumpMarker::Call => pending_call = true,
1828 JumpMarker::Return => frame.pop(),
1829 JumpMarker::None if frame.returns_to(pc) => frame.pop(),
1830 JumpMarker::None => {}
1831 }
1832 }
1833 let return_address = |function: usize| -> Option<u64> {
1836 if !landed_by_jump {
1837 return None;
1838 }
1839 let contract = &contracts[step.contract?];
1840 let arguments = contract.functions.get(function)?.params.len();
1841 let word = trace.steps.step(index - 1).stack().peek(1 + arguments)?;
1842 let pc = word.to_u64()?;
1843 contract.is_jumpdest(pc).then_some(pc)
1844 };
1845 let (location, key) = match step.location {
1846 None => {
1847 if pending_call {
1850 frame.push_placeholder();
1851 }
1852 (None, None)
1853 }
1854 Some(own) if !structured => {
1855 frame.statement = Some(own);
1857 (Some(own), Some(own.key))
1858 }
1859 Some(own) => match own.function {
1860 Some(function) => {
1861 let entering = landed_by_jump && step.entry == Some(function);
1862 if landed_by_jump
1866 && previous_marker == JumpMarker::Return
1867 && frame.is_active(function)
1868 {
1869 if let Some(contract) = contract_info {
1870 place_at_entry_landing(
1871 frame, contract, function, trace, root_input, index, own,
1872 height, false,
1873 );
1874 }
1875 }
1876 if entering || (pending_call && !frame.is_active(function)) {
1877 frame.push_function(
1880 function,
1881 return_address(function),
1882 frame_variables(function),
1883 );
1884 frame_entry = true;
1885 } else if pending_call {
1886 if let Some(contract) = contract_info {
1892 place_at_entry_landing(
1893 frame, contract, function, trace, root_input, index, own,
1894 height, true,
1895 );
1896 }
1897 frame.push_placeholder();
1898 } else if !frame.is_active(function) {
1899 match frame
1900 .functions
1901 .iter()
1902 .rposition(|entry| entry.function == Some(function))
1903 {
1904 Some(position) => {
1905 frame.named -= frame.functions[position + 1..]
1908 .iter()
1909 .filter(|entry| entry.function.is_some())
1910 .count();
1911 for left in frame.functions.drain(position + 1..) {
1912 if let (Some(function), Some(variables)) =
1913 (left.function, left.variables)
1914 {
1915 frame.remembered.insert(function, variables);
1916 }
1917 }
1918 }
1919 None => {
1920 let mut variables = frame.remembered.remove(&function);
1921 if variables.is_none() {
1922 variables = frame_variables(function);
1923 if let (Some(variables), Some(contract)) =
1924 (variables.as_mut(), contract_info)
1925 {
1926 let declared = &contract.functions[function];
1927 if declared.placeholder.is_some() {
1928 if variables.params_base.is_none() {
1934 variables.params_base =
1935 modifier_base(frame, contract, function);
1936 }
1937 } else if declared.name != "constructor"
1938 && from_function_code
1939 && step.entry != Some(function)
1940 {
1941 variables.inlined = true;
1945 }
1946 }
1947 }
1948 frame.push_function(function, None, variables);
1949 frame_entry = true;
1950 }
1951 }
1952 }
1953 frame.statement = Some(own);
1954 (Some(own), Some(own.key))
1955 }
1956 None if epilogue[index] => {
1957 frame.clear();
1958 frame.statement = None;
1959 (Some(own), None)
1960 }
1961 None => {
1962 if pending_call {
1963 frame.push_placeholder();
1964 }
1965 match frame.statement {
1966 Some(statement) => {
1967 generated = true;
1968 (Some(statement), Some(statement.key))
1969 }
1970 None => (Some(own), None),
1973 }
1974 }
1975 },
1976 };
1977 let layout = {
1980 let frame = internal.last_mut().expect("the root frame is never popped");
1981 let contract = step.contract.map(|index| &contracts[index]);
1982 let active = frame
1983 .functions
1984 .iter()
1985 .rposition(|entry| entry.function.is_some());
1986 match (active, step.location, contract) {
1987 (Some(position), Some(own), Some(contract))
1988 if own.function.is_some()
1989 && own.function == frame.functions[position].function =>
1990 {
1991 let function_index = own.function.unwrap_or_default();
1992 let function = &contract.functions[function_index];
1993 let span = ByteRange {
1994 start: own.offset,
1995 end: own.offset + own.length,
1996 };
1997 let (below, rest) = frame.functions.split_at_mut(position);
1998 let entry = &mut rest[0];
1999 let layout = entry.variables.as_mut().map(|variables| {
2000 variables.track(function, span, height, &mut variable_layouts)
2001 });
2002 let modifier_base = entry
2007 .variables
2008 .as_ref()
2009 .and_then(|variables| variables.params_base);
2010 let parent = below
2011 .iter_mut()
2012 .rev()
2013 .find(|entry| entry.function.is_some());
2014 if let (Some(modifier_base), Some(parent)) = (modifier_base, parent) {
2015 let parent_function =
2016 &contract.functions[parent.function.unwrap_or_default()];
2017 let position = parent_function
2018 .modifiers
2019 .iter()
2020 .position(|name| *name == function.name);
2021 if let (Some(position), Some(variables)) =
2022 (position, parent.variables.as_mut())
2023 {
2024 if variables.params_base.is_none() {
2025 variables.params_base =
2026 modifier_slots(contract, parent_function, position)
2027 .and_then(|before| {
2028 modifier_base.checked_sub(
2029 parameter_slots(&parent_function.params)
2030 + parent_function.returns.len()
2031 + before,
2032 )
2033 });
2034 }
2035 }
2036 }
2037 layout
2038 }
2039 (Some(position), _, _) => frame.functions[position]
2040 .variables
2041 .as_ref()
2042 .and_then(|variables| variables.layout),
2043 _ => None,
2044 }
2045 };
2046 step_variables.push(layout.unwrap_or(NO_LAYOUT));
2047 let internal_total = internal.last().map_or(0, InternalFrame::depth);
2048 steps.push(StepInfo {
2049 location,
2050 generated,
2051 key,
2052 contract: step.contract,
2053 address: step.address,
2054 storage: step.storage,
2055 evm_depth: step.evm_depth,
2056 starts_root: new_root,
2057 frame_depth: (evm_depth + internal_total) as u32,
2058 frame_entry,
2059 line_start: false,
2060 });
2061 pcs.push(trace.steps.step(index).pc());
2062 }
2063
2064 let mut map = Self {
2065 contracts,
2066 steps,
2067 addresses,
2068 pcs,
2069 reverted,
2070 argument_layouts,
2071 locals_support,
2072 variable_layouts,
2073 step_variables,
2074 roots,
2075 };
2076 if smooth {
2077 Self::smooth_single_step_excursions(&mut map.steps);
2078 }
2079 map.mark_line_starts();
2080 map
2081 }
2082
2083 fn smooth_single_step_excursions(steps: &mut [StepInfo]) {
2091 let mut after = vec![None; steps.len()];
2094 let mut lines = NearestLines::default();
2095 for (index, step) in steps.iter().enumerate().rev() {
2096 let own = step.key.and_then(|_| u32::try_from(index).ok());
2097 after[index] = lines.visit(step.frame_depth, own);
2098 if step.starts_root {
2100 lines.clear();
2101 }
2102 }
2103 let mut lines = NearestLines::default();
2104 for (index, after) in after.into_iter().enumerate() {
2105 let step = &steps[index];
2106 if step.starts_root {
2107 lines.clear();
2108 }
2109 let before = lines.visit(step.frame_depth, step.key);
2110 let Some(key) = step.key else {
2111 continue;
2112 };
2113 let after = after.and_then(|after| steps[after as usize].key);
2114 if before.is_some() && before == after && before != Some(key) {
2115 steps[index].key = before;
2116 }
2117 }
2118 }
2119
2120 fn mark_line_starts(&mut self) {
2123 let mut runs = Vec::<(usize, Option<LineKey>)>::new();
2127 let mut depths = 0;
2128 for step in &mut self.steps {
2129 if step.starts_root {
2130 runs.clear();
2131 depths = 0;
2132 }
2133 let depth = step.frame_depth as usize;
2134 if depths <= depth {
2135 if runs.last().is_none_or(|(_, key)| key.is_some()) {
2137 runs.push((depths, None));
2138 }
2139 depths = depth + 1;
2140 }
2141 let Some(key) = step.key else {
2142 step.line_start = false;
2143 continue;
2144 };
2145 while runs.last().is_some_and(|(from, _)| *from > depth) {
2146 runs.pop();
2147 }
2148 let last = runs.last_mut().expect("a run starts at depth zero");
2149 step.line_start = last.1 != Some(key);
2150 if last.0 == depth {
2151 last.1 = Some(key);
2152 } else {
2153 runs.push((depth, Some(key)));
2154 }
2155 }
2156 }
2157
2158 #[must_use]
2159 pub fn contracts(&self) -> &[ContractDebugInfo] {
2160 &self.contracts
2161 }
2162
2163 #[must_use]
2165 pub fn contract_at_step(&self, step: usize) -> Option<&ContractDebugInfo> {
2166 self.contracts.get(self.steps.get(step)?.contract?)
2167 }
2168
2169 #[must_use]
2170 pub fn step_count(&self) -> usize {
2171 self.steps.len()
2172 }
2173
2174 #[must_use]
2176 pub fn has_source(&self) -> bool {
2177 self.steps.iter().any(|step| step.location.is_some())
2178 }
2179
2180 #[must_use]
2182 pub fn line_key(&self, step: usize) -> Option<LineKey> {
2183 self.steps.get(step)?.key
2184 }
2185
2186 #[must_use]
2187 pub fn frame_depth(&self, step: usize) -> Option<u32> {
2188 self.steps.get(step).map(|step| step.frame_depth)
2189 }
2190
2191 #[must_use]
2192 pub fn is_line_start(&self, step: usize) -> bool {
2193 self.steps.get(step).is_some_and(|step| step.line_start)
2194 }
2195
2196 #[must_use]
2197 pub fn is_frame_entry(&self, step: usize) -> bool {
2198 self.steps.get(step).is_some_and(|step| step.frame_entry)
2199 }
2200
2201 #[must_use]
2203 pub fn function_id(&self, step: usize) -> Option<FunctionId> {
2204 let location = self.steps.get(step)?.location?;
2205 location.function.map(|function| FunctionId {
2206 contract: location.key.contract,
2207 function,
2208 })
2209 }
2210
2211 #[must_use]
2213 pub fn executing_address(&self, step: usize) -> Option<&str> {
2214 let index = self.steps.get(step)?.address?;
2215 self.addresses.get(index).map(String::as_str)
2216 }
2217
2218 #[must_use]
2221 pub fn storage_address(&self, step: usize) -> Option<&str> {
2222 let index = self.storage_context_index(step)?;
2223 self.addresses.get(index).map(String::as_str)
2224 }
2225
2226 #[must_use]
2228 pub fn storage_context_index(&self, step: usize) -> Option<usize> {
2229 self.steps.get(step)?.storage
2230 }
2231
2232 #[must_use]
2235 pub fn reverted_spans(&self) -> &[(usize, usize)] {
2236 &self.reverted
2237 }
2238
2239 #[must_use]
2243 pub fn argument_layout(&self, contract: usize) -> Option<ArgumentLayout> {
2244 self.argument_layouts.get(contract).copied()?.layout()
2245 }
2246
2247 #[must_use]
2257 pub fn locals_at(&self, step: usize) -> LocalsStatus<'_> {
2258 let Some(info) = self.steps.get(step) else {
2259 return LocalsStatus::Unavailable("the step is outside the trace");
2260 };
2261 let Some(contract) = info.contract else {
2262 return LocalsStatus::Unavailable(
2263 "no sources matched the contract executing at this step",
2264 );
2265 };
2266 if let Err(reason) = self.locals_support[contract] {
2267 return LocalsStatus::Unavailable(reason);
2268 }
2269 match self.step_variables.get(step).copied() {
2270 Some(INLINED_LAYOUT) => LocalsStatus::Unavailable(
2271 "this function was inlined by the optimizer, so its variables have no stack \
2272 slots of their own",
2273 ),
2274 Some(id) if id != NO_LAYOUT => {
2275 LocalsStatus::Inferred(&self.variable_layouts[id as usize])
2276 }
2277 _ => LocalsStatus::Unavailable("no function is executing at this step"),
2278 }
2279 }
2280
2281 #[must_use]
2284 pub fn calldata_at<'a>(&self, trace: &'a TransactionTrace, step: usize) -> &'a str {
2285 calldata_for_step(trace, &self.roots, step)
2286 }
2287
2288 pub(crate) fn scope_at_step(&self, step: usize) -> Option<&str> {
2291 let info = self.steps.get(step)?;
2292 let contract = self.contracts.get(info.contract?)?;
2293 let function = contract.functions.get(info.location?.function?)?;
2294 contract
2295 .types
2296 .scope_at(function.source_id, function.declaration_start)
2297 }
2298
2299 #[must_use]
2305 pub fn local_path(
2306 &self,
2307 trace: &TransactionTrace,
2308 step: usize,
2309 words: Option<&StorageWords<'_>>,
2310 path: &str,
2311 ) -> Option<Result<DebugVariable, String>> {
2312 let found = match self.local_place(trace, step, words, path)? {
2313 Ok(found) => found,
2314 Err(reason) => return Some(Err(reason)),
2315 };
2316 let (reader, slot, place) = found;
2317 let (ty, value) = match place {
2318 Ok(place) => (place.ty(), reader.show(&place)),
2319 Err((ty, shown)) => (ty, shown),
2320 };
2321 Some(Ok(DebugVariable {
2322 name: path.trim().to_owned(),
2323 ty,
2324 location: DebugLocation {
2325 kind: "stack".to_owned(),
2326 offset: slot as u64,
2327 },
2328 value,
2329 }))
2330 }
2331
2332 #[must_use]
2335 pub fn local_condition_value(
2336 &self,
2337 trace: &TransactionTrace,
2338 step: usize,
2339 words: Option<&StorageWords<'_>>,
2340 path: &str,
2341 ) -> Option<Result<Value, String>> {
2342 let (reader, _, place) = match self.local_place(trace, step, words, path)? {
2343 Ok(found) => found,
2344 Err(reason) => return Some(Err(reason)),
2345 };
2346 Some(match place {
2347 Ok(place) => reader.condition_value(&place, path.trim()),
2348 Err((_, shown)) => Err(format!("`{}` is {}", path.trim(), shown.display)),
2349 })
2350 }
2351
2352 #[allow(clippy::type_complexity)]
2357 fn local_place<'a>(
2358 &'a self,
2359 trace: &'a TransactionTrace,
2360 step: usize,
2361 words: Option<&'a StorageWords<'a>>,
2362 path: &str,
2363 ) -> Option<Result<(ValueReader<'a>, usize, Result<Place, (String, DebugValue)>), String>> {
2364 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
2365 return None;
2366 };
2367 let segments = parse_path(path).ok()?;
2368 let Some(PathSegment::Name(name)) = segments.first() else {
2369 return None;
2370 };
2371 let variable = layout
2373 .iter()
2374 .rev()
2375 .find(|variable| variable.name == *name)?;
2376 let contract = self.contract_at_step(step)?;
2377 let view = trace.steps.get(step);
2378 let stack = view.map(|view| view.stack().to_vec()).unwrap_or_default();
2379 let reader = ValueReader {
2380 memory: view.and_then(|view| view.memory()),
2381 calldata: self.calldata_at(trace, step),
2382 storage: words,
2383 layout: contract.storage_layout.as_ref(),
2384 types: &contract.types,
2385 scope: self.scope_at_step(step),
2386 };
2387 let Some(slot) = variable.slot else {
2388 return Some(Err(format!(
2389 "`{name}` is in scope but its stack slot could not be placed here"
2390 )));
2391 };
2392 let Some(stack_words) = stack.get(slot..slot + variable.words) else {
2393 return Some(Err(format!(
2394 "`{name}` is in scope but its stack slot is above the stack here"
2395 )));
2396 };
2397 let stack_words = stack_words
2398 .iter()
2399 .map(EvmWord::to_string)
2400 .collect::<Vec<_>>();
2401 let stack_words = stack_words.iter().map(String::as_str).collect::<Vec<_>>();
2402 let mut place = match reader.root(&variable.ty, &stack_words) {
2403 Ok(place) => place,
2404 Err(shown) if segments.len() == 1 => {
2405 return Some(Ok((reader, slot, Err((variable.ty.clone(), shown)))));
2406 }
2407 Err(shown) => return Some(Err(format!("`{name}` is {}", shown.display))),
2408 };
2409 let mut so_far = name.clone();
2410 for segment in &segments[1..] {
2411 place = match reader.follow(place, segment, &so_far) {
2412 Ok(place) => place,
2413 Err(reason) => return Some(Err(reason)),
2414 };
2415 match segment {
2416 PathSegment::Member(member) => so_far = format!("{so_far}.{member}"),
2417 PathSegment::Index(key) => so_far = format!("{so_far}[{key}]"),
2418 PathSegment::Name(_) => {}
2419 }
2420 }
2421 Some(Ok((reader, slot, Ok(place))))
2422 }
2423
2424 #[must_use]
2432 pub fn inferred_variables(
2433 &self,
2434 trace: &TransactionTrace,
2435 step: usize,
2436 words: Option<&StorageWords<'_>>,
2437 ) -> Vec<DebugVariable> {
2438 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
2439 return Vec::new();
2440 };
2441 let Some(contract) = self.contract_at_step(step) else {
2442 return Vec::new();
2443 };
2444 let view = trace.steps.get(step);
2445 let stack = view.map(|view| view.stack().to_vec()).unwrap_or_default();
2446 let reader = ValueReader {
2447 memory: view.and_then(|view| view.memory()),
2448 calldata: self.calldata_at(trace, step),
2449 storage: words,
2450 layout: contract.storage_layout.as_ref(),
2451 types: &contract.types,
2452 scope: self.scope_at_step(step),
2453 };
2454 let unavailable = || DebugValue {
2455 display: "<unavailable>".to_owned(),
2456 raw: None,
2457 status: DebugValueStatus::Unavailable,
2458 };
2459 layout
2460 .iter()
2461 .map(|variable| {
2462 let words = variable
2463 .slot
2464 .and_then(|slot| stack.get(slot..slot + variable.words))
2465 .map(|words| words.iter().map(EvmWord::to_string).collect::<Vec<_>>());
2466 let value = match words {
2467 Some(words) => {
2468 let words = words.iter().map(String::as_str).collect::<Vec<_>>();
2469 reader.variable(&variable.ty, &words)
2470 }
2471 None => unavailable(),
2472 };
2473 DebugVariable {
2474 name: variable.name.clone(),
2475 ty: variable.ty.clone(),
2476 location: DebugLocation {
2477 kind: "stack".to_owned(),
2478 offset: variable.slot.unwrap_or_default() as u64,
2479 },
2480 value,
2481 }
2482 })
2483 .collect()
2484 }
2485
2486 #[must_use]
2494 pub fn frame_arguments(&self, frame: &Frame, state: FrameState<'_>) -> Vec<FrameArgument> {
2495 let Some(info) = self.steps.get(frame.entry_step) else {
2496 return Vec::new();
2497 };
2498 let Some(location) = info.location else {
2499 return Vec::new();
2500 };
2501 let Some(function_index) = location.function else {
2502 return Vec::new();
2503 };
2504 let Some(contract) = self.contracts.get(location.key.contract) else {
2505 return Vec::new();
2506 };
2507 if contract.function_entry_at_pc(self.pcs[frame.entry_step]) != Some(function_index) {
2508 return Vec::new();
2509 }
2510 let Some(function) = contract.functions.get(function_index) else {
2511 return Vec::new();
2512 };
2513 let Some(layout) = self.argument_layout(location.key.contract) else {
2514 return Vec::new();
2515 };
2516 decode_arguments(
2517 &function.params,
2518 state,
2519 layout,
2520 &contract.types,
2521 contract
2522 .types
2523 .scope_at(function.source_id, function.declaration_start),
2524 )
2525 }
2526
2527 #[must_use]
2528 pub fn location(&self, step: usize) -> Option<StepLocation> {
2529 let info = self.steps.get(step)?;
2530 self.describe(&info.location?, info.generated)
2531 }
2532
2533 fn describe(&self, location: &LocationRef, generated: bool) -> Option<StepLocation> {
2534 let contract = self.contracts.get(location.key.contract)?;
2535 let function = location
2536 .function
2537 .and_then(|index| contract.functions.get(index));
2538 Some(StepLocation {
2539 key: location.key,
2540 generated,
2541 contract_name: contract.name.clone(),
2542 path: contract
2543 .source_path(location.key.source_id)
2544 .map(str::to_owned)
2545 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2546 offset: location.offset,
2547 length: location.length,
2548 line: location.key.line,
2549 column: contract
2550 .column_of(location.key.source_id, location.offset)
2551 .unwrap_or(0),
2552 function: location.function.map(|function| FunctionId {
2553 contract: location.key.contract,
2554 function,
2555 }),
2556 function_name: function.map(|function| function.name.clone()),
2557 })
2558 }
2559
2560 #[must_use]
2570 pub fn next_source(&self, step: usize) -> Option<usize> {
2571 let current = *self.steps.get(step)?;
2572 if self.outside_functions(¤t) {
2573 return self.step_into(step);
2574 }
2575 let key = current.key;
2576 ((step + 1)..self.steps.len()).find(|&index| {
2577 let candidate = &self.steps[index];
2578 if candidate.frame_depth > current.frame_depth {
2579 return false;
2580 }
2581 candidate.frame_depth < current.frame_depth
2582 || candidate
2583 .key
2584 .is_some_and(|candidate| Some(candidate) != key)
2585 })
2586 }
2587
2588 #[must_use]
2591 pub fn step_into(&self, step: usize) -> Option<usize> {
2592 let current = *self.steps.get(step)?;
2593 let key = current.key;
2594 ((step + 1)..self.steps.len()).find(|&index| {
2595 let candidate = &self.steps[index];
2596 candidate.frame_depth != current.frame_depth
2597 || candidate
2598 .key
2599 .is_some_and(|candidate| Some(candidate) != key)
2600 })
2601 }
2602
2603 #[must_use]
2606 pub fn finish(&self, step: usize) -> Option<usize> {
2607 let current = *self.steps.get(step)?;
2608 ((step + 1)..self.steps.len()).find(|&index| {
2609 let candidate = &self.steps[index];
2610 candidate.frame_depth < current.frame_depth || candidate.starts_root
2611 })
2612 }
2613
2614 #[must_use]
2619 pub fn previous_source(&self, step: usize) -> Option<usize> {
2620 let current = *self.steps.get(step)?;
2621 if self.outside_functions(¤t) {
2622 return self.reverse_step_into(step);
2623 }
2624 self.previous_line_start(step, true)
2625 }
2626
2627 #[must_use]
2629 pub fn reverse_step_into(&self, step: usize) -> Option<usize> {
2630 self.steps.get(step)?;
2631 self.previous_line_start(step, false)
2632 }
2633
2634 fn previous_line_start(&self, step: usize, skip_deeper: bool) -> Option<usize> {
2635 let current = self.steps[step];
2636 let anchor = if current.key.is_some() {
2638 step
2639 } else {
2640 self.previous_mapped(step, current.frame_depth, skip_deeper)?
2641 };
2642 let start = self.run_start(anchor, skip_deeper);
2643 if start < step {
2644 return Some(start);
2645 }
2646 let previous = self.previous_mapped(start, current.frame_depth, skip_deeper)?;
2647 Some(self.run_start(previous, skip_deeper))
2648 }
2649
2650 #[must_use]
2653 pub fn reverse_finish(&self, step: usize) -> Option<usize> {
2654 let current = *self.steps.get(step)?;
2655 (0..step).rev().find(|&index| {
2656 let candidate = &self.steps[index];
2657 candidate.frame_depth < current.frame_depth || candidate.starts_root
2658 })
2659 }
2660
2661 fn run_start(&self, step: usize, skip_deeper: bool) -> usize {
2665 let info = self.steps[step];
2666 let key = info.key;
2667 let root = root_index(&self.roots, step);
2668 let mut current = step;
2669 while let Some(previous) = self.previous_mapped(current, info.frame_depth, skip_deeper) {
2670 let candidate = self.steps[previous];
2671 if candidate.frame_depth != info.frame_depth || candidate.key != key {
2672 break;
2673 }
2674 if root_index(&self.roots, previous) != root {
2675 break;
2676 }
2677 current = previous;
2678 }
2679 current
2680 }
2681
2682 fn outside_functions(&self, info: &StepInfo) -> bool {
2685 info.location.is_some() && info.key.is_none()
2686 }
2687
2688 fn previous_mapped(&self, step: usize, depth: u32, skip_deeper: bool) -> Option<usize> {
2691 (0..step).rev().find(|&index| {
2692 let candidate = &self.steps[index];
2693 candidate.key.is_some() && (!skip_deeper || candidate.frame_depth <= depth)
2694 })
2695 }
2696
2697 pub fn resolve_line(&self, file: Option<&str>, line: u64) -> Result<Vec<ResolvedLine>, String> {
2700 let mut sources = Vec::<(usize, u64, String)>::new();
2701 for (contract_index, contract) in self.contracts.iter().enumerate() {
2702 for (source_id, path) in &contract.info.sources {
2703 if file.is_none_or(|file| source_path_matches(path, file)) {
2704 sources.push((contract_index, *source_id, path.clone()));
2705 }
2706 }
2707 }
2708 if sources.is_empty() {
2709 return Err(match file {
2710 Some(file) => format!("source file not found: {file}"),
2711 None => "no source files are available".to_owned(),
2712 });
2713 }
2714 if file.is_none() {
2715 let mut paths = sources.iter().map(|(_, _, path)| path).collect::<Vec<_>>();
2716 paths.sort();
2717 paths.dedup();
2718 if paths.len() > 1 {
2719 return Err("line breakpoint is ambiguous; use break <file>:<line>".to_owned());
2720 }
2721 }
2722
2723 let mut resolved = sources
2724 .into_iter()
2725 .filter_map(|(contract_index, source_id, path)| {
2726 let effective = self.contracts[contract_index].effective_line(source_id, line)?;
2727 Some(ResolvedLine {
2728 key: LineKey {
2729 contract: contract_index,
2730 source_id,
2731 line: effective,
2732 },
2733 path,
2734 requested_line: line,
2735 })
2736 })
2737 .collect::<Vec<_>>();
2738 if resolved.is_empty() {
2739 return Err(match file {
2740 Some(file) => format!("no instruction maps to {file}:{line}"),
2741 None => format!("no instruction maps to line {line}"),
2742 });
2743 }
2744 if resolved.iter().any(|candidate| candidate.key.line == line) {
2749 resolved.retain(|candidate| candidate.key.line == line);
2750 }
2751 Ok(resolved)
2752 }
2753
2754 pub fn resolve_function(&self, name: &str) -> Result<Vec<ResolvedFunction>, String> {
2757 let (contract_name, function_name) = match name.rsplit_once('.') {
2758 Some((contract, function)) => (Some(contract), function),
2759 None => (None, name),
2760 };
2761 let resolved = self
2762 .contracts
2763 .iter()
2764 .enumerate()
2765 .filter(|(_, contract)| contract_name.is_none_or(|name| contract.name == name))
2766 .flat_map(|(contract_index, contract)| {
2767 contract
2768 .functions
2769 .iter()
2770 .enumerate()
2771 .filter(move |(_, function)| function.name == function_name)
2772 .map(move |(function_index, function)| ResolvedFunction {
2773 id: FunctionId {
2774 contract: contract_index,
2775 function: function_index,
2776 },
2777 name: function.name.clone(),
2778 contract_name: contract.name.clone(),
2779 path: contract
2780 .source_path(function.source_id)
2781 .map(str::to_owned)
2782 .unwrap_or_else(|| format!("source:{}", function.source_id)),
2783 line: function.declaration_line,
2784 })
2785 })
2786 .collect::<Vec<_>>();
2787 if resolved.is_empty() {
2788 return Err(format!("no function named `{name}` in the loaded sources"));
2789 }
2790 Ok(resolved)
2791 }
2792
2793 #[must_use]
2795 pub fn frames(&self, step: usize) -> Vec<Frame> {
2796 struct Record {
2797 entry_step: usize,
2798 last_step: usize,
2799 external: bool,
2800 }
2801 if step >= self.steps.len() {
2802 return Vec::new();
2803 }
2804 let mut stack = Vec::<Record>::new();
2805 for (index, info) in self.steps.iter().enumerate().take(step + 1) {
2806 let depth = info.frame_depth as usize;
2807 stack.truncate(if info.starts_root { 0 } else { depth + 1 });
2808 let entered_call =
2811 index == 0 || info.starts_root || info.evm_depth > self.steps[index - 1].evm_depth;
2812 let mut first_new = true;
2813 while stack.len() <= depth {
2814 stack.push(Record {
2815 entry_step: index,
2816 last_step: index,
2817 external: first_new && entered_call,
2818 });
2819 first_new = false;
2820 }
2821 if let Some(top) = stack.last_mut() {
2822 top.last_step = index;
2823 }
2824 }
2825
2826 stack
2827 .iter()
2828 .enumerate()
2829 .rev()
2830 .map(|(depth, record)| {
2831 let info = self.steps[record.last_step];
2832 let location = info
2833 .location
2834 .and_then(|location| self.describe(&location, info.generated));
2835 Frame {
2836 depth: depth as u32,
2837 external: record.external,
2838 address: info
2839 .address
2840 .and_then(|index| self.addresses.get(index))
2841 .cloned(),
2842 contract_name: info
2843 .contract
2844 .and_then(|index| self.contracts.get(index))
2845 .map(|contract| contract.name.clone()),
2846 function_name: location
2847 .as_ref()
2848 .and_then(|location| location.function_name.clone()),
2849 entry_step: record.entry_step,
2850 step: record.last_step,
2851 pc: self.pcs[record.last_step],
2852 location,
2853 arguments: Vec::new(),
2854 kind: self.root_kind(depth, record.entry_step),
2855 }
2856 })
2857 .collect()
2858 }
2859
2860 fn root_kind(&self, depth: usize, entry_step: usize) -> Option<String> {
2863 if depth != 0 {
2864 return None;
2865 }
2866 let root = self.roots.get(root_index(&self.roots, entry_step))?;
2867 if root.entry_step != entry_step {
2868 return None;
2869 }
2870 root.kind.clone()
2871 }
2872
2873 #[must_use]
2875 pub fn source_listing(&self, step: usize, radius: u64) -> Option<SourceListing> {
2876 let location = self.steps.get(step)?.location?;
2877 let contract = self.contracts.get(location.key.contract)?;
2878 let count = contract.line_count(location.key.source_id)?;
2879 let first = location.key.line.saturating_sub(radius).max(1);
2880 let last = location.key.line.saturating_add(radius).min(count);
2881 let lines = (first..=last)
2882 .filter_map(|line| {
2883 contract
2884 .line_text(location.key.source_id, line)
2885 .map(|text| (line, text.to_owned()))
2886 })
2887 .collect();
2888 Some(SourceListing {
2889 path: contract
2890 .source_path(location.key.source_id)
2891 .map(str::to_owned)
2892 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2893 current_line: location.key.line,
2894 lines,
2895 })
2896 }
2897}
2898
2899fn prove_argument_layouts(
2914 trace: &TransactionTrace,
2915 contracts: &[ContractDebugInfo],
2916 framed: &[FramedStep],
2917) -> Vec<Evidence> {
2918 let mut evidence = vec![Evidence::Unknown; contracts.len()];
2919 if contracts.is_empty() || framed.is_empty() {
2920 return evidence;
2921 }
2922 let mut calldata_by_entry = FxHashMap::<usize, &str>::default();
2925 calldata_by_entry.insert(0, trace.input_data.as_str());
2926 for call in &trace.artifacts.calls {
2927 if let Some(entry_step) = call.entry_step {
2928 calldata_by_entry.insert(entry_step, call.input.as_str());
2929 }
2930 }
2931
2932 let mut examined = std::collections::HashSet::<u32>::new();
2933 let mut frame_calldata = FxHashMap::<u32, &str>::default();
2934 for (index, step) in framed.iter().enumerate() {
2935 let entered = index == 0 || framed[index - 1].frame_id != step.frame_id;
2936 if entered {
2937 if let Some(calldata) = calldata_by_entry.get(&index) {
2938 frame_calldata.insert(step.frame_id, calldata);
2939 }
2940 }
2941 let (Some(contract_index), Some(function_index)) = (step.contract, step.entry) else {
2942 continue;
2943 };
2944 if !examined.insert(step.frame_id) {
2947 continue;
2948 }
2949 let Some(calldata) = frame_calldata.get(&step.frame_id) else {
2950 continue;
2951 };
2952 let Some(function) = contracts[contract_index].functions.get(function_index) else {
2953 continue;
2954 };
2955 let stack = trace.steps.step(index).stack().to_vec();
2956 let observed = argument_evidence(function, calldata, &stack);
2957 evidence[contract_index] = evidence[contract_index].merge(observed);
2958 }
2959 evidence
2960}
2961
2962fn argument_evidence(function: &SourceFunction, calldata: &str, stack: &[EvmWord]) -> Evidence {
2970 let count = function.params.len();
2971 if count == 0 || stack.len() < count {
2972 return Evidence::Unknown;
2973 }
2974 if !function.params.iter().all(readable_parameter) {
2976 return Evidence::Unknown;
2977 }
2978 let signature = format!(
2979 "{}({})",
2980 function.name,
2981 function
2982 .params
2983 .iter()
2984 .map(|param| canonical_value_type(¶m.ty))
2985 .collect::<Vec<_>>()
2986 .join(",")
2987 );
2988 let Ok(selector) = function_selector(&signature) else {
2989 return Evidence::Unknown;
2990 };
2991 let data = calldata.trim_start_matches("0x");
2992 let Some((encoded_selector, arguments)) = data.split_at_checked(8) else {
2993 return Evidence::Unknown;
2994 };
2995 if encoded_selector.to_ascii_lowercase() != hex_bytes(&selector) {
2997 return Evidence::Unknown;
2998 }
2999 let expected = (0..count)
3000 .map(|index| {
3001 arguments
3002 .get(index * 64..(index + 1) * 64)
3003 .map(str::to_ascii_lowercase)
3004 })
3005 .collect::<Option<Vec<_>>>();
3006 let Some(expected) = expected else {
3007 return Evidence::Unknown;
3008 };
3009 let checkable = (0..count)
3012 .filter(|index| is_value_type(&function.params[*index].ty))
3013 .collect::<Vec<_>>();
3014 if checkable.is_empty() {
3015 return Evidence::Unknown;
3016 }
3017 let matches = |order: ArgumentOrder| {
3018 checkable.iter().all(|index| {
3019 let candidate = &stack[order.word_index(*index, count, stack.len())];
3020 hex_bytes(candidate.as_bytes()) == expected[*index]
3021 })
3022 };
3023 let distinguishing = checkable.iter().any(|index| {
3025 ArgumentOrder::FirstOnTop.word_index(*index, count, stack.len())
3026 != ArgumentOrder::LastOnTop.word_index(*index, count, stack.len())
3027 });
3028 let first = matches(ArgumentOrder::FirstOnTop);
3029 let last = matches(ArgumentOrder::LastOnTop);
3030 match (first, last) {
3031 (true, true) => Evidence::TopWords,
3034 (true, false) if distinguishing => Evidence::Ordered(ArgumentOrder::FirstOnTop),
3035 (false, true) if distinguishing => Evidence::Ordered(ArgumentOrder::LastOnTop),
3036 (true, false) | (false, true) => Evidence::TopWords,
3038 (false, false) => Evidence::Contradicted,
3040 }
3041}
3042
3043fn canonical_value_type(ty: &str) -> String {
3046 match ty {
3047 "uint" => "uint256".to_owned(),
3048 "int" => "int256".to_owned(),
3049 "address payable" => "address".to_owned(),
3050 other => other.to_owned(),
3051 }
3052}
3053
3054fn hex_bytes(bytes: &[u8]) -> String {
3055 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
3056}
3057
3058#[must_use]
3060pub fn call_target(step: StepView<'_>) -> Option<String> {
3061 if !opcode::is_call(step.op()) {
3062 return None;
3063 }
3064 address_from_word(&step.stack().peek(1)?.to_string())
3065}
3066
3067#[must_use]
3070pub fn address_from_word(word: &str) -> Option<String> {
3071 let hex = word.trim_start_matches("0x");
3072 if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
3073 return None;
3074 }
3075 let padded = format!("{hex:0>40}");
3076 let address = padded.get(padded.len() - 40..)?;
3077 if address.bytes().all(|byte| byte == b'0') {
3078 return None;
3079 }
3080 Some(format!("0x{}", address.to_ascii_lowercase()))
3081}
3082
3083#[cfg(test)]
3084mod tests {
3085 use std::collections::BTreeMap;
3086
3087 use serde_json::json;
3088 use soldb_core::{ContractCreation, ExecutionCall, StepSnapshot, TraceStep, TransactionTrace};
3089 use soldb_ethdebug::{
3090 function_selector, source_range, CodeGenerator, EthdebugInfo, Instruction,
3091 };
3092
3093 use soldb_core::{EvmWord, Word as StackWord};
3094
3095 use crate::FrameState;
3096
3097 use super::{
3098 address_from_word, normalize_address, ArgumentLayout, ArgumentOrder, ContractDebugInfo,
3099 InferredVariable, JumpMarker, LineKey, LocalsStatus, NearestLines, SourceCache, StepInfo,
3100 StepMap, VariableKind,
3101 };
3102
3103 const SOURCE: &str = "\
3105contract C {
3106 function outer(uint256 a) public {
3107 uint256 b = a + 1;
3108 inner(b);
3109 b = 0;
3110 }
3111 function inner(uint256 x) internal {
3112 x += 1;
3113 }
3114}
3115";
3116
3117 fn offset_of(needle: &str) -> u64 {
3118 SOURCE.find(needle).expect(needle) as u64
3119 }
3120
3121 fn instruction(pc: u64, offset: u64, length: u64) -> Instruction {
3122 instruction_with(pc, offset, length, json!({}))
3123 }
3124
3125 fn instruction_with(
3127 pc: u64,
3128 offset: u64,
3129 length: u64,
3130 extra: serde_json::Value,
3131 ) -> Instruction {
3132 let mut context = json!({"code": source_range(0, offset, length)});
3133 for (key, value) in extra.as_object().expect("object") {
3134 context[key] = value.clone();
3135 }
3136 Instruction::new(pc, "JUMPDEST", Some(context))
3137 }
3138
3139 fn contract(address: Option<&str>) -> ContractDebugInfo {
3140 let whole = SOURCE.len() as u64;
3141 let info = EthdebugInfo {
3142 compilation: serde_json::Value::Null,
3143 contract_name: "C".to_owned(),
3144 environment: "runtime".to_owned(),
3145 instructions: vec![
3146 instruction(0, 0, whole),
3148 instruction(1, 0, whole),
3149 instruction(10, offset_of("function outer"), 90),
3151 instruction(11, offset_of("uint256 b = a + 1;"), 18),
3152 instruction(12, offset_of("inner(b);"), 9),
3153 instruction(13, offset_of("inner(b);"), 9),
3154 instruction(14, offset_of("b = 0;"), 6),
3155 instruction(20, offset_of("function inner"), 50),
3157 instruction(21, offset_of("x += 1;"), 7),
3158 instruction(30, 0, whole),
3160 instruction(40, 0, whole),
3162 ],
3163 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
3164 variable_locations: BTreeMap::new(),
3165 };
3166 ContractDebugInfo::new(address, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]))
3167 }
3168
3169 fn step(pc: u64, depth: u64, op: &str, stack: &[&str]) -> TraceStep {
3170 TraceStep {
3171 pc,
3172 op: op.into(),
3173 gas: 0,
3174 gas_cost: 0,
3175 depth,
3176 stack: stack.iter().map(|word| StackWord::from(*word)).collect(),
3177 memory: None,
3178 storage: None,
3179 error: None,
3180 snapshot: StepSnapshot::default(),
3181 }
3182 }
3183
3184 fn trace(steps: Vec<TraceStep>) -> TransactionTrace {
3185 TransactionTrace {
3186 tx_hash: None,
3187 from_addr: "0x1".to_owned(),
3188 to_addr: Some("0xAAaa000000000000000000000000000000000001".to_owned()),
3189 value: "0x0".to_owned(),
3190 input_data: "0x".to_owned(),
3191 gas_used: 0,
3192 output: "0x".to_owned(),
3193 success: true,
3194 error: None,
3195 debug_trace_available: true,
3196 contract_address: None,
3197 backend: None,
3198 capabilities: Default::default(),
3199 artifacts: Default::default(),
3200 steps: steps.into(),
3201 }
3202 }
3203
3204 fn outer_calls_inner() -> TransactionTrace {
3207 trace(vec![
3208 step(0, 1, "PUSH1", &[]),
3209 step(1, 1, "JUMPI", &[]),
3210 step(10, 1, "JUMPDEST", &[]),
3211 step(11, 1, "ADD", &[]),
3212 step(12, 1, "PUSH1", &[]),
3213 step(20, 1, "JUMPDEST", &[]),
3214 step(21, 1, "ADD", &[]),
3215 step(21, 1, "SWAP1", &[]),
3216 step(13, 1, "JUMPDEST", &[]),
3217 step(14, 1, "POP", &[]),
3218 step(30, 1, "STOP", &[]),
3219 ])
3220 }
3221
3222 const PAY_SOURCE: &str = "\
3225contract P {
3226 function pay(address to, uint256 amount) public {
3227 total(to, amount);
3228 }
3229 function total(address to, uint256 amount) internal {
3230 amount += 1;
3231 }
3232}
3233";
3234
3235 fn pay_contract() -> ContractDebugInfo {
3236 let whole = PAY_SOURCE.len() as u64;
3237 let at = |needle: &str| PAY_SOURCE.find(needle).expect(needle) as u64;
3238 let info = EthdebugInfo {
3239 compilation: serde_json::Value::Null,
3240 contract_name: "P".to_owned(),
3241 environment: "runtime".to_owned(),
3242 instructions: vec![
3243 instruction(0, 0, whole),
3244 instruction(10, at("function pay"), 80),
3245 instruction(11, at("total(to, amount);"), 18),
3246 instruction(20, at("function total"), 70),
3247 instruction(21, at("amount += 1;"), 12),
3248 ],
3249 sources: BTreeMap::from([(0, "P.sol".to_owned())]),
3250 variable_locations: BTreeMap::new(),
3251 };
3252 ContractDebugInfo::new(
3253 None,
3254 "P",
3255 info,
3256 BTreeMap::from([(0, PAY_SOURCE.to_owned())]),
3257 )
3258 }
3259
3260 fn evm_words(stack: &[StackWord]) -> Vec<EvmWord> {
3262 stack
3263 .iter()
3264 .map(|word| EvmWord::parse(word).expect("test word"))
3265 .collect()
3266 }
3267
3268 fn word_of(value: &str) -> StackWord {
3269 StackWord::from(format!("0x{:0>64}", value.trim_start_matches("0x")).as_str())
3270 }
3271
3272 fn pay_trace(entry_stack: &[StackWord], inner_stack: &[StackWord]) -> TransactionTrace {
3275 let selector = soldb_ethdebug::function_selector("pay(address,uint256)").expect("selector");
3276 let calldata = format!(
3277 "0x{}{}{}",
3278 selector
3279 .iter()
3280 .map(|byte| format!("{byte:02x}"))
3281 .collect::<String>(),
3282 &word_of(PAY_TO)[2..],
3283 &word_of(PAY_AMOUNT)[2..]
3284 );
3285 fn borrow(stack: &[StackWord]) -> Vec<&str> {
3286 stack.iter().map(|word| &**word).collect()
3287 }
3288 let mut trace = trace(vec![
3289 step(0, 1, "PUSH1", &[]),
3290 step(10, 1, "JUMPDEST", &borrow(entry_stack)),
3291 step(11, 1, "JUMP", &borrow(inner_stack)),
3292 step(20, 1, "JUMPDEST", &borrow(inner_stack)),
3293 step(21, 1, "ADD", &[]),
3294 ]);
3295 trace.input_data = calldata;
3296 trace
3297 }
3298
3299 const PAY_TO: &str = "00000000000000000000000000000000000000aa";
3300 const PAY_AMOUNT: &str = "7";
3301
3302 #[test]
3303 fn the_trace_proves_which_end_the_first_argument_is_at() {
3304 let to = word_of(PAY_TO);
3305 let amount = word_of(PAY_AMOUNT);
3306 let tag = word_of("2a");
3307
3308 let first_on_top = [tag.clone(), amount.clone(), to.clone()];
3310 let trace = pay_trace(&first_on_top, &first_on_top);
3311 let map = StepMap::new(&trace, vec![pay_contract()]);
3312 assert_eq!(
3313 map.argument_layout(0),
3314 Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))
3315 );
3316 let frames = map.frames(4);
3317 let inner = frames.first().expect("innermost frame");
3318 assert_eq!(inner.function_name.as_deref(), Some("total"));
3319 let arguments = map.frame_arguments(
3320 inner,
3321 FrameState {
3322 stack: &evm_words(&first_on_top),
3323 memory: None,
3324 },
3325 );
3326 assert_eq!(
3327 arguments
3328 .iter()
3329 .map(|argument| format!("{} = {}", argument.name, argument.value.display))
3330 .collect::<Vec<_>>(),
3331 [
3332 "to = 0x00000000000000000000000000000000000000aa".to_owned(),
3333 "amount = 7".to_owned()
3334 ]
3335 );
3336
3337 let last_on_top = [tag.clone(), to.clone(), amount.clone()];
3339 let trace = pay_trace(&last_on_top, &last_on_top);
3340 let map = StepMap::new(&trace, vec![pay_contract()]);
3341 assert_eq!(
3342 map.argument_layout(0),
3343 Some(ArgumentLayout::Ordered(ArgumentOrder::LastOnTop))
3344 );
3345 let frames = map.frames(4);
3346 let arguments = map.frame_arguments(
3347 frames.first().expect("frame"),
3348 FrameState {
3349 stack: &evm_words(&last_on_top),
3350 memory: None,
3351 },
3352 );
3353 assert_eq!(
3354 arguments[0].value.display,
3355 "0x00000000000000000000000000000000000000aa"
3356 );
3357 assert_eq!(arguments[1].value.display, "7");
3358
3359 let wrapper = [tag.clone(), word_of("dead"), word_of("beef")];
3362 let trace = pay_trace(&wrapper, &first_on_top);
3363 let map = StepMap::new(&trace, vec![pay_contract()]);
3364 assert_eq!(map.argument_layout(0), None);
3365 let frames = map.frames(4);
3366 assert!(map
3367 .frame_arguments(
3368 frames.first().expect("frame"),
3369 FrameState {
3370 stack: &evm_words(&first_on_top),
3371 memory: None
3372 }
3373 )
3374 .is_empty());
3375
3376 let mut unknown = pay_trace(&first_on_top, &first_on_top);
3378 unknown.input_data = "0x".to_owned();
3379 let map = StepMap::new(&unknown, vec![pay_contract()]);
3380 assert_eq!(map.argument_layout(0), None);
3381 let frames = map.frames(4);
3382 assert!(map
3383 .frame_arguments(
3384 frames.first().expect("frame"),
3385 FrameState {
3386 stack: &evm_words(&first_on_top),
3387 memory: None
3388 }
3389 )
3390 .is_empty());
3391 }
3392
3393 #[test]
3394 fn one_parameter_is_on_top_under_either_order() {
3395 let mut trace = outer_calls_inner();
3398 let selector = soldb_ethdebug::function_selector("outer(uint256)").expect("selector");
3399 let amount = word_of("5");
3400 trace.input_data = format!(
3401 "0x{}{}",
3402 selector
3403 .iter()
3404 .map(|byte| format!("{byte:02x}"))
3405 .collect::<String>(),
3406 &amount[2..]
3407 );
3408 let stack = [word_of("2a"), amount.clone()];
3409 let mut steps = trace.steps.to_steps();
3410 steps[2].snapshot =
3411 StepSnapshot::new(stack.to_vec(), None, BTreeMap::new(), BTreeMap::new());
3412 trace.steps = steps.into();
3413 let map = StepMap::new(&trace, vec![contract(None)]);
3414 assert_eq!(map.argument_layout(0), Some(ArgumentLayout::TopWords));
3415 let frames = map.frames(3);
3416 let outer = frames
3417 .iter()
3418 .find(|frame| frame.function_name.as_deref() == Some("outer"))
3419 .expect("outer frame");
3420 let arguments = map.frame_arguments(
3421 outer,
3422 FrameState {
3423 stack: &evm_words(&stack),
3424 memory: None,
3425 },
3426 );
3427 assert_eq!(arguments.len(), 1);
3428 assert_eq!(arguments[0].name, "a");
3429 assert_eq!(arguments[0].value.display, "5");
3430
3431 let map = StepMap::new(&trace, vec![pay_contract()]);
3433 assert_eq!(map.argument_layout(0), None);
3434 }
3435
3436 fn key(line: u64) -> Option<LineKey> {
3437 Some(LineKey {
3438 contract: 0,
3439 source_id: 0,
3440 line,
3441 })
3442 }
3443
3444 #[test]
3445 fn maps_steps_to_lines_and_infers_internal_frames() {
3446 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3447 assert!(map.has_source());
3448 assert_eq!(map.line_key(0), None);
3450 assert_eq!(map.location(0).expect("location").line, 1);
3451 assert!(!map.location(0).expect("location").generated);
3452 assert_eq!(map.line_key(2), key(2));
3453 assert_eq!(map.line_key(3), key(3));
3454 assert_eq!(map.line_key(4), key(4));
3455 assert_eq!(map.line_key(5), key(7));
3456 assert_eq!(map.line_key(6), key(8));
3457 assert_eq!(map.line_key(8), key(4));
3458 assert_eq!(map.line_key(10), None);
3459
3460 let depths = (0..11)
3462 .map(|step| map.frame_depth(step).expect("depth"))
3463 .collect::<Vec<_>>();
3464 assert_eq!(depths, vec![0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 0]);
3465 assert!(map.is_frame_entry(2));
3466 assert!(map.is_frame_entry(5));
3467 assert!(!map.is_frame_entry(8));
3468 assert_eq!(
3469 map.location(6).expect("location").function_name.as_deref(),
3470 Some("inner")
3471 );
3472 assert_eq!(map.location(6).expect("location").path, "C.sol");
3473 assert_eq!(map.location(6).expect("location").column, 9);
3474 }
3475
3476 #[test]
3477 fn next_steps_over_calls_and_step_enters_them() {
3478 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3479 assert_eq!(map.next_source(0), Some(2));
3481 assert_eq!(map.next_source(2), Some(3));
3482 assert_eq!(map.next_source(4), Some(9));
3484 assert_eq!(map.step_into(4), Some(5));
3485 assert_eq!(map.step_into(5), Some(6));
3486 assert_eq!(map.next_source(6), Some(8));
3488 assert_eq!(map.finish(6), Some(8));
3489 assert_eq!(map.next_source(9), Some(10));
3491 assert_eq!(map.finish(9), Some(10));
3492 assert_eq!(map.next_source(10), None);
3493 assert_eq!(map.finish(10), None);
3494 }
3495
3496 #[test]
3497 fn reverse_stepping_lands_on_line_starts() {
3498 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3499 assert_eq!(map.previous_source(9), Some(4));
3501 assert_eq!(map.previous_source(7), Some(6));
3503 assert_eq!(map.previous_source(6), Some(5));
3504 assert_eq!(map.reverse_step_into(9), Some(8));
3506 assert_eq!(map.reverse_step_into(8), Some(6));
3507 assert_eq!(map.reverse_finish(6), Some(4));
3509 assert_eq!(map.reverse_finish(0), None);
3510 assert_eq!(map.previous_source(0), None);
3511 assert_eq!(map.previous_source(10), Some(9));
3514 }
3515
3516 #[test]
3517 fn line_starts_skip_returns_into_the_middle_of_a_line() {
3518 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3519 assert!(map.is_line_start(3));
3520 assert!(map.is_line_start(4));
3521 assert!(map.is_line_start(6));
3522 assert!(!map.is_line_start(7));
3523 assert!(!map.is_line_start(8));
3525 assert!(map.is_line_start(9));
3526 }
3527
3528 #[test]
3529 fn resolves_line_and_function_breakpoints() {
3530 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3531 let resolved = map.resolve_line(Some("C.sol"), 4).expect("line 4");
3532 assert_eq!(resolved.len(), 1);
3533 assert_eq!(resolved[0].key, key(4).unwrap());
3534 assert_eq!(resolved[0].path, "C.sol");
3535 assert_eq!(
3537 map.resolve_line(None, 8).expect("line 8")[0].key,
3538 key(8).unwrap()
3539 );
3540 assert_eq!(
3543 map.resolve_line(None, 10).expect("line 10")[0].key,
3544 key(1).unwrap()
3545 );
3546 assert_eq!(
3547 map.resolve_line(None, 99).expect_err("past the end"),
3548 "no instruction maps to line 99"
3549 );
3550 assert_eq!(
3551 map.resolve_line(Some("D.sol"), 1)
3552 .expect_err("unknown file"),
3553 "source file not found: D.sol"
3554 );
3555
3556 let inner = map.resolve_function("inner").expect("inner");
3557 assert_eq!(inner.len(), 1);
3558 assert_eq!(inner[0].line, 7);
3559 assert_eq!(inner[0].contract_name, "C");
3560 assert_eq!(
3561 map.resolve_function("C.outer").expect("qualified")[0].line,
3562 2
3563 );
3564 assert!(map.resolve_function("D.outer").is_err());
3565 assert!(map.resolve_function("missing").is_err());
3566 }
3567
3568 #[test]
3569 fn frames_list_the_call_structure_innermost_first() {
3570 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3571 let frames = map.frames(6);
3572 assert_eq!(frames.len(), 3);
3573 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3574 assert_eq!(frames[0].depth, 2);
3575 assert!(!frames[0].external);
3576 assert_eq!(frames[0].step, 6);
3577 assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
3578 assert_eq!(
3579 frames[1].location.as_ref().map(|location| location.line),
3580 Some(4)
3581 );
3582 assert_eq!(frames[1].step, 4);
3583 assert!(frames[2].external);
3584 assert_eq!(frames[2].depth, 0);
3585 assert_eq!(
3586 frames[2].address.as_deref(),
3587 Some("0xaaaa000000000000000000000000000000000001")
3588 );
3589 assert!(map.frames(99).is_empty());
3590
3591 let listing = map.source_listing(6, 1).expect("listing");
3592 assert_eq!(listing.current_line, 8);
3593 assert_eq!(listing.lines.len(), 3);
3594 assert_eq!(listing.lines[1].0, 8);
3595 assert_eq!(listing.lines[1].1.trim(), "x += 1;");
3596 }
3597
3598 #[test]
3599 fn external_calls_map_through_the_callee_contract() {
3600 let callee = "0xbbbb000000000000000000000000000000000002";
3602 let word = format!("0x{:0>64}", callee.trim_start_matches("0x"));
3603 let steps = vec![
3604 step(0, 1, "PUSH1", &[]),
3605 step(
3606 12,
3607 1,
3608 "CALL",
3609 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3610 ),
3611 step(21, 2, "JUMPDEST", &[]),
3612 step(21, 2, "ADD", &[]),
3613 step(13, 1, "JUMPDEST", &[]),
3614 step(30, 1, "STOP", &[]),
3615 ];
3616 let root = contract(Some("0xAAAA000000000000000000000000000000000001"));
3617 let map = StepMap::new(&trace(steps), vec![root, contract(Some(callee))]);
3618 assert_eq!(
3619 map.executing_address(1),
3620 Some("0xaaaa000000000000000000000000000000000001")
3621 );
3622 assert_eq!(map.executing_address(2), Some(callee));
3623 assert_eq!(
3624 map.line_key(2),
3625 Some(LineKey {
3626 contract: 1,
3627 source_id: 0,
3628 line: 8
3629 })
3630 );
3631 assert_eq!(map.frame_depth(1), Some(1));
3632 assert_eq!(map.frame_depth(2), Some(3));
3634 assert!(map.is_frame_entry(2));
3635 assert_eq!(map.frame_depth(4), Some(1));
3636 assert_eq!(map.next_source(1), Some(5));
3638 assert_eq!(map.step_into(1), Some(2));
3639 let frames = map.frames(2);
3640 assert_eq!(frames.len(), 4);
3641 assert!(frames[1].external);
3642 assert_eq!(frames[1].address.as_deref(), Some(callee));
3643 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3644 }
3645
3646 fn program(address: &str, environment: &str, spans: &[(u64, &str)]) -> ContractDebugInfo {
3649 let info = EthdebugInfo {
3650 compilation: serde_json::Value::Null,
3651 contract_name: "C".to_owned(),
3652 environment: environment.to_owned(),
3653 instructions: spans
3654 .iter()
3655 .map(|(pc, needle)| instruction(*pc, offset_of(needle), needle.len() as u64))
3656 .collect(),
3657 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
3658 variable_locations: BTreeMap::new(),
3659 };
3660 ContractDebugInfo::new(
3661 Some(address),
3662 "C",
3663 info,
3664 BTreeMap::from([(0, SOURCE.to_owned())]),
3665 )
3666 }
3667
3668 const ROOT: &str = "0xaaaa000000000000000000000000000000000001";
3669 const CREATED: &str = "0xcccc000000000000000000000000000000000003";
3670
3671 fn creation(entry_step: usize, exit_step: usize, address: Option<&str>) -> ContractCreation {
3672 ContractCreation {
3673 id: 0,
3674 parent_id: None,
3675 depth: 2,
3676 entry_step: Some(entry_step),
3677 exit_step: Some(exit_step),
3678 create_type: "CREATE".to_owned(),
3679 caller: ROOT.to_owned(),
3680 address: address.map(str::to_owned),
3681 value: "0x0".to_owned(),
3682 init_code: "0x".to_owned(),
3683 gas_limit: 0,
3684 gas_used: None,
3685 output: None,
3686 success: Some(address.is_some()),
3687 error: None,
3688 }
3689 }
3690
3691 fn call(
3692 id: usize,
3693 entry_step: usize,
3694 exit_step: usize,
3695 call_type: &str,
3696 bytecode_address: &str,
3697 ) -> ExecutionCall {
3698 ExecutionCall {
3699 id,
3700 parent_id: None,
3701 depth: 2,
3702 entry_step: Some(entry_step),
3703 exit_step: Some(exit_step),
3704 call_type: call_type.to_owned(),
3705 from: ROOT.to_owned(),
3706 to: ROOT.to_owned(),
3707 bytecode_address: bytecode_address.to_owned(),
3708 value: "0x0".to_owned(),
3709 input: "0x".to_owned(),
3710 gas_limit: 0,
3711 gas_used: None,
3712 output: None,
3713 success: Some(true),
3714 error: None,
3715 }
3716 }
3717
3718 fn root_creates_then_calls() -> TransactionTrace {
3721 let word = format!("0x{:0>64}", CREATED.trim_start_matches("0x"));
3722 let mut trace = trace(vec![
3723 step(0, 1, "PUSH1", &[]),
3724 step(12, 1, "CREATE", &["0x0", "0x0", "0x0"]),
3726 step(21, 2, "JUMPDEST", &[]),
3727 step(21, 2, "RETURN", &[]),
3728 step(13, 1, "JUMPDEST", &[]),
3729 step(
3730 14,
3731 1,
3732 "CALL",
3733 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3734 ),
3735 step(21, 2, "JUMPDEST", &[]),
3736 step(30, 1, "STOP", &[]),
3737 ]);
3738 trace
3739 .artifacts
3740 .creations
3741 .push(creation(2, 4, Some(CREATED)));
3742 trace.artifacts.calls.push(call(0, 6, 7, "CALL", CREATED));
3743 trace
3744 }
3745
3746 #[test]
3747 fn creations_map_through_the_created_contracts_creation_program() {
3748 let trace = root_creates_then_calls();
3749 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3753 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3754 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed, creation]);
3755
3756 assert_eq!(map.executing_address(2), Some(CREATED));
3757 assert_eq!(map.storage_address(2), Some(CREATED));
3758 assert_eq!(
3759 map.line_key(2).map(|key| (key.contract, key.line)),
3760 Some((2, 8))
3761 );
3762 assert_eq!(
3763 map.line_key(6).map(|key| (key.contract, key.line)),
3764 Some((1, 5))
3765 );
3766 let frames = map.frames(2);
3768 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3769 assert!(frames[1].external);
3770 assert_eq!(frames[1].address.as_deref(), Some(CREATED));
3771 assert_eq!(map.frame_depth(4), Some(1));
3772 }
3773
3774 #[test]
3775 fn a_line_resolves_only_in_the_programs_that_map_it() {
3776 let trace = root_creates_then_calls();
3780 let whole = SOURCE.len() as u64;
3781 let mut deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3782 deployed.info.instructions.push(instruction(20, 0, whole));
3783 let mut creation = program(CREATED, "create", &[(21, "x += 1;")]);
3784 creation.info.instructions.push(instruction(20, 0, whole));
3785 let map = StepMap::new(&trace, vec![deployed, creation]);
3786 let resolved = map.resolve_line(Some("C.sol"), 8).expect("line 8");
3787 assert_eq!(resolved.len(), 1);
3788 assert_eq!(resolved[0].key.contract, 1);
3789 assert_eq!(resolved[0].key.line, 8);
3790 let resolved = map.resolve_line(Some("C.sol"), 10).expect("line 10");
3792 assert_eq!(resolved.len(), 2);
3793 assert!(resolved.iter().all(|line| line.key.line == 1));
3794 }
3795
3796 #[test]
3797 fn a_creation_uses_the_only_program_at_its_address() {
3798 let trace = root_creates_then_calls();
3801 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3802 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed]);
3803 assert_eq!(
3804 map.line_key(2).map(|key| (key.contract, key.line)),
3805 Some((1, 5))
3806 );
3807 }
3808
3809 #[test]
3810 fn a_creation_the_backend_did_not_record_has_no_contract() {
3811 let mut trace = root_creates_then_calls();
3812 trace.artifacts.creations.clear();
3813 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3814 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), creation]);
3815 assert_eq!(map.executing_address(2), None);
3816 assert_eq!(map.line_key(2), None);
3817 assert_eq!(map.executing_address(6), Some(CREATED));
3819 }
3820
3821 #[test]
3822 fn a_deployment_root_prefers_the_creation_program() {
3823 let mut trace = trace(vec![step(21, 1, "JUMPDEST", &[]), step(30, 1, "STOP", &[])]);
3824 trace.to_addr = None;
3825 trace.contract_address = Some(CREATED.to_owned());
3826 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3827 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3828 let map = StepMap::new(&trace, vec![deployed, creation]);
3829 assert_eq!(
3830 map.line_key(0).map(|key| (key.contract, key.line)),
3831 Some((1, 8))
3832 );
3833 }
3834
3835 #[test]
3836 fn frames_are_resolved_from_recorded_calls_before_the_stack() {
3837 let callee = "0xbbbb000000000000000000000000000000000002";
3838 let precompile = "0x0000000000000000000000000000000000000001";
3839 let mut trace = trace(vec![
3841 step(0, 1, "PUSH1", &[]),
3842 step(11, 1, "STATICCALL", &[]),
3843 step(12, 1, "DELEGATECALL", &[]),
3844 step(21, 2, "JUMPDEST", &[]),
3845 step(21, 2, "ADD", &[]),
3846 step(13, 1, "JUMPDEST", &[]),
3847 ]);
3848 trace
3851 .artifacts
3852 .calls
3853 .push(call(0, 2, 2, "STATICCALL", precompile));
3854 trace
3855 .artifacts
3856 .calls
3857 .push(call(1, 3, 5, "DELEGATECALL", callee));
3858 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), contract(Some(callee))]);
3859
3860 assert_eq!(map.executing_address(2), Some(ROOT));
3861 assert_eq!(map.frame_depth(2), Some(1));
3862 assert_eq!(map.executing_address(3), Some(callee));
3863 assert_eq!(map.line_key(3).map(|key| key.contract), Some(1));
3864 assert_eq!(map.storage_address(3), Some(ROOT));
3866 }
3867
3868 const LEGACY_SOURCE: &str = "contract L {
3871 uint256 total;
3872 modifier tracked(uint256 tag) {
3873 uint256 before = total;
3874 _;
3875 total = before + tag;
3876 }
3877 function outer(uint256 a) public returns (uint256 sum) {
3878 uint256 twice = a * 2;
3879 if (twice > 2) {
3880 uint256 inner = twice - 2;
3881 sum = inner;
3882 }
3883 sum = twice + helper(twice);
3884 }
3885 function helper(uint256 x) internal pure returns (uint256 r) {
3886 uint256 y = x + 1;
3887 r = y;
3888 }
3889 function guarded(uint256 v) public tracked(9) {
3890 uint256 kept = v;
3891 total = kept;
3892 }
3893 function set(uint256 v) public {
3894 total = v;
3895 }
3896 function take(bytes calldata data, uint256[] calldata items, uint256 n) public {
3897 total = n;
3898 }
3899}
3900";
3901
3902 fn legacy_offset(needle: &str) -> u64 {
3903 LEGACY_SOURCE.find(needle).expect(needle) as u64
3904 }
3905
3906 fn legacy_contract(code_generator: Option<CodeGenerator>) -> ContractDebugInfo {
3910 let spans: &[(u64, &str)] = &[
3911 (0, "contract L"),
3912 (10, "function outer"),
3915 (11, "uint256 sum"),
3916 (12, "uint256 twice"),
3917 (13, "a * 2"),
3918 (14, "twice > 2"),
3919 (15, "uint256 inner"),
3920 (16, "sum = inner"),
3921 (17, "helper(twice)"),
3922 (18, "sum = twice + helper(twice)"),
3923 (19, "function outer"),
3924 (20, "function helper"),
3926 (21, "uint256 y"),
3927 (22, "r = y"),
3928 (23, "function helper"),
3929 (24, "uint256 r"),
3930 (30, "function guarded"),
3934 (31, "uint256 before"),
3935 (32, "uint256 kept"),
3936 (33, "total = kept"),
3937 (34, "total = before + tag"),
3938 (35, "function guarded"),
3939 (36, "function guarded"),
3940 (40, "function set"),
3943 (41, "total = v"),
3944 (42, "function set"),
3945 (43, "function set"),
3946 (50, "function take"),
3948 (51, "total = n"),
3949 ];
3950 let instructions = spans
3951 .iter()
3952 .map(|(pc, needle)| {
3953 if matches!(pc, 35 | 42) {
3955 instruction_with(
3956 *pc,
3957 legacy_offset(needle),
3958 needle.len() as u64,
3959 json!({"invoke": {}}),
3960 )
3961 } else {
3962 instruction(*pc, legacy_offset(needle), needle.len() as u64)
3963 }
3964 })
3965 .collect();
3966 let info = EthdebugInfo {
3967 compilation: serde_json::Value::Null,
3968 contract_name: "L".to_owned(),
3969 environment: "call".to_owned(),
3970 instructions,
3971 sources: BTreeMap::from([(0, "L.sol".to_owned())]),
3972 variable_locations: BTreeMap::new(),
3973 };
3974 ContractDebugInfo::new(
3975 Some(ROOT),
3976 "L",
3977 info,
3978 BTreeMap::from([(0, LEGACY_SOURCE.to_owned())]),
3979 )
3980 .with_code_generator(code_generator)
3981 }
3982
3983 fn outer_trace() -> TransactionTrace {
3986 let ret = "0x9";
3987 trace(vec![
3988 step(0, 1, "PUSH1", &[]),
3989 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
3990 step(11, 1, "PUSH0", &[ret, "0x5"]),
3991 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
3992 step(13, 1, "MUL", &[ret, "0x5", "0x0", "0x0"]),
3993 step(14, 1, "GT", &[ret, "0x5", "0x0", "0xa"]),
3994 step(15, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
3995 step(16, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0x8"]),
3996 step(18, 1, "DUP2", &[ret, "0x5", "0x8", "0xa"]),
3997 step(17, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
3998 step(
3999 20,
4000 1,
4001 "JUMPDEST",
4002 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"],
4003 ),
4004 step(24, 1, "PUSH0", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
4005 step(
4006 21,
4007 1,
4008 "PUSH0",
4009 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0"],
4010 ),
4011 step(
4012 22,
4013 1,
4014 "DUP1",
4015 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0", "0xb"],
4016 ),
4017 step(23, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0xb", "0x12"]),
4018 step(18, 1, "ADD", &[ret, "0x5", "0x8", "0xa", "0xb"]),
4019 step(19, 1, "JUMP", &[ret, "0x5", "0x15", "0xa"]),
4020 step(0, 1, "STOP", &[]),
4021 ])
4022 }
4023
4024 fn named(variables: &[InferredVariable]) -> Vec<(&str, VariableKind, Option<usize>)> {
4025 variables
4026 .iter()
4027 .map(|variable| (variable.name.as_str(), variable.kind, variable.slot))
4028 .collect()
4029 }
4030
4031 #[test]
4032 fn locals_are_inferred_from_the_legacy_stack_layout() {
4033 let map = StepMap::new(
4034 &outer_trace(),
4035 vec![legacy_contract(Some(CodeGenerator::Legacy))],
4036 );
4037 let trace = outer_trace();
4038
4039 assert_eq!(
4041 map.locals_at(0),
4042 LocalsStatus::Unavailable("no function is executing at this step")
4043 );
4044 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
4048 panic!("{:?}", map.locals_at(1));
4049 };
4050 assert_eq!(named(at_entry), [("sum", VariableKind::Return, None)]);
4051 let LocalsStatus::Inferred(at_return) = map.locals_at(2) else {
4052 panic!("{:?}", map.locals_at(2));
4053 };
4054 assert_eq!(
4055 named(at_return),
4056 [
4057 ("a", VariableKind::Parameter, Some(1)),
4058 ("sum", VariableKind::Return, Some(2))
4059 ]
4060 );
4061 let LocalsStatus::Inferred(at_twice) = map.locals_at(5) else {
4063 panic!("{:?}", map.locals_at(5));
4064 };
4065 assert_eq!(
4066 named(at_twice),
4067 [
4068 ("a", VariableKind::Parameter, Some(1)),
4069 ("sum", VariableKind::Return, Some(2)),
4070 ("twice", VariableKind::Local, Some(3))
4071 ]
4072 );
4073 let values = map.inferred_variables(&trace, 5, None);
4074 assert_eq!(values[0].value.display, "5");
4075 assert_eq!(values[2].value.display, "10");
4076 assert_eq!(values[2].location.offset, 3);
4077 let LocalsStatus::Inferred(in_block) = map.locals_at(7) else {
4079 panic!("{:?}", map.locals_at(7));
4080 };
4081 assert_eq!(
4082 named(in_block).last(),
4083 Some(&("inner", VariableKind::Local, Some(4)))
4084 );
4085 assert_eq!(
4086 map.inferred_variables(&trace, 7, None)[3].value.display,
4087 "8"
4088 );
4089 let LocalsStatus::Inferred(after_block) = map.locals_at(8) else {
4090 panic!("{:?}", map.locals_at(8));
4091 };
4092 assert_eq!(after_block.len(), 3);
4093 assert_eq!(
4094 map.inferred_variables(&trace, 8, None)[1].value.display,
4095 "8"
4096 );
4097
4098 let LocalsStatus::Inferred(helper_entry) = map.locals_at(10) else {
4103 panic!("{:?}", map.locals_at(10));
4104 };
4105 assert_eq!(
4106 named(helper_entry),
4107 [
4108 ("x", VariableKind::Parameter, Some(5)),
4109 ("r", VariableKind::Return, Some(6))
4110 ]
4111 );
4112 let at_entry = map.inferred_variables(&trace, 10, None);
4113 assert_eq!(
4114 at_entry[1].value.status,
4115 crate::DebugValueStatus::Unavailable
4116 );
4117 let helper_values = map.inferred_variables(&trace, 13, None);
4118 assert_eq!(
4119 helper_values
4120 .iter()
4121 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
4122 .collect::<Vec<_>>(),
4123 [("x", "10"), ("r", "0"), ("y", "11")]
4124 );
4125 assert_eq!(helper_values[2].location.offset, 7);
4126 let LocalsStatus::Inferred(back) = map.locals_at(15) else {
4127 panic!("{:?}", map.locals_at(15));
4128 };
4129 assert_eq!(named(back).len(), 3);
4130 assert_eq!(
4131 map.inferred_variables(&trace, 15, None)[2].value.display,
4132 "10"
4133 );
4134 }
4135
4136 #[test]
4137 fn a_modifier_places_itself_and_the_function_it_runs_for() {
4138 let ret = "0x9";
4139 let trace = trace(vec![
4140 step(30, 1, "JUMPDEST", &[ret, "0x7"]),
4141 step(31, 1, "PUSH0", &[ret, "0x7", "0x9"]),
4144 step(32, 1, "PUSH0", &[ret, "0x7", "0x9", "0x3"]),
4146 step(33, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3", "0x7"]),
4147 step(34, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3"]),
4149 ]);
4150 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4151 let LocalsStatus::Inferred(modifier) = map.locals_at(1) else {
4152 panic!("{:?}", map.locals_at(1));
4153 };
4154 assert_eq!(
4155 named(modifier),
4156 [
4157 ("tag", VariableKind::Parameter, Some(2)),
4158 ("before", VariableKind::Local, Some(3))
4159 ]
4160 );
4161 let LocalsStatus::Inferred(body) = map.locals_at(3) else {
4164 panic!("{:?}", map.locals_at(3));
4165 };
4166 assert_eq!(
4167 named(body),
4168 [
4169 ("v", VariableKind::Parameter, Some(1)),
4170 ("kept", VariableKind::Local, Some(4))
4171 ]
4172 );
4173 assert_eq!(
4174 map.inferred_variables(&trace, 3, None)[1].value.display,
4175 "7"
4176 );
4177 let LocalsStatus::Inferred(resumed) = map.locals_at(4) else {
4178 panic!("{:?}", map.locals_at(4));
4179 };
4180 assert_eq!(
4181 named(resumed),
4182 [
4183 ("tag", VariableKind::Parameter, Some(2)),
4184 ("before", VariableKind::Local, Some(3))
4185 ]
4186 );
4187 assert_eq!(
4188 map.inferred_variables(&trace, 4, None)[1].value.display,
4189 "3"
4190 );
4191 }
4192
4193 #[test]
4194 fn calldata_slices_read_the_frames_calldata() {
4195 let ret = "0x9";
4200 let mut trace = trace(vec![
4201 step(
4202 50,
4203 1,
4204 "JUMPDEST",
4205 &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"],
4206 ),
4207 step(51, 1, "PUSH0", &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"]),
4208 ]);
4209 trace.input_data = format!(
4210 "0xaabbccdd{}{}6162",
4211 "0000000000000000000000000000000000000000000000000000000000000003",
4212 "0000000000000000000000000000000000000000000000000000000000000004"
4213 );
4214 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4215 let LocalsStatus::Inferred(layout) = map.locals_at(1) else {
4216 panic!("{:?}", map.locals_at(1));
4217 };
4218 assert_eq!(
4219 layout
4220 .iter()
4221 .map(|variable| (variable.name.as_str(), variable.slot, variable.words))
4222 .collect::<Vec<_>>(),
4223 [
4224 ("data", Some(1), 2),
4225 ("items", Some(3), 2),
4226 ("n", Some(5), 1)
4227 ]
4228 );
4229 let values = map.inferred_variables(&trace, 1, None);
4230 assert_eq!(
4231 values
4232 .iter()
4233 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
4234 .collect::<Vec<_>>(),
4235 [("data", "0x6162"), ("items", "[3, 4]"), ("n", "5")]
4236 );
4237 assert_eq!(values[0].value.raw.as_deref(), Some("0x6162"));
4238 }
4239
4240 #[test]
4241 fn parameters_are_placed_by_the_first_statement_of_a_body_without_locals() {
4242 let trace = trace(vec![
4246 step(40, 1, "JUMPDEST", &["0x9", "0x7"]),
4247 step(41, 1, "DUP1", &["0x9", "0x7"]),
4248 step(41, 1, "SSTORE", &["0x9", "0x7", "0x7", "0x0"]),
4249 ]);
4250 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4251 let LocalsStatus::Inferred(at_entry) = map.locals_at(0) else {
4252 panic!("{:?}", map.locals_at(0));
4253 };
4254 assert!(at_entry.is_empty());
4255 let LocalsStatus::Inferred(in_body) = map.locals_at(2) else {
4256 panic!("{:?}", map.locals_at(2));
4257 };
4258 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(1))]);
4259 assert_eq!(
4260 map.inferred_variables(&trace, 2, None)[0].value.display,
4261 "7"
4262 );
4263 }
4264
4265 #[test]
4266 fn the_dispatchers_jump_to_the_body_places_a_public_function() {
4267 let selector = "0x9c";
4272 let mut trace = trace(vec![
4273 step(0, 1, "PUSH1", &[]),
4274 step(40, 1, "JUMPDEST", &[selector]),
4275 step(42, 1, "JUMP", &[selector, "0x9", "0x7", "0x2b"]),
4276 step(43, 1, "JUMPDEST", &[selector, "0x9", "0x7"]),
4277 step(41, 1, "SSTORE", &[selector, "0x9", "0x7", "0x0"]),
4278 ]);
4279 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4280 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
4281 panic!("{:?}", map.locals_at(1));
4282 };
4283 assert!(at_entry.is_empty());
4284 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
4285 panic!("{:?}", map.locals_at(4));
4286 };
4287 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
4288 assert_eq!(
4289 map.inferred_variables(&trace, 4, None)[0].value.display,
4290 "7"
4291 );
4292
4293 let selector = super::hex_bytes(&function_selector("set(uint256)").expect("selector"));
4297 trace.input_data = format!("0x{selector}{:064x}", 8);
4298 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4299 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
4300 panic!("{:?}", map.locals_at(4));
4301 };
4302 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(3))]);
4303 trace.input_data = format!("0x{selector}{:064x}", 7);
4305 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4306 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
4307 panic!("{:?}", map.locals_at(4));
4308 };
4309 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
4310 }
4311
4312 #[test]
4313 fn a_placed_function_places_its_modifiers() {
4314 let selector = "0x9c";
4320 let ret = "0x9";
4321 let trace = trace(vec![
4322 step(0, 1, "PUSH1", &[]),
4323 step(30, 1, "JUMPDEST", &[selector]),
4324 step(35, 1, "JUMP", &[selector, ret, "0x5", "0x24"]),
4325 step(36, 1, "JUMPDEST", &[selector, ret, "0x5"]),
4326 step(31, 1, "DUP1", &[selector, ret, "0x5", "0x9", "0x9"]),
4327 step(32, 1, "PUSH0", &[selector, ret, "0x5", "0x9", "0x9"]),
4328 step(
4329 33,
4330 1,
4331 "SSTORE",
4332 &[selector, ret, "0x5", "0x9", "0x9", "0x5"],
4333 ),
4334 step(34, 1, "SSTORE", &[selector, ret, "0x5", "0x9", "0x9"]),
4335 ]);
4336 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4337 let LocalsStatus::Inferred(modifier) = map.locals_at(4) else {
4338 panic!("{:?}", map.locals_at(4));
4339 };
4340 assert_eq!(
4341 named(modifier),
4342 [
4343 ("tag", VariableKind::Parameter, Some(3)),
4344 ("before", VariableKind::Local, Some(4))
4345 ]
4346 );
4347 let LocalsStatus::Inferred(body) = map.locals_at(6) else {
4348 panic!("{:?}", map.locals_at(6));
4349 };
4350 assert_eq!(
4351 named(body),
4352 [
4353 ("v", VariableKind::Parameter, Some(2)),
4354 ("kept", VariableKind::Local, Some(5))
4355 ]
4356 );
4357 assert_eq!(
4358 map.inferred_variables(&trace, 6, None)[1].value.display,
4359 "5"
4360 );
4361 let LocalsStatus::Inferred(resumed) = map.locals_at(7) else {
4362 panic!("{:?}", map.locals_at(7));
4363 };
4364 assert_eq!(
4365 named(resumed),
4366 [
4367 ("tag", VariableKind::Parameter, Some(3)),
4368 ("before", VariableKind::Local, Some(4))
4369 ]
4370 );
4371 let values = map.inferred_variables(&trace, 7, None);
4372 assert_eq!(values[0].value.display, "9");
4373 assert_eq!(values[1].value.display, "9");
4374 }
4375
4376 #[test]
4377 fn a_function_reached_by_fallthrough_was_inlined_and_has_no_slots() {
4378 let ret = "0x9";
4382 let trace = trace(vec![
4383 step(0, 1, "PUSH1", &[]),
4384 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
4385 step(11, 1, "PUSH0", &[ret, "0x5"]),
4386 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
4387 step(21, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
4388 step(22, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0xb"]),
4389 step(18, 1, "ADD", &[ret, "0x5", "0x0", "0xa", "0xb"]),
4390 ]);
4391 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4392 assert_eq!(
4393 map.location(4).and_then(|location| location.function_name),
4394 Some("helper".to_owned())
4395 );
4396 assert_eq!(
4397 map.locals_at(4),
4398 LocalsStatus::Unavailable(
4399 "this function was inlined by the optimizer, so its variables have no stack \
4400 slots of their own"
4401 )
4402 );
4403 assert_eq!(
4404 map.location(6).and_then(|location| location.function_name),
4405 Some("outer".to_owned())
4406 );
4407 let LocalsStatus::Inferred(back) = map.locals_at(6) else {
4408 panic!("{:?}", map.locals_at(6));
4409 };
4410 assert_eq!(
4411 named(back),
4412 [
4413 ("a", VariableKind::Parameter, Some(1)),
4414 ("sum", VariableKind::Return, Some(2)),
4415 ("twice", VariableKind::Local, Some(3))
4416 ]
4417 );
4418 }
4419
4420 #[test]
4421 fn locals_are_not_inferred_for_the_via_ir_pipeline_or_an_unknown_one() {
4422 let via_ir = StepMap::new(
4423 &outer_trace(),
4424 vec![legacy_contract(Some(CodeGenerator::ViaIr))],
4425 );
4426 assert!(matches!(
4427 via_ir.locals_at(5),
4428 LocalsStatus::Unavailable(reason) if reason.contains("via-IR pipeline")
4429 ));
4430 assert!(via_ir
4431 .inferred_variables(&outer_trace(), 5, None)
4432 .is_empty());
4433 let unknown = StepMap::new(&outer_trace(), vec![legacy_contract(None)]);
4434 assert!(matches!(
4435 unknown.locals_at(5),
4436 LocalsStatus::Unavailable(reason) if reason.contains("not known")
4437 ));
4438 }
4439
4440 #[test]
4441 fn steps_without_debug_info_have_no_location() {
4442 let map = StepMap::new(&outer_calls_inner(), Vec::new());
4443 assert!(!map.has_source());
4444 assert_eq!(map.line_key(3), None);
4445 assert_eq!(map.frame_depth(3), Some(0));
4446 assert_eq!(map.next_source(3), None);
4448 assert_eq!(map.previous_source(3), None);
4449 assert!(map.resolve_line(None, 3).is_err());
4450 assert_eq!(map.frames(3).len(), 1);
4451 }
4452
4453 #[test]
4454 fn steps_without_source_are_transparent_to_line_stepping() {
4455 let map = StepMap::new(
4457 &trace(vec![
4458 step(0, 1, "PUSH1", &[]),
4459 step(10, 1, "JUMPDEST", &[]),
4460 step(11, 1, "ADD", &[]),
4461 step(99, 1, "MUL", &[]),
4462 step(11, 1, "SWAP1", &[]),
4463 step(14, 1, "POP", &[]),
4464 step(30, 1, "STOP", &[]),
4465 ]),
4466 vec![contract(None)],
4467 );
4468 assert_eq!(map.line_key(3), None);
4469 assert_eq!(map.frame_depth(3), Some(1));
4470 assert_eq!(map.next_source(2), Some(5));
4472 assert_eq!(map.step_into(2), Some(5));
4473 assert!(!map.is_line_start(3));
4474 assert!(!map.is_line_start(4));
4475 assert_eq!(map.previous_source(5), Some(2));
4477 assert_eq!(map.previous_source(4), Some(2));
4478 assert_eq!(map.previous_source(3), Some(2));
4479 assert_eq!(map.previous_source(2), Some(1));
4480 }
4481
4482 #[test]
4483 fn generated_code_belongs_to_the_statement_and_the_epilogue_pops_the_function() {
4484 let map = StepMap::new(
4487 &trace(vec![
4488 step(0, 1, "PUSH1", &[]),
4489 step(10, 1, "JUMPDEST", &[]),
4490 step(11, 1, "ADD", &[]),
4491 step(40, 1, "JUMPDEST", &[]),
4492 step(40, 1, "SSTORE", &[]),
4493 step(11, 1, "SWAP1", &[]),
4494 step(14, 1, "POP", &[]),
4495 step(30, 1, "STOP", &[]),
4496 ]),
4497 vec![contract(None)],
4498 );
4499 let helper = map.location(4).expect("location");
4500 assert!(helper.generated);
4501 assert_eq!(helper.line, 3);
4502 assert_eq!(helper.function_name.as_deref(), Some("outer"));
4503 assert_eq!(map.line_key(4), key(3));
4504 assert_eq!(map.function_id(4).map(|id| id.function), Some(0));
4505 assert!(!map.is_line_start(4));
4506 assert!(!map.is_line_start(5));
4507 assert_eq!(map.frame_depth(4), Some(1));
4509 assert_eq!(map.frame_depth(7), Some(0));
4510 assert_eq!(map.line_key(7), None);
4511 assert!(!map.location(7).expect("location").generated);
4512 assert_eq!(map.next_source(2), Some(6));
4513 assert_eq!(map.finish(2), Some(7));
4514 assert_eq!(map.reverse_finish(4), Some(0));
4515 assert_eq!(map.previous_source(6), Some(2));
4516 }
4517
4518 #[test]
4519 fn a_lone_instruction_on_another_line_belongs_to_the_line_around_it() {
4520 let map = StepMap::new(
4523 &trace(vec![
4524 step(0, 1, "PUSH1", &[]),
4525 step(10, 1, "JUMPDEST", &[]),
4526 step(12, 1, "PUSH2", &[]),
4527 step(10, 1, "SWAP1", &[]),
4528 step(11, 1, "ADD", &[]),
4529 step(14, 1, "POP", &[]),
4530 step(30, 1, "STOP", &[]),
4531 ]),
4532 vec![contract(None)],
4533 );
4534 assert_eq!(map.line_key(2), key(2));
4535 assert_eq!(map.location(2).expect("location").line, 4);
4536 assert!(!map.is_line_start(2));
4537 assert!(!map.is_line_start(3));
4538 assert_eq!(map.next_source(1), Some(4));
4539 assert_eq!(map.previous_source(4), Some(1));
4540 assert_eq!(map.line_key(5), key(5));
4542 assert!(map.is_line_start(5));
4543 }
4544
4545 #[test]
4546 fn a_jump_onto_a_function_entry_is_a_call_even_when_it_is_active() {
4547 let map = StepMap::new(
4551 &trace(vec![
4552 step(0, 1, "PUSH1", &[]),
4553 step(1, 1, "JUMP", &[]),
4554 step(10, 1, "JUMPDEST", &[]),
4555 step(12, 1, "JUMP", &[]),
4556 step(20, 1, "JUMPDEST", &[]),
4557 step(21, 1, "JUMP", &[]),
4558 step(10, 1, "JUMPDEST", &[]),
4559 step(14, 1, "JUMP", &[]),
4560 step(21, 1, "SWAP1", &[]),
4561 step(21, 1, "JUMP", &[]),
4562 step(13, 1, "JUMPDEST", &[]),
4563 step(14, 1, "POP", &[]),
4564 step(30, 1, "STOP", &[]),
4565 ]),
4566 vec![contract(None)],
4567 );
4568 let contract = &map.contracts()[0];
4569 assert_eq!(contract.function_entry_at_pc(10), Some(0));
4570 assert_eq!(contract.function_entry_at_pc(20), Some(1));
4571 assert_eq!(contract.function_entry_at_pc(11), None);
4572 let depths = (0..13)
4573 .map(|step| map.frame_depth(step).expect("depth"))
4574 .collect::<Vec<_>>();
4575 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 1, 1, 0]);
4576 assert!(map.is_frame_entry(6));
4577 let frames = map.frames(6);
4578 assert_eq!(
4579 frames
4580 .iter()
4581 .map(|frame| frame.function_name.as_deref().unwrap_or("-"))
4582 .collect::<Vec<_>>(),
4583 vec!["outer", "inner", "outer", "-"]
4584 );
4585 assert_eq!(map.finish(6), Some(8));
4586 assert_eq!(map.reverse_finish(6), Some(5));
4587 assert_eq!(map.finish(4), Some(10));
4588 }
4589
4590 #[test]
4591 fn a_recursive_call_returns_at_the_address_read_off_the_stack() {
4592 let map = StepMap::new(
4597 &trace(vec![
4598 step(0, 1, "PUSH1", &[]),
4599 step(1, 1, "JUMP", &[]),
4600 step(10, 1, "JUMPDEST", &[]),
4601 step(12, 1, "JUMP", &["0xd", "0x1", "0xa"]),
4602 step(10, 1, "JUMPDEST", &["0x1"]),
4603 step(11, 1, "ADD", &["0x2"]),
4604 step(14, 1, "JUMP", &["0xd"]),
4605 step(13, 1, "JUMPDEST", &[]),
4606 step(14, 1, "POP", &[]),
4607 step(30, 1, "STOP", &[]),
4608 ]),
4609 vec![contract(None)],
4610 );
4611 let depths = (0..10)
4612 .map(|step| map.frame_depth(step).expect("depth"))
4613 .collect::<Vec<_>>();
4614 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 2, 1, 1, 0]);
4615 assert!(map.is_frame_entry(4));
4616 assert_eq!(map.finish(4), Some(7));
4617 assert_eq!(map.frames(5).len(), 3);
4618 assert_eq!(map.frames(7).len(), 2);
4619 assert!(map.contracts()[0].is_jumpdest(13));
4620 assert!(!map.contracts()[0].is_jumpdest(99));
4621 }
4622
4623 #[test]
4624 fn jump_markers_decide_calls_and_returns_without_entry_points() {
4625 let whole = SOURCE.len() as u64;
4630 let info = EthdebugInfo {
4631 compilation: serde_json::Value::Null,
4632 contract_name: "C".to_owned(),
4633 environment: "runtime".to_owned(),
4634 instructions: vec![
4635 instruction(0, 0, whole),
4636 instruction(1, 0, whole),
4637 instruction(10, offset_of("function outer"), 90),
4638 instruction(11, offset_of("uint256 b = a + 1;"), 18),
4639 instruction_with(
4640 12,
4641 offset_of("inner(b);"),
4642 9,
4643 json!({"invoke": {"identifier": "outer"}}),
4644 ),
4645 instruction(13, offset_of("inner(b);"), 9),
4646 instruction_with(14, offset_of("b = 0;"), 6, json!({"return": {}})),
4647 instruction(30, 0, whole),
4648 ],
4649 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4650 variable_locations: BTreeMap::new(),
4651 };
4652 let marked =
4653 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4654 assert_eq!(marked.jump_marker_at_pc(12), JumpMarker::Call);
4655 assert_eq!(marked.jump_marker_at_pc(14), JumpMarker::Return);
4656 assert_eq!(marked.jump_marker_at_pc(13), JumpMarker::None);
4657 let map = StepMap::new(
4658 &trace(vec![
4659 step(0, 1, "PUSH1", &[]),
4660 step(1, 1, "JUMP", &[]),
4661 step(10, 1, "JUMPDEST", &[]),
4662 step(12, 1, "JUMP", &[]),
4663 step(11, 1, "ADD", &[]),
4664 step(14, 1, "JUMP", &[]),
4665 step(13, 1, "JUMPDEST", &[]),
4666 step(14, 1, "POP", &[]),
4667 step(30, 1, "STOP", &[]),
4668 ]),
4669 vec![marked],
4670 );
4671 let depths = (0..9)
4672 .map(|step| map.frame_depth(step).expect("depth"))
4673 .collect::<Vec<_>>();
4674 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 1, 0]);
4675 assert!(!map.is_frame_entry(4));
4676 assert_eq!(map.frames(4).len(), 2);
4677 assert_eq!(map.finish(4), Some(8));
4678 let map = StepMap::new(
4680 &trace(vec![
4681 step(0, 1, "PUSH1", &[]),
4682 step(1, 1, "JUMP", &[]),
4683 step(10, 1, "JUMPDEST", &[]),
4684 step(12, 1, "JUMP", &[]),
4685 step(20, 1, "JUMPDEST", &[]),
4686 step(21, 1, "JUMP", &[]),
4687 step(13, 1, "JUMPDEST", &[]),
4688 step(30, 1, "STOP", &[]),
4689 ]),
4690 vec![contract(None)],
4691 );
4692 assert_eq!(map.frame_depth(4), Some(2));
4693 assert_eq!(map.frame_depth(6), Some(1));
4694 }
4695
4696 #[test]
4697 fn marked_jumps_into_generated_helpers_do_not_pop_the_function() {
4698 let whole = SOURCE.len() as u64;
4702 let info = EthdebugInfo {
4703 compilation: serde_json::Value::Null,
4704 contract_name: "C".to_owned(),
4705 environment: "runtime".to_owned(),
4706 instructions: vec![
4707 instruction(0, 0, whole),
4708 instruction(1, 0, whole),
4709 instruction(10, offset_of("function outer"), 90),
4710 instruction_with(
4711 11,
4712 offset_of("uint256 b = a + 1;"),
4713 18,
4714 json!({"invoke": {}}),
4715 ),
4716 instruction_with(40, 0, whole, json!({"return": {}})),
4717 instruction(14, offset_of("b = 0;"), 6),
4718 instruction(30, 0, whole),
4719 ],
4720 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4721 variable_locations: BTreeMap::new(),
4722 };
4723 let contract =
4724 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4725 let map = StepMap::new(
4726 &trace(vec![
4727 step(0, 1, "PUSH1", &[]),
4728 step(1, 1, "JUMP", &[]),
4729 step(10, 1, "JUMPDEST", &[]),
4730 step(11, 1, "JUMP", &[]),
4731 step(40, 1, "JUMPDEST", &[]),
4732 step(40, 1, "JUMP", &[]),
4733 step(14, 1, "JUMPDEST", &[]),
4734 step(30, 1, "STOP", &[]),
4735 ]),
4736 vec![contract],
4737 );
4738 let depths = (0..8)
4739 .map(|step| map.frame_depth(step).expect("depth"))
4740 .collect::<Vec<_>>();
4741 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 0]);
4742 assert!(map.location(4).expect("location").generated);
4743 assert_eq!(
4744 map.location(6).expect("location").function_name.as_deref(),
4745 Some("outer")
4746 );
4747 assert_eq!(map.next_source(3), Some(6));
4748 assert_eq!(map.finish(3), Some(7));
4749 }
4750
4751 fn random_steps(seed: u64, count: usize) -> Vec<StepInfo> {
4753 let mut state = seed;
4754 let mut next = |bound: u64| {
4755 state = state
4756 .wrapping_mul(6_364_136_223_846_793_005)
4757 .wrapping_add(1_442_695_040_888_963_407);
4758 (state >> 33) % bound
4759 };
4760 (0..count)
4761 .map(|_| {
4762 let line = next(5);
4763 StepInfo {
4764 location: None,
4765 generated: false,
4766 key: (line > 0).then_some(LineKey {
4767 contract: 0,
4768 source_id: 0,
4769 line,
4770 }),
4771 contract: None,
4772 address: None,
4773 storage: None,
4774 evm_depth: 0,
4775 starts_root: next(40) == 0,
4776 frame_depth: next(6) as u32,
4777 frame_entry: false,
4778 line_start: false,
4779 }
4780 })
4781 .collect()
4782 }
4783
4784 #[test]
4785 fn line_passes_match_their_definitions() {
4786 for seed in 0..200 {
4787 let steps = random_steps(seed, 300);
4788
4789 let mut smoothed = steps.clone();
4791 StepMap::smooth_single_step_excursions(&mut smoothed);
4792 let mut expected = steps.clone();
4793 let mut after = vec![None; expected.len()];
4794 let mut lines = NearestLines::default();
4795 for (index, step) in expected.iter().enumerate().rev() {
4796 after[index] = lines.visit(step.frame_depth, step.key);
4797 if step.starts_root {
4798 lines.clear();
4799 }
4800 }
4801 let mut lines = NearestLines::default();
4802 for (step, after) in expected.iter_mut().zip(after) {
4803 if step.starts_root {
4804 lines.clear();
4805 }
4806 let before = lines.visit(step.frame_depth, step.key);
4807 let Some(key) = step.key else {
4808 continue;
4809 };
4810 if before.is_some() && before == after && before != Some(key) {
4811 step.key = before;
4812 }
4813 }
4814 assert_eq!(smoothed, expected, "smoothing, seed {seed}");
4815
4816 let mut map = StepMap::new(&trace(Vec::new()), Vec::new());
4819 map.steps = steps.clone();
4820 map.mark_line_starts();
4821 let mut last = Vec::<Option<LineKey>>::new();
4822 for (index, step) in steps.iter().enumerate() {
4823 if step.starts_root {
4824 last.clear();
4825 }
4826 let depth = step.frame_depth as usize;
4827 if last.len() <= depth {
4828 last.resize(depth + 1, None);
4829 }
4830 let Some(key) = step.key else {
4831 assert!(!map.steps[index].line_start, "seed {seed} step {index}");
4832 continue;
4833 };
4834 assert_eq!(
4835 map.steps[index].line_start,
4836 last[depth] != Some(key),
4837 "line start, seed {seed} step {index}"
4838 );
4839 for entry in last.iter_mut().skip(depth) {
4840 *entry = Some(key);
4841 }
4842 }
4843 }
4844 }
4845
4846 #[test]
4847 fn contracts_sharing_a_source_read_it_once() {
4848 let built = contract(Some("0xabcd"));
4849 let mut cache = SourceCache::default();
4850 let mut through_cache = || {
4851 ContractDebugInfo::with_source_cache(
4852 Some("0xabcd"),
4853 "C",
4854 built.info.clone(),
4855 built.source_contents.clone(),
4856 &mut cache,
4857 )
4858 };
4859 let first = through_cache();
4860 let second = through_cache();
4861 assert_eq!(first, built);
4862 assert_eq!(second, built);
4863 assert_eq!(cache.sources[&0].len(), 1);
4864
4865 let other = ContractDebugInfo::with_source_cache(
4868 None,
4869 "D",
4870 built.info.clone(),
4871 BTreeMap::from([(0, "contract D { function g() public {} }".to_owned())]),
4872 &mut cache,
4873 );
4874 assert_eq!(other.functions[0].name, "g");
4875 assert_eq!(cache.sources[&0].len(), 2);
4876 }
4877
4878 #[test]
4879 fn contract_info_indexes_lines_and_functions() {
4880 let contract = contract(Some("0xABCD"));
4881 assert_eq!(contract.address.as_deref(), Some("0xabcd"));
4882 assert_eq!(contract.line_of(0, 0), Some(1));
4883 assert_eq!(
4884 contract.line_of(0, SOURCE.find("inner(b)").unwrap() as u64),
4885 Some(4)
4886 );
4887 assert_eq!(contract.line_count(0), Some(11));
4888 assert_eq!(contract.line_text(0, 8).map(str::trim), Some("x += 1;"));
4889 assert_eq!(contract.line_text(0, 99), None);
4890 assert_eq!(
4891 contract.function_at_pc(21).map(|f| f.name.as_str()),
4892 Some("inner")
4893 );
4894 assert_eq!(contract.function_at_pc(0), None);
4895 assert_eq!(contract.function_at_pc(999), None);
4896 assert_eq!(contract.functions[0].declaration_line, 2);
4897 assert_eq!(contract.effective_line(0, 3), Some(3));
4898 assert_eq!(contract.effective_line(0, 6), Some(1));
4899 assert_eq!(contract.effective_line(0, 0), None);
4900 assert_eq!(contract.effective_line(0, 12), None);
4901 assert_eq!(contract.effective_line(7, 1), None);
4902
4903 assert_eq!(normalize_address("0XAbC"), "0xabc");
4904 assert_eq!(
4905 address_from_word("0x000000000000000000000000AAAA000000000000000000000000000000000001"),
4906 Some("0xaaaa000000000000000000000000000000000001".to_owned())
4907 );
4908 assert_eq!(address_from_word("0x0"), None);
4909 assert_eq!(address_from_word("zz"), None);
4910 }
4911
4912 #[test]
4914 fn prepared_instruction_facts_match_the_instructions() {
4915 let whole = SOURCE.len() as u64;
4916 let outer = offset_of("function outer");
4917 let statement = offset_of("x += 1;");
4918 let instructions = vec![
4919 Instruction::new(0, "PUSH1", Some(json!({"code": source_range(0, 0, whole)}))),
4920 Instruction::new(
4922 21,
4923 "JUMP",
4924 Some(json!({"code": source_range(0, statement, 7), "invoke": {"identifier": "f"}})),
4925 ),
4926 Instruction::new(
4927 22,
4928 "JUMP",
4929 Some(json!({"code": source_range(0, statement, 7), "return": {}})),
4930 ),
4931 Instruction::new(
4932 23,
4933 "REVERT",
4934 Some(json!({"code": source_range(0, statement, 7), "revert": {}})),
4935 ),
4936 Instruction::new(30, "STOP", None),
4938 Instruction::new(31, "POP", Some(json!({}))),
4939 Instruction::new(
4942 40,
4943 "JUMP",
4944 Some(json!({"pick": [
4945 {"code": source_range(0, statement, 7), "invoke": {}},
4946 {"code": source_range(0, outer, 90)}
4947 ]})),
4948 ),
4949 Instruction::new(
4950 42,
4951 "ADD",
4952 Some(json!({"pick": [
4953 {"code": source_range(0, statement, 7)},
4954 {"code": source_range(0, statement, 7)}
4955 ]})),
4956 ),
4957 Instruction::new(
4959 50,
4960 "ADD",
4961 Some(json!({"code": source_range(7, statement, 7)})),
4962 ),
4963 Instruction::new(60, "ADD", Some(json!({"code": source_range(0, 0, whole)}))),
4965 Instruction::new(
4966 60,
4967 "JUMPDEST",
4968 Some(json!({"code": source_range(0, statement, 7)})),
4969 ),
4970 Instruction::new(
4972 1 << 40,
4973 "JUMPDEST",
4974 Some(json!({"code": source_range(0, statement, 7), "return": {}})),
4975 ),
4976 ];
4977 let info = EthdebugInfo {
4978 compilation: serde_json::Value::Null,
4979 contract_name: "C".to_owned(),
4980 environment: "call".to_owned(),
4981 instructions,
4982 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4983 variable_locations: BTreeMap::new(),
4984 };
4985 let contract =
4986 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4987 let line = |pc| contract.facts_at_pc(pc).and_then(|facts| facts.line);
4988 let function = |pc| {
4989 contract
4990 .function_at_pc(pc)
4991 .map(|function| function.name.as_str())
4992 };
4993
4994 assert_eq!(contract.jump_marker_at_pc(21), JumpMarker::Call);
4995 assert_eq!(contract.jump_marker_at_pc(22), JumpMarker::Return);
4996 assert_eq!(contract.jump_marker_at_pc(23), JumpMarker::None);
4997 assert_eq!(contract.jump_marker_at_pc(40), JumpMarker::Call);
4998 assert_eq!(contract.location_at_pc(30), None);
4999 assert_eq!(contract.location_at_pc(31), None);
5000 assert_eq!(contract.location_at_pc(40), None);
5001 assert_eq!(contract.location_at_pc(42), contract.location_at_pc(21));
5002 assert_eq!(line(21), contract.line_of(0, statement));
5003 assert_eq!(function(21), Some("inner"));
5004 assert!(contract.location_at_pc(50).is_some());
5005 assert_eq!(line(50), None);
5006 assert_eq!(function(60), Some("inner"));
5007 assert!(contract.is_jumpdest(60));
5008 assert_eq!(contract.jump_marker_at_pc(1 << 40), JumpMarker::Return);
5009 assert!(contract.pc_index.sparse.contains_key(&(1 << 40)));
5010 for pc in [1, 61, (1 << 40) + 1, u64::MAX] {
5012 assert_eq!(contract.instruction_at_pc(pc), None, "pc {pc}");
5013 assert_eq!(contract.location_at_pc(pc), None, "pc {pc}");
5014 }
5015 }
5016
5017 #[test]
5018 fn function_entries_are_the_earliest_jumpdest_at_each_declaration() {
5019 let source = "\
5022contract D {
5023 function a() public { uint256 x = 1; }
5024 function b() public { uint256 y = 2; }
5025 function c() public { uint256 z = 3; }
5026 function d() public { uint256 w = 4; }
5027}
5028";
5029 let start = |needle: &str| source.find(needle).expect(needle) as u64;
5030 let (a, b, c) = (
5031 start("function a"),
5032 start("function b"),
5033 start("function c"),
5034 );
5035 let instructions = vec![
5036 Instruction::new(3, "JUMP", Some(json!({"code": source_range(0, a, 10)}))),
5038 Instruction::new(
5040 30,
5041 "JUMPDEST",
5042 Some(json!({"code": source_range(0, a, 10)})),
5043 ),
5044 Instruction::new(
5045 10,
5046 "JUMPDEST",
5047 Some(json!({"code": source_range(0, a, 10)})),
5048 ),
5049 Instruction::new(
5050 40,
5051 "JUMPDEST",
5052 Some(json!({"code": source_range(0, b, 10)})),
5053 ),
5054 Instruction::new(50, "JUMP", Some(json!({"code": source_range(0, c, 10)}))),
5057 Instruction::new(
5058 51,
5059 "JUMPDEST",
5060 Some(json!({"code": source_range(0, c + 22, 10)})),
5061 ),
5062 Instruction::new(5, "JUMPDEST", Some(json!({"code": source_range(1, a, 10)}))),
5064 Instruction::new(
5066 20,
5067 "JUMPDEST",
5068 Some(json!({"code": source_range(1, b, 10)})),
5069 ),
5070 Instruction::new(20, "ADD", Some(json!({"code": source_range(1, b, 10)}))),
5071 ];
5072 let info = EthdebugInfo {
5073 compilation: serde_json::Value::Null,
5074 contract_name: "D".to_owned(),
5075 environment: "call".to_owned(),
5076 instructions,
5077 sources: BTreeMap::from([(0, "D.sol".to_owned()), (1, "E.sol".to_owned())]),
5078 variable_locations: BTreeMap::new(),
5079 };
5080 let contract = ContractDebugInfo::new(
5081 None,
5082 "D",
5083 info,
5084 BTreeMap::from([(0, source.to_owned()), (1, source.to_owned())]),
5085 );
5086 let names = contract
5087 .functions
5088 .iter()
5089 .map(|function| (function.source_id, function.name.as_str()))
5090 .collect::<Vec<_>>();
5091 assert_eq!(
5092 names,
5093 [
5094 (0, "a"),
5095 (0, "b"),
5096 (0, "c"),
5097 (0, "d"),
5098 (1, "a"),
5099 (1, "b"),
5100 (1, "c"),
5101 (1, "d")
5102 ]
5103 );
5104 let entries = [3, 5, 10, 20, 30, 40, 50, 51, 999]
5105 .into_iter()
5106 .map(|pc| (pc, contract.function_entry_at_pc(pc)))
5107 .collect::<Vec<_>>();
5108 assert_eq!(
5109 entries,
5110 [
5111 (3, None),
5112 (5, Some(4)),
5113 (10, Some(0)),
5114 (20, Some(5)),
5115 (30, None),
5116 (40, Some(1)),
5117 (50, None),
5118 (51, None),
5119 (999, None)
5120 ]
5121 );
5122 assert!(!contract.is_jumpdest(20));
5123 }
5124
5125 const SECOND_ROOT: &str = "0xbbbb000000000000000000000000000000000002";
5126
5127 fn two_roots() -> TransactionTrace {
5131 let mut trace = outer_calls_inner();
5132 let again = trace.steps.to_steps();
5133 trace.steps.extend(again);
5134 let mut first = call(0, 0, 11, "CALL", ROOT);
5135 first.input = "0x01".to_owned();
5136 let mut second = call(1, 11, 22, "CALL", SECOND_ROOT);
5137 second.input = "0x02".to_owned();
5138 trace.artifacts.calls = vec![first, second];
5139 trace
5140 }
5141
5142 #[test]
5143 fn each_root_call_maps_through_its_own_contract() {
5144 let trace = two_roots();
5145 let map = StepMap::new(
5146 &trace,
5147 vec![contract(Some(ROOT)), contract(Some(SECOND_ROOT))],
5148 );
5149 let address_at = |step: usize| {
5150 map.contract_at_step(step)
5151 .and_then(|contract| contract.address.clone())
5152 };
5153 assert_eq!(address_at(3).as_deref(), Some(ROOT));
5154 assert_eq!(address_at(14).as_deref(), Some(SECOND_ROOT));
5155
5156 let frames = map.frames(17);
5158 assert_eq!(frames.len(), 3);
5159 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
5160 assert!(frames[2].external);
5161 assert_eq!(frames[2].entry_step, 11);
5162 assert_eq!(frames[2].address.as_deref(), Some(SECOND_ROOT));
5163
5164 let next = map.next_source(10).expect("a line in the second call");
5166 assert!(next > 10);
5167 assert_eq!(address_at(next).as_deref(), Some(SECOND_ROOT));
5168
5169 assert_eq!(map.calldata_at(&trace, 3), "0x01");
5170 assert_eq!(map.calldata_at(&trace, 14), "0x02");
5171 }
5172
5173 #[test]
5174 fn finish_from_a_root_frame_stops_where_the_next_root_starts() {
5175 let map = StepMap::new(
5176 &two_roots(),
5177 vec![contract(Some(ROOT)), contract(Some(SECOND_ROOT))],
5178 );
5179 assert_eq!(map.finish(1), Some(11));
5181 assert_eq!(map.finish(10), Some(11));
5182 assert_eq!(map.reverse_finish(12), Some(11));
5184 assert_eq!(map.finish(12), None);
5186 assert_eq!(map.reverse_finish(1), None);
5187 }
5188
5189 #[test]
5190 fn reverse_next_stays_within_a_root() {
5191 let mut trace = outer_calls_inner();
5194 trace.steps.truncate(10);
5195 trace.steps.push(step(10, 1, "JUMP", &[]));
5196 trace.steps.extend(outer_calls_inner().steps.to_steps());
5197 trace.artifacts.calls = vec![call(0, 0, 11, "CALL", ROOT), call(1, 11, 22, "CALL", ROOT)];
5198 let map = StepMap::new(&trace, vec![contract(Some(ROOT))]);
5199 assert_eq!(map.line_key(10), map.line_key(13));
5200 assert_eq!(map.previous_source(14), Some(13));
5201 }
5202
5203 #[test]
5204 fn a_single_contract_describes_only_a_single_root() {
5205 let map = StepMap::new(&two_roots(), vec![contract(None)]);
5206 assert!(map.contract_at_step(3).is_none());
5207 assert!(map.contract_at_step(14).is_none());
5208 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
5209 assert!(map.contract_at_step(3).is_some());
5210 }
5211
5212 #[test]
5213 fn smoothing_stays_within_a_root() {
5214 let mut trace = outer_calls_inner();
5217 trace.steps.truncate(10);
5218 trace.steps.push(step(10, 1, "JUMPDEST", &[]));
5219 trace.steps.push(step(14, 1, "POP", &[]));
5220 trace.steps.push(step(30, 1, "STOP", &[]));
5221 trace.artifacts.calls = vec![call(0, 0, 10, "CALL", ROOT), call(1, 10, 13, "CALL", ROOT)];
5222 let map = StepMap::new(&trace, vec![contract(Some(ROOT))]);
5223 assert_eq!(map.frames(10)[0].function_name.as_deref(), Some("outer"));
5224 assert_ne!(map.line_key(10), map.line_key(9));
5225 assert_eq!(map.line_key(9), map.line_key(11));
5226 }
5227
5228 fn step_info(depth: u32, line: Option<u64>, starts_root: bool) -> StepInfo {
5230 StepInfo {
5231 location: None,
5232 generated: false,
5233 key: line.and_then(key),
5234 contract: None,
5235 address: None,
5236 storage: None,
5237 evm_depth: 0,
5238 starts_root,
5239 frame_depth: depth,
5240 frame_entry: false,
5241 line_start: false,
5242 }
5243 }
5244
5245 #[test]
5246 fn smoothing_looks_past_deeper_frames_only() {
5247 let mut steps = vec![
5248 step_info(1, Some(1), false),
5249 step_info(1, Some(2), false),
5252 step_info(2, Some(3), false),
5253 step_info(3, Some(4), false),
5254 step_info(4, Some(5), false),
5255 step_info(3, Some(6), false),
5256 step_info(1, None, false),
5257 step_info(1, Some(1), false),
5258 step_info(0, Some(7), false),
5260 step_info(1, Some(8), false),
5261 step_info(1, Some(1), false),
5262 step_info(1, Some(9), false),
5264 step_info(0, Some(7), false),
5265 step_info(1, Some(1), false),
5266 step_info(1, Some(2), false),
5268 step_info(1, Some(1), true),
5269 ];
5270 let mut expected = steps
5271 .iter()
5272 .map(|step| step.key.map(|key| key.line))
5273 .collect::<Vec<_>>();
5274 expected[1] = Some(1);
5275 StepMap::smooth_single_step_excursions(&mut steps);
5276 let lines = steps
5277 .iter()
5278 .map(|step| step.key.map(|key| key.line))
5279 .collect::<Vec<_>>();
5280 assert_eq!(lines, expected);
5281 }
5282}