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 code_hash: Option<String>,
75 pub name: String,
76 pub info: EthdebugInfo,
77 pub source_contents: BTreeMap<u64, String>,
78 pub functions: Vec<SourceFunction>,
79 pub types: SourceTypes,
82 pub storage_layout: Option<StorageLayout>,
85 pub transient_storage_layout: Option<StorageLayout>,
87 pub code_generator: Option<CodeGenerator>,
91 line_starts: BTreeMap<u64, Vec<usize>>,
93 pc_index: PcIndex,
95 facts: Vec<InstructionFacts>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103struct InstructionFacts {
104 location: Option<SourceLocation>,
106 line: Option<u64>,
108 function: Option<usize>,
110 entry: Option<usize>,
113 marker: JumpMarker,
115 jumpdest: bool,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
121struct PcIndex {
122 dense: Vec<Option<NonZeroU32>>,
125 sparse: FxHashMap<u64, usize>,
127}
128
129impl PcIndex {
130 fn new(instructions: &[Instruction]) -> Self {
133 let bound = instructions.len().saturating_mul(33);
137 let dense_len = instructions
138 .iter()
139 .filter_map(|instruction| usize::try_from(instruction.offset).ok())
140 .filter(|offset| *offset < bound)
141 .max()
142 .map_or(0, |offset| offset + 1);
143 let mut dense = vec![None; dense_len];
144 let mut sparse = FxHashMap::default();
145 for (index, instruction) in instructions.iter().enumerate() {
146 let slot = usize::try_from(instruction.offset)
147 .ok()
148 .and_then(|offset| dense.get_mut(offset));
149 let entry = u32::try_from(index + 1).ok().and_then(NonZeroU32::new);
150 match (slot, entry) {
151 (Some(slot), Some(entry)) => *slot = Some(entry),
152 (slot, _) => {
153 if let Some(slot) = slot {
154 *slot = None;
155 }
156 sparse.insert(instruction.offset, index);
157 }
158 }
159 }
160 Self { dense, sparse }
161 }
162
163 fn get(&self, pc: u64) -> Option<usize> {
164 let dense = usize::try_from(pc)
165 .ok()
166 .and_then(|pc| self.dense.get(pc).copied().flatten());
167 match dense {
168 Some(entry) => Some(entry.get() as usize - 1),
169 None => self.sparse.get(&pc).copied(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum CodeEnvironment {
178 Create,
179 Call,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum JumpMarker {
185 None,
186 Call,
188 Return,
190}
191
192#[derive(Debug, Default)]
197pub struct SourceCache {
198 sources: FxHashMap<u64, Vec<Arc<ReadSource>>>,
201}
202
203#[derive(Debug)]
204struct ReadSource {
205 text: String,
206 functions: Vec<SourceFunction>,
207 types: SourceTypes,
208 line_starts: Vec<usize>,
209}
210
211impl SourceCache {
212 fn read(&mut self, source_id: u64, text: &str) -> Arc<ReadSource> {
214 let read = self.sources.entry(source_id).or_default();
215 if let Some(source) = read.iter().find(|source| source.text == text) {
216 return Arc::clone(source);
217 }
218 let mut types = SourceTypes::default();
219 types.add_source(source_id, text);
220 let source = Arc::new(ReadSource {
221 text: text.to_owned(),
222 functions: parse_source_functions(source_id, text),
223 types,
224 line_starts: line_starts(text),
225 });
226 read.push(Arc::clone(&source));
227 source
228 }
229}
230
231impl ContractDebugInfo {
232 #[must_use]
236 pub fn new(
237 address: Option<&str>,
238 name: &str,
239 info: EthdebugInfo,
240 source_contents: BTreeMap<u64, String>,
241 ) -> Self {
242 Self::with_source_cache(
243 address,
244 name,
245 info,
246 source_contents,
247 &mut SourceCache::default(),
248 )
249 }
250
251 #[must_use]
254 pub fn with_source_cache(
255 address: Option<&str>,
256 name: &str,
257 info: EthdebugInfo,
258 source_contents: BTreeMap<u64, String>,
259 cache: &mut SourceCache,
260 ) -> Self {
261 let read = source_contents
262 .iter()
263 .map(|(source_id, text)| (*source_id, cache.read(*source_id, text)))
264 .collect::<Vec<_>>();
265 let functions = read
266 .iter()
267 .flat_map(|(_, source)| source.functions.iter().cloned())
268 .collect::<Vec<SourceFunction>>();
269 let mut types = SourceTypes::default();
270 for (_, source) in &read {
271 types.extend_from(&source.types);
272 }
273 let line_starts = read
274 .iter()
275 .map(|(source_id, source)| (*source_id, source.line_starts.clone()))
276 .collect();
277 let pc_index = PcIndex::new(&info.instructions);
278 let locations = info
279 .instructions
280 .iter()
281 .map(Instruction::source_location)
282 .collect::<Vec<_>>();
283 let jumpdests = info
284 .instructions
285 .iter()
286 .map(|instruction| instruction.mnemonic() == Some("JUMPDEST"))
287 .collect::<Vec<_>>();
288 let mut earliest_jumpdests = FxHashMap::<(u64, u64), u64>::default();
291 for (index, instruction) in info.instructions.iter().enumerate() {
292 if !jumpdests[index] {
293 continue;
294 }
295 let Some(location) = &locations[index] else {
296 continue;
297 };
298 let earliest = earliest_jumpdests
299 .entry((location.source_id, location.offset))
300 .or_insert(instruction.offset);
301 *earliest = (*earliest).min(instruction.offset);
302 }
303 let function_entries = functions
305 .iter()
306 .enumerate()
307 .filter_map(|(function_index, function)| {
308 earliest_jumpdests
309 .get(&(function.source_id, function.declaration_start))
310 .map(|pc| (*pc, function_index))
311 })
312 .collect::<FxHashMap<_, _>>();
313 let mut contract = Self {
314 address: address.map(normalize_address),
315 code_hash: None,
316 name: name.to_owned(),
317 info,
318 source_contents,
319 functions,
320 types,
321 storage_layout: None,
322 transient_storage_layout: None,
323 code_generator: None,
324 line_starts,
325 pc_index,
326 facts: Vec::new(),
327 };
328 contract.facts = contract.instruction_facts(locations, &jumpdests, &function_entries);
329 contract
330 }
331
332 fn instruction_facts(
335 &self,
336 locations: Vec<Option<SourceLocation>>,
337 jumpdests: &[bool],
338 function_entries: &FxHashMap<u64, usize>,
339 ) -> Vec<InstructionFacts> {
340 let mut by_source = FxHashMap::<u64, Vec<usize>>::default();
342 for (index, function) in self.functions.iter().enumerate() {
343 by_source.entry(function.source_id).or_default().push(index);
344 }
345 let mut found = FxHashMap::<(u64, u64), (Option<u64>, Option<usize>)>::default();
348 let mut line_and_function = |location: &SourceLocation| {
349 *found
350 .entry((location.source_id, location.offset))
351 .or_insert_with(|| {
352 let function = by_source.get(&location.source_id).and_then(|candidates| {
353 narrowest_function(&self.functions, candidates, location.offset)
354 });
355 (self.line_of(location.source_id, location.offset), function)
356 })
357 };
358 self.info
359 .instructions
360 .iter()
361 .zip(locations)
362 .zip(jumpdests)
363 .map(|((instruction, location), jumpdest)| {
364 let (line, function) = location
365 .as_ref()
366 .map_or((None, None), &mut line_and_function);
367 InstructionFacts {
368 line,
369 function,
370 entry: function_entries.get(&instruction.offset).copied(),
371 marker: jump_marker(instruction),
372 jumpdest: *jumpdest,
373 location,
374 }
375 })
376 .collect()
377 }
378
379 fn facts_at_pc(&self, pc: u64) -> Option<&InstructionFacts> {
381 self.facts.get(self.pc_index.get(pc)?)
382 }
383
384 #[must_use]
386 pub const fn with_code_generator(mut self, code_generator: Option<CodeGenerator>) -> Self {
387 self.code_generator = code_generator;
388 self
389 }
390
391 #[must_use]
394 pub fn with_code_hash(mut self, code_hash: Option<&str>) -> Self {
395 self.code_hash = code_hash.map(normalize_address);
396 self
397 }
398
399 #[must_use]
401 pub fn with_storage_layout(mut self, storage_layout: Option<StorageLayout>) -> Self {
402 self.storage_layout = self.with_enum_variants(storage_layout);
403 self
404 }
405
406 #[must_use]
408 pub fn with_transient_storage_layout(mut self, layout: Option<StorageLayout>) -> Self {
409 self.transient_storage_layout = self.with_enum_variants(layout);
410 self
411 }
412
413 fn with_enum_variants(&self, layout: Option<StorageLayout>) -> Option<StorageLayout> {
415 layout.map(|mut layout| {
416 layout.enum_variants = self.types.enum_variants_by_name();
417 layout
418 })
419 }
420
421 fn code_environment(&self) -> Option<CodeEnvironment> {
424 match self.info.environment.as_str() {
425 "create" => Some(CodeEnvironment::Create),
426 "call" => Some(CodeEnvironment::Call),
427 _ => None,
428 }
429 }
430
431 #[must_use]
433 pub fn function_entry_at_pc(&self, pc: u64) -> Option<usize> {
434 self.facts_at_pc(pc).and_then(|facts| facts.entry)
435 }
436
437 #[must_use]
439 pub fn is_jumpdest(&self, pc: u64) -> bool {
440 self.facts_at_pc(pc).is_some_and(|facts| facts.jumpdest)
441 }
442
443 #[must_use]
445 pub fn jump_marker_at_pc(&self, pc: u64) -> JumpMarker {
446 self.facts_at_pc(pc)
447 .map_or(JumpMarker::None, |facts| facts.marker)
448 }
449
450 #[must_use]
451 pub fn source_path(&self, source_id: u64) -> Option<&str> {
452 self.info.sources.get(&source_id).map(String::as_str)
453 }
454
455 #[must_use]
457 pub fn line_of(&self, source_id: u64, offset: u64) -> Option<u64> {
458 let starts = self.line_starts.get(&source_id)?;
459 let offset = usize::try_from(offset).ok()?;
460 Some(starts.partition_point(|start| *start <= offset) as u64)
461 }
462
463 #[must_use]
464 pub fn column_of(&self, source_id: u64, offset: u64) -> Option<u64> {
465 let starts = self.line_starts.get(&source_id)?;
466 let line = self.line_of(source_id, offset)?;
467 let line_start = *starts.get(usize::try_from(line).ok()?.checked_sub(1)?)?;
468 Some(offset.saturating_sub(line_start as u64) + 1)
469 }
470
471 #[must_use]
472 pub fn line_count(&self, source_id: u64) -> Option<u64> {
473 self.line_starts
474 .get(&source_id)
475 .map(|starts| starts.len() as u64)
476 }
477
478 #[must_use]
480 pub fn line_text(&self, source_id: u64, line: u64) -> Option<&str> {
481 let source = self.source_contents.get(&source_id)?;
482 let index = usize::try_from(line).ok()?.checked_sub(1)?;
483 source.lines().nth(index)
484 }
485
486 #[must_use]
488 pub fn location_at_pc(&self, pc: u64) -> Option<SourceLocation> {
489 self.facts_at_pc(pc)?.location.clone()
490 }
491
492 #[must_use]
494 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
495 self.info.instructions.get(self.pc_index.get(pc)?)
496 }
497
498 #[must_use]
500 pub fn modifier_depth_at_pc(&self, pc: u64) -> Option<i64> {
501 self.instruction_at_pc(pc)?.modifier_depth()
502 }
503
504 #[must_use]
506 pub fn function_for_location(&self, location: &SourceLocation) -> Option<usize> {
507 let candidates = (0..self.functions.len())
508 .filter(|index| self.functions[*index].source_id == location.source_id)
509 .collect::<Vec<_>>();
510 narrowest_function(&self.functions, &candidates, location.offset)
511 }
512
513 #[must_use]
514 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
515 self.functions.get(self.facts_at_pc(pc)?.function?)
516 }
517
518 #[must_use]
525 pub fn effective_line(&self, source_id: u64, line: u64) -> Option<u64> {
526 if line == 0 || line > self.line_count(source_id)? {
527 return None;
528 }
529 let mut narrowest = None::<(u64, u64)>;
530 for instruction in &self.info.instructions {
531 let Some(location) = instruction.source_location() else {
532 continue;
533 };
534 if location.source_id != source_id {
535 continue;
536 }
537 let start = self.line_of(source_id, location.offset)?;
538 let end = self.line_of(
539 source_id,
540 location
541 .offset
542 .saturating_add(location.length.saturating_sub(1)),
543 )?;
544 if start > line || line > end {
545 continue;
546 }
547 if start == line {
548 return Some(line);
549 }
550 if narrowest.is_none_or(|(length, _)| location.length < length) {
551 narrowest = Some((location.length, start));
552 }
553 }
554 narrowest.map(|(_, start)| start)
555 }
556}
557
558#[must_use]
561pub fn normalize_address(value: &str) -> String {
562 let hex = value.trim_start_matches("0x").trim_start_matches("0X");
563 format!("0x{}", hex.to_ascii_lowercase())
564}
565
566#[must_use]
570pub fn source_path_matches(source_path: &str, requested: &str) -> bool {
571 fn ends_with_path(longer: &str, shorter: &str) -> bool {
572 longer.ends_with(shorter)
573 && longer
574 .as_bytes()
575 .get(longer.len() - shorter.len() - 1)
576 .is_none_or(|byte| *byte == b'/')
577 }
578 fn file_name(path: &str) -> &str {
579 path.rsplit('/').next().unwrap_or(path)
580 }
581 source_path == requested
582 || ends_with_path(source_path, requested)
583 || ends_with_path(requested, source_path)
584 || (!requested.is_empty() && file_name(source_path) == file_name(requested))
585}
586
587fn narrowest_function(
590 functions: &[SourceFunction],
591 candidates: &[usize],
592 offset: u64,
593) -> Option<usize> {
594 candidates
595 .iter()
596 .copied()
597 .filter(|index| {
598 let function = &functions[*index];
599 function.declaration_start <= offset && offset <= function.body_end
600 })
601 .min_by_key(|index| {
602 let function = &functions[*index];
603 function.body_end.saturating_sub(function.declaration_start)
604 })
605}
606
607fn line_starts(source: &str) -> Vec<usize> {
608 let mut starts = vec![0];
609 starts.extend(
610 source
611 .bytes()
612 .enumerate()
613 .filter(|(_, byte)| *byte == b'\n')
614 .map(|(index, _)| index + 1),
615 );
616 starts
617}
618
619fn jump_marker(instruction: &Instruction) -> JumpMarker {
621 if !instruction.function_invocations().is_empty() {
622 JumpMarker::Call
623 } else if instruction.function_exit() == Some(FunctionExit::Return) {
624 JumpMarker::Return
625 } else {
626 JumpMarker::None
627 }
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
632pub struct LineKey {
633 pub contract: usize,
634 pub source_id: u64,
635 pub line: u64,
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
640pub struct FunctionId {
641 pub contract: usize,
642 pub function: usize,
643}
644
645#[derive(Debug, Clone, PartialEq, Eq)]
647pub struct StepLocation {
648 pub key: LineKey,
649 pub generated: bool,
652 pub contract_name: String,
653 pub path: String,
654 pub offset: u64,
655 pub length: u64,
656 pub line: u64,
657 pub column: u64,
658 pub function: Option<FunctionId>,
659 pub function_name: Option<String>,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq)]
664pub struct ResolvedLine {
665 pub key: LineKey,
666 pub path: String,
667 pub requested_line: u64,
669}
670
671#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct ResolvedFunction {
674 pub id: FunctionId,
675 pub name: String,
676 pub contract_name: String,
677 pub path: String,
678 pub line: u64,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq)]
683pub struct Frame {
684 pub depth: u32,
686 pub external: bool,
689 pub address: Option<String>,
690 pub contract_name: Option<String>,
691 pub function_name: Option<String>,
692 pub entry_step: usize,
694 pub step: usize,
697 pub pc: u64,
698 pub location: Option<StepLocation>,
699 pub arguments: Vec<FrameArgument>,
703 pub kind: Option<String>,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq)]
709pub struct SourceListing {
710 pub path: String,
711 pub current_line: u64,
712 pub lines: Vec<(u64, String)>,
714}
715
716#[derive(Debug, Clone, Copy, PartialEq, Eq)]
717struct LocationRef {
718 key: LineKey,
719 offset: u64,
720 length: u64,
721 function: Option<usize>,
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
725struct StepInfo {
726 location: Option<LocationRef>,
729 generated: bool,
731 key: Option<LineKey>,
734 contract: Option<usize>,
736 address: Option<usize>,
738 storage: Option<usize>,
741 evm_depth: u32,
743 starts_root: bool,
745 frame_depth: u32,
747 frame_entry: bool,
749 line_start: bool,
752}
753
754#[derive(Debug)]
759struct NearestLines<T> {
760 lines: Vec<(u32, T)>,
762}
763
764impl<T> Default for NearestLines<T> {
765 fn default() -> Self {
766 Self { lines: Vec::new() }
767 }
768}
769
770impl<T: Copy> NearestLines<T> {
771 fn visit(&mut self, depth: u32, key: Option<T>) -> Option<T> {
774 while self.lines.last().is_some_and(|(level, _)| *level > depth) {
775 self.lines.pop();
776 }
777 let nearest = match self.lines.last() {
778 Some((level, line)) if *level == depth => Some(*line),
779 _ => None,
780 };
781 if let Some(key) = key {
782 if nearest.is_some() {
783 self.lines.pop();
784 }
785 self.lines.push((depth, key));
786 }
787 nearest
788 }
789
790 fn clear(&mut self) {
792 self.lines.clear();
793 }
794}
795
796#[derive(Debug, Clone, PartialEq, Eq)]
798pub struct StepMap {
799 contracts: Vec<ContractDebugInfo>,
800 steps: Vec<StepInfo>,
801 addresses: Vec<String>,
802 pcs: Vec<u64>,
803 reverted: Vec<(usize, usize)>,
806 argument_layouts: Vec<Evidence>,
809 locals_support: Vec<Result<(), &'static str>>,
811 variable_layouts: Vec<Vec<InferredVariable>>,
814 step_variables: Vec<u32>,
816 roots: Vec<RootCall>,
818}
819
820const NO_LAYOUT: u32 = u32::MAX;
821const INLINED_LAYOUT: u32 = u32::MAX - 1;
823
824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub enum VariableKind {
827 Parameter,
828 Return,
829 Local,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq)]
834pub struct InferredVariable {
835 pub name: String,
836 pub ty: String,
838 pub kind: VariableKind,
839 pub slot: Option<usize>,
842 pub words: usize,
845}
846
847#[derive(Debug, Clone, Copy, PartialEq, Eq)]
849pub enum LocalsStatus<'a> {
850 Inferred(&'a [InferredVariable]),
852 Unavailable(&'static str),
854}
855
856#[derive(Debug, Clone, Copy, PartialEq, Eq)]
860enum Evidence {
861 Unknown,
863 Contradicted,
865 TopWords,
867 Ordered(ArgumentOrder),
869}
870
871impl Evidence {
872 fn merge(self, other: Self) -> Self {
875 match (self, other) {
876 (Self::Contradicted, _) | (_, Self::Contradicted) => Self::Contradicted,
877 (Self::Ordered(order), _) | (_, Self::Ordered(order)) => Self::Ordered(order),
878 (Self::TopWords, _) | (_, Self::TopWords) => Self::TopWords,
879 _ => Self::Unknown,
880 }
881 }
882
883 fn layout(self) -> Option<ArgumentLayout> {
884 match self {
885 Self::Ordered(order) => Some(ArgumentLayout::Ordered(order)),
886 Self::TopWords => Some(ArgumentLayout::TopWords),
887 Self::Unknown | Self::Contradicted => None,
888 }
889 }
890}
891
892#[derive(Debug, Clone, Copy)]
894struct FramedStep {
895 evm_depth: u32,
896 frame_id: u32,
897 root: usize,
899 contract: Option<usize>,
900 address: Option<usize>,
901 storage: Option<usize>,
902 location: Option<LocationRef>,
903 entry: Option<usize>,
905 marker: JumpMarker,
907}
908
909struct EvmFrame {
910 id: u32,
911 contract: Option<usize>,
912 address: Option<usize>,
913 storage: Option<usize>,
915 entry_step: usize,
917}
918
919#[derive(Clone)]
921struct FrameEntered {
922 address: Option<String>,
925 code_hash: Option<String>,
927 environment: CodeEnvironment,
928 delegated: bool,
930}
931
932impl FrameEntered {
933 fn recorded(trace: &TransactionTrace) -> FxHashMap<usize, Self> {
939 let entered = |entry: Option<usize>, exit: Option<usize>| {
940 entry.filter(|entry| exit.is_none_or(|exit| exit > *entry))
941 };
942 let artifacts = &trace.artifacts;
943 let mut recorded = FxHashMap::default();
944 for call in &artifacts.calls {
945 if let Some(step) = entered(call.entry_step, call.exit_step) {
946 recorded.entry(step).or_insert_with(|| Self {
947 address: Some(call.bytecode_address.clone()),
948 code_hash: call.code_hash.clone(),
949 environment: CodeEnvironment::Call,
950 delegated: matches!(call.call_type.as_str(), "DELEGATECALL" | "CALLCODE"),
951 });
952 }
953 }
954 for creation in &artifacts.creations {
955 if let Some(step) = entered(creation.entry_step, creation.exit_step) {
956 recorded.entry(step).or_insert_with(|| Self {
957 address: creation.address.clone(),
958 code_hash: creation.code_hash.clone(),
959 environment: CodeEnvironment::Create,
960 delegated: false,
961 });
962 }
963 }
964 recorded
965 }
966
967 fn from_call(trace: &TransactionTrace, caller: Option<usize>) -> Self {
969 let op = caller.map(|caller| trace.steps.step(caller).op());
970 Self {
971 address: caller.and_then(|caller| call_target(trace.steps.step(caller))),
972 code_hash: None,
973 environment: if matches!(op, Some(opcode::CREATE | opcode::CREATE2)) {
974 CodeEnvironment::Create
975 } else {
976 CodeEnvironment::Call
977 },
978 delegated: matches!(op, Some(opcode::DELEGATECALL | opcode::CALLCODE)),
979 }
980 }
981}
982
983#[derive(Debug, Clone, PartialEq, Eq)]
987struct FrameEntry {
988 function: Option<usize>,
989 return_pc: Option<u64>,
994 variables: Option<FrameVariables>,
997}
998
999#[derive(Debug, Clone, PartialEq, Eq)]
1015struct FrameVariables {
1016 params_base: Option<usize>,
1018 returns: Vec<Option<usize>>,
1020 locals_base: Option<usize>,
1022 body_seen: bool,
1024 layout: Option<u32>,
1027 modifier_slots: Option<usize>,
1031 inlined: bool,
1036 layout_inputs: LayoutInputs,
1039 param_slots: usize,
1042 last_span: Option<ByteRange>,
1044 last_height: usize,
1045}
1046
1047#[derive(Debug, Clone, Default, PartialEq, Eq)]
1050struct LayoutInputs {
1051 params_base: Option<usize>,
1052 returns: Vec<Option<usize>>,
1053 locals_base: Option<usize>,
1054 in_scope: Vec<bool>,
1056}
1057
1058impl FrameVariables {
1059 fn new(
1060 function: &SourceFunction,
1061 params_base: Option<usize>,
1062 modifier_slots: Option<usize>,
1063 ) -> Self {
1064 Self {
1065 params_base,
1066 returns: vec![None; function.returns.len()],
1067 locals_base: None,
1068 body_seen: false,
1069 layout: None,
1070 modifier_slots,
1071 inlined: false,
1072 layout_inputs: LayoutInputs::default(),
1073 param_slots: parameter_slots(&function.params),
1074 last_span: None,
1075 last_height: 0,
1076 }
1077 }
1078
1079 fn place_from_parameters(&mut self, function: &SourceFunction) {
1082 let Some(base) = self.params_base else {
1083 return;
1084 };
1085 let returns_base = base + self.param_slots;
1086 for (index, slot) in self.returns.iter_mut().enumerate() {
1087 slot.get_or_insert(returns_base + index);
1088 }
1089 if self.locals_base.is_none() {
1090 if let Some(modifier_slots) = self.modifier_slots {
1091 self.locals_base = Some(returns_base + function.returns.len() + modifier_slots);
1092 }
1093 }
1094 }
1095
1096 fn track(
1099 &mut self,
1100 function: &SourceFunction,
1101 span: ByteRange,
1102 height: usize,
1103 layouts: &mut Vec<Vec<InferredVariable>>,
1104 ) -> u32 {
1105 if !self.inlined
1110 && self.last_span == Some(span)
1111 && (self.locals_base.is_some() || self.last_height == height)
1112 {
1113 if let Some(id) = self.layout {
1114 let inputs = &self.layout_inputs;
1115 if inputs.params_base == self.params_base
1116 && inputs.locals_base == self.locals_base
1117 && inputs.returns == self.returns
1118 {
1119 self.last_height = height;
1120 return id;
1121 }
1122 }
1123 }
1124 let id = self.track_span(function, span, height, layouts);
1125 self.last_span = Some(span);
1126 self.last_height = height;
1127 id
1128 }
1129
1130 fn track_span(
1132 &mut self,
1133 function: &SourceFunction,
1134 span: ByteRange,
1135 height: usize,
1136 layouts: &mut Vec<Vec<InferredVariable>>,
1137 ) -> u32 {
1138 if self.inlined {
1139 self.layout = Some(INLINED_LAYOUT);
1140 return INLINED_LAYOUT;
1141 }
1142 let length = span.end - span.start;
1143 let param_slots = self.param_slots;
1144 self.place_from_parameters(function);
1146 let at_beginning = function
1151 .placeholder
1152 .is_none_or(|placeholder| span.start < placeholder);
1153 if !self.body_seen
1154 && at_beginning
1155 && span.start >= function.body_start
1156 && span.start < function.body_end
1157 {
1158 self.body_seen = true;
1159 if self.locals_base.is_none() {
1160 self.locals_base = Some(height);
1161 }
1162 if self.params_base.is_none() && !function.has_modifiers {
1163 self.params_base = height.checked_sub(param_slots + function.returns.len());
1164 }
1165 }
1166 for (index, parameter) in function.returns.iter().enumerate() {
1169 if self.returns[index].is_none() && parameter.declaration.covers(span.start, length) {
1170 self.returns[index] = Some(height);
1171 if index == 0 && self.params_base.is_none() {
1172 self.params_base = height.checked_sub(param_slots);
1173 }
1174 }
1175 }
1176 if self.locals_base.is_none() {
1179 if let Some((index, _)) = function
1180 .locals
1181 .iter()
1182 .enumerate()
1183 .find(|(_, local)| local.statement.covers(span.start, length))
1184 {
1185 self.locals_base = height.checked_sub(live_index(function, index, span.start));
1186 }
1187 }
1188 self.place_from_parameters(function);
1190
1191 let inputs = &self.layout_inputs;
1194 if let Some(id) = self.layout {
1195 if inputs.params_base == self.params_base
1196 && inputs.locals_base == self.locals_base
1197 && inputs.returns == self.returns
1198 && inputs.in_scope.len() == function.locals.len()
1199 && function
1200 .locals
1201 .iter()
1202 .zip(&inputs.in_scope)
1203 .all(|(local, in_scope)| local.scope.contains(span.start) == *in_scope)
1204 {
1205 debug_assert_eq!(
1206 layouts[id as usize],
1207 self.layout_at(function, span.start),
1208 "a cache hit must give the layout its inputs compute"
1209 );
1210 return id;
1211 }
1212 }
1213
1214 let layout = self.layout_at(function, span.start);
1215 let id = match self.layout {
1216 Some(id) if layouts[id as usize] == layout => id,
1217 _ => {
1218 layouts.push(layout);
1219 (layouts.len() - 1) as u32
1220 }
1221 };
1222 self.layout = Some(id);
1223 let inputs = &mut self.layout_inputs;
1224 inputs.params_base = self.params_base;
1225 inputs.returns.clone_from(&self.returns);
1226 inputs.locals_base = self.locals_base;
1227 inputs.in_scope.clear();
1228 inputs.in_scope.extend(
1229 function
1230 .locals
1231 .iter()
1232 .map(|local| local.scope.contains(span.start)),
1233 );
1234 id
1235 }
1236
1237 fn layout_at(&self, function: &SourceFunction, offset: u64) -> Vec<InferredVariable> {
1240 let mut layout = Vec::new();
1241 if let Some(base) = self.params_base {
1242 let mut slot = base;
1243 for parameter in &function.params {
1244 let size = parameter_stack_slots(parameter);
1245 layout.push(InferredVariable {
1246 name: parameter.name.clone(),
1247 ty: match ¶meter.location {
1248 Some(location) => format!("{} {location}", parameter.ty),
1249 None => parameter.ty.clone(),
1250 },
1251 kind: VariableKind::Parameter,
1252 slot: Some(slot),
1253 words: size,
1254 });
1255 slot += size;
1256 }
1257 }
1258 for (index, parameter) in function.returns.iter().enumerate() {
1259 layout.push(InferredVariable {
1260 name: parameter.name.clone(),
1261 ty: parameter.declared_type(),
1262 kind: VariableKind::Return,
1263 slot: self.returns[index],
1264 words: 1,
1265 });
1266 }
1267 for (index, local) in function.locals.iter().enumerate() {
1268 if local.scope.contains(offset) {
1269 layout.push(InferredVariable {
1270 name: local.name.clone(),
1271 ty: local.declared_type(),
1272 kind: VariableKind::Local,
1273 slot: self
1274 .locals_base
1275 .map(|base| base + live_index(function, index, offset)),
1276 words: 1,
1277 });
1278 }
1279 }
1280 layout
1281 }
1282}
1283
1284fn live_index(function: &SourceFunction, index: usize, offset: u64) -> usize {
1287 let declaration = function.locals[index].declaration.start;
1288 function
1289 .locals
1290 .iter()
1291 .filter(|other| other.scope.contains(offset) && other.declaration.start < declaration)
1292 .count()
1293}
1294
1295pub(crate) fn calldata_for_step<'a>(
1299 trace: &'a TransactionTrace,
1300 roots: &[RootCall],
1301 step: usize,
1302) -> &'a str {
1303 let root_input = roots
1304 .get(root_index(roots, step))
1305 .map_or("", |root| root.input(trace));
1306 frame_calldata(trace, step, root_input)
1307}
1308
1309fn frame_calldata<'a>(trace: &'a TransactionTrace, step: usize, root_input: &'a str) -> &'a str {
1311 let root_depth = trace.steps.first().map_or(0, |first| first.depth());
1312 let depth = trace
1313 .steps
1314 .get(step)
1315 .map_or(root_depth, |current| current.depth());
1316 if depth == root_depth {
1317 return root_input;
1318 }
1319 trace
1320 .artifacts
1321 .calls
1322 .iter()
1323 .filter(|call| {
1324 call.entry_step.is_some_and(|entry| entry <= step)
1325 && call.exit_step.is_none_or(|exit| step < exit)
1326 })
1327 .max_by_key(|call| call.depth)
1328 .map_or("", |call| call.input.as_str())
1329}
1330
1331fn parameter_stack_slots(parameter: &SourceParam) -> usize {
1335 let dynamic =
1336 parameter.ty == "bytes" || parameter.ty == "string" || parameter.ty.ends_with("[]");
1337 if parameter.location.as_deref() == Some("calldata") && dynamic
1338 || parameter.ty.starts_with("function")
1339 {
1340 2
1341 } else {
1342 1
1343 }
1344}
1345
1346fn parameter_slots(parameters: &[SourceParam]) -> usize {
1347 parameters.iter().map(parameter_stack_slots).sum()
1348}
1349
1350fn modifier_slots(
1355 contract: &ContractDebugInfo,
1356 function: &SourceFunction,
1357 count: usize,
1358) -> Option<usize> {
1359 let mut slots = 0;
1360 for name in function.modifiers.iter().take(count) {
1361 let modifier = contract.functions.iter().find(|candidate| {
1362 candidate.placeholder.is_some()
1363 && candidate.name == *name
1364 && candidate.source_id == function.source_id
1365 });
1366 let modifier = modifier.or_else(|| {
1367 contract
1368 .functions
1369 .iter()
1370 .find(|candidate| candidate.placeholder.is_some() && candidate.name == *name)
1371 })?;
1372 let placeholder = modifier.placeholder?;
1373 let live = modifier
1374 .locals
1375 .iter()
1376 .filter(|local| {
1377 local.declaration.start < placeholder && local.scope.contains(placeholder)
1378 })
1379 .count();
1380 slots += parameter_slots(&modifier.params) + live;
1381 }
1382 Some(slots)
1383}
1384
1385fn locals_support(contract: &ContractDebugInfo, evidence: Evidence) -> Result<(), &'static str> {
1387 match (contract.code_generator, evidence.layout()) {
1388 (_, Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))) => Err(
1389 "this trace shows the via-IR calling convention, whose stack layout cannot be \
1390 recovered without compiler-reported variable locations",
1391 ),
1392 (Some(CodeGenerator::ViaIr), _) => Err(
1393 "this contract was compiled through the via-IR pipeline, whose stack layout cannot \
1394 be recovered without compiler-reported variable locations",
1395 ),
1396 (Some(CodeGenerator::Legacy), _) | (None, Some(ArgumentLayout::Ordered(_))) => Ok(()),
1397 (None, _) => Err(
1398 "the code generator is not known; only solc's legacy pipeline keeps variables at \
1399 fixed stack slots",
1400 ),
1401 }
1402}
1403
1404#[derive(Default)]
1406struct InternalFrame {
1407 functions: Vec<FrameEntry>,
1409 named: usize,
1411 named_below: usize,
1414 statement: Option<LocationRef>,
1416 remembered: FxHashMap<usize, FrameVariables>,
1419}
1420
1421impl InternalFrame {
1422 fn active(&self) -> Option<usize> {
1424 self.functions.iter().rev().find_map(|entry| entry.function)
1425 }
1426
1427 fn is_active(&self, function: usize) -> bool {
1428 self.active() == Some(function)
1429 }
1430
1431 fn push_function(
1432 &mut self,
1433 function: usize,
1434 return_pc: Option<u64>,
1435 variables: Option<FrameVariables>,
1436 ) {
1437 self.functions.push(FrameEntry {
1438 function: Some(function),
1439 return_pc,
1440 variables,
1441 });
1442 self.named += 1;
1443 }
1444
1445 fn pop(&mut self) {
1447 if self
1448 .functions
1449 .pop()
1450 .is_some_and(|entry| entry.function.is_some())
1451 {
1452 self.named -= 1;
1453 }
1454 }
1455
1456 fn clear(&mut self) {
1458 self.functions.clear();
1459 self.named = 0;
1460 }
1461
1462 fn depth(&self) -> usize {
1464 self.named_below + self.named
1465 }
1466
1467 fn push_placeholder(&mut self) {
1468 self.functions.push(FrameEntry {
1469 function: None,
1470 return_pc: None,
1471 variables: None,
1472 });
1473 }
1474
1475 fn returns_to(&self, pc: u64) -> bool {
1477 self.functions
1478 .last()
1479 .is_some_and(|entry| entry.return_pc == Some(pc))
1480 }
1481}
1482
1483#[allow(clippy::too_many_arguments)]
1492fn place_at_entry_landing(
1493 frame: &mut InternalFrame,
1494 contract: &ContractDebugInfo,
1495 function: usize,
1496 trace: &TransactionTrace,
1497 root_input: &str,
1498 index: usize,
1499 own: LocationRef,
1500 height: usize,
1501 from_call: bool,
1502) {
1503 let Some(declared) = contract.functions.get(function) else {
1504 return;
1505 };
1506 let at_declaration = contract.is_jumpdest(trace.steps.step(index).pc())
1507 && own.offset == declared.declaration_start;
1508 let from_dispatcher =
1509 !from_call || (index > 0 && framed_declaration(trace, contract, index - 1, declared));
1510 if !at_declaration || !from_dispatcher {
1511 return;
1512 }
1513 let entry = frame
1514 .functions
1515 .iter_mut()
1516 .rev()
1517 .find(|entry| entry.function == Some(function));
1518 let Some(variables) = entry.and_then(|entry| entry.variables.as_mut()) else {
1519 return;
1520 };
1521 if variables.params_base.is_some() || variables.body_seen {
1522 return;
1523 }
1524 let stack = trace.steps.step(index).stack().to_vec();
1525 let calldata = frame_calldata(trace, index, root_input);
1526 if argument_evidence(declared, calldata, &stack) == Evidence::Contradicted {
1527 return;
1528 }
1529 variables.params_base = height.checked_sub(parameter_slots(&declared.params));
1530}
1531
1532fn framed_declaration(
1535 trace: &TransactionTrace,
1536 contract: &ContractDebugInfo,
1537 index: usize,
1538 function: &SourceFunction,
1539) -> bool {
1540 let step = trace.steps.step(index);
1541 if step.op() != opcode::JUMP {
1542 return false;
1543 }
1544 contract.location_at_pc(step.pc()).is_some_and(|location| {
1545 location.source_id == function.source_id && location.offset == function.declaration_start
1546 })
1547}
1548
1549fn modifier_base(
1553 frame: &InternalFrame,
1554 contract: &ContractDebugInfo,
1555 function: usize,
1556) -> Option<usize> {
1557 let modifier = contract.functions.get(function)?;
1558 modifier.placeholder?;
1559 let parent = frame
1560 .functions
1561 .iter()
1562 .rev()
1563 .find(|entry| entry.function.is_some())?;
1564 let parent_function = contract.functions.get(parent.function?)?;
1565 let position = parent_function
1566 .modifiers
1567 .iter()
1568 .position(|name| *name == modifier.name)?;
1569 let parent_base = parent.variables.as_ref()?.params_base?;
1570 let before = modifier_slots(contract, parent_function, position)?;
1571 Some(
1572 parent_base
1573 + parameter_slots(&parent_function.params)
1574 + parent_function.returns.len()
1575 + before,
1576 )
1577}
1578
1579impl StepMap {
1580 #[must_use]
1589 pub fn new(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1590 Self::build(trace, contracts, true)
1591 }
1592
1593 #[must_use]
1598 pub fn for_debug_diff(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1599 Self::build(trace, contracts, false)
1600 }
1601
1602 fn build(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>, smooth: bool) -> Self {
1603 let mut addresses = Vec::<String>::new();
1604 let mut address_index = FxHashMap::<String, usize>::default();
1605 let mut intern = |address: &str| -> usize {
1606 let address = normalize_address(address);
1607 *address_index.entry(address.clone()).or_insert_with(|| {
1608 addresses.push(address);
1609 addresses.len() - 1
1610 })
1611 };
1612 let roots = root_calls(trace);
1618 let single_root = roots.len() == 1;
1619 let contract_for = |address: Option<&str>,
1620 code_hash: Option<&str>,
1621 environment: CodeEnvironment,
1622 root: bool|
1623 -> Option<usize> {
1624 let address = address.map(normalize_address);
1625 let code_hash = code_hash.map(normalize_address);
1626 if let Some(address) = &address {
1627 let mut at_address = contracts.iter().enumerate().filter(|(_, contract)| {
1629 contract.address.as_deref() == Some(address)
1630 && (code_hash.is_none()
1631 || contract.code_hash.is_none()
1632 || contract.code_hash == code_hash)
1633 });
1634 let exact = at_address.clone().find(|(_, contract)| {
1635 contract
1636 .code_environment()
1637 .is_none_or(|candidate| candidate == environment)
1638 });
1639 if let Some((index, _)) = exact.or_else(|| at_address.next()) {
1640 return Some(index);
1641 }
1642 }
1643 (root && single_root && contracts.len() == 1).then_some(0)
1644 };
1645
1646 let root_depth = trace.steps.first().map_or(0, |step| step.depth());
1648 let mut next_frame_id = 0_u32;
1649 let mut evm_frames = Vec::<EvmFrame>::new();
1650 let mut upcoming_roots = roots.iter().enumerate().peekable();
1651 let mut current_root = 0;
1652 let mut reverted = Vec::new();
1653 let mut framed = Vec::with_capacity(trace.steps.len());
1654 let recorded = FrameEntered::recorded(trace);
1655 for (index, step) in trace.steps.iter().enumerate() {
1656 let evm_depth = step.depth().saturating_sub(root_depth) as usize;
1657 if let Some((position, root)) =
1658 upcoming_roots.next_if(|(_, root)| root.entry_step == index)
1659 {
1660 current_root = position;
1661 let environment = if root.creation(trace) {
1663 CodeEnvironment::Create
1664 } else {
1665 CodeEnvironment::Call
1666 };
1667 let root_address = root.address(trace);
1668 let root_code_hash = root.code_hash(trace);
1669 let address = root_address.map(&mut intern);
1670 evm_frames.clear();
1671 evm_frames.push(EvmFrame {
1672 id: next_frame_id,
1673 contract: contract_for(root_address, root_code_hash, environment, true),
1674 address,
1675 storage: address,
1676 entry_step: index,
1677 });
1678 next_frame_id += 1;
1679 }
1680 evm_frames.truncate(evm_depth + 1);
1681 while evm_frames.len() <= evm_depth {
1682 let call = (evm_frames.len() == evm_depth)
1687 .then(|| index.checked_sub(1))
1688 .flatten();
1689 let entered = recorded
1690 .get(&index)
1691 .filter(|_| evm_frames.len() == evm_depth)
1692 .cloned()
1693 .unwrap_or_else(|| FrameEntered::from_call(trace, call));
1694 let address = entered.address.as_deref().map(&mut intern);
1695 let storage = if entered.delegated {
1698 evm_frames.last().and_then(|frame| frame.storage)
1699 } else {
1700 address
1701 };
1702 evm_frames.push(EvmFrame {
1703 id: next_frame_id,
1704 contract: contract_for(
1705 entered.address.as_deref(),
1706 entered.code_hash.as_deref(),
1707 entered.environment,
1708 false,
1709 ),
1710 address,
1711 storage,
1712 entry_step: index,
1713 });
1714 next_frame_id += 1;
1715 }
1716 if step.op() == opcode::REVERT || step.error().is_some() {
1717 let entry_step = evm_frames
1718 .last()
1719 .expect("the root frame is never popped")
1720 .entry_step;
1721 reverted.push((entry_step, index));
1722 }
1723 let frame = evm_frames.last().expect("the root frame is never popped");
1724 let facts = frame
1725 .contract
1726 .and_then(|index| contracts[index].facts_at_pc(step.pc()));
1727 let location = frame.contract.zip(facts).and_then(|(contract, facts)| {
1728 let location = facts.location.as_ref()?;
1729 Some(LocationRef {
1730 key: LineKey {
1731 contract,
1732 source_id: location.source_id,
1733 line: facts.line?,
1734 },
1735 offset: location.offset,
1736 length: location.length,
1737 function: facts.function,
1738 })
1739 });
1740 framed.push(FramedStep {
1741 evm_depth: evm_depth as u32,
1742 frame_id: frame.id,
1743 root: current_root,
1744 contract: frame.contract,
1745 address: frame.address,
1746 storage: frame.storage,
1747 location,
1748 entry: facts.and_then(|facts| facts.entry),
1749 marker: facts.map_or(JumpMarker::None, |facts| facts.marker),
1750 });
1751 }
1752
1753 let structured = |contract: Option<usize>| {
1757 contract.is_some_and(|index| !contracts[index].functions.is_empty())
1758 };
1759 let mut function_follows = vec![false; next_frame_id as usize];
1761 let mut epilogue = vec![false; framed.len()];
1762 for (index, step) in framed.iter().enumerate().rev() {
1763 match step.location.map(|location| location.function) {
1764 Some(Some(_)) => {
1765 function_follows[step.frame_id as usize] = true;
1766 }
1767 Some(None) if structured(step.contract) => {
1768 epilogue[index] = !function_follows[step.frame_id as usize];
1769 }
1770 _ => {}
1771 }
1772 }
1773
1774 let argument_layouts = prove_argument_layouts(trace, &contracts, &framed);
1775 let locals_support = contracts
1776 .iter()
1777 .zip(&argument_layouts)
1778 .map(|(contract, evidence)| locals_support(contract, *evidence))
1779 .collect::<Vec<_>>();
1780
1781 let mut internal = Vec::<InternalFrame>::new();
1784 let mut steps = Vec::with_capacity(framed.len());
1785 let mut pcs = Vec::with_capacity(framed.len());
1786 let mut variable_layouts = Vec::<Vec<InferredVariable>>::new();
1787 let mut step_variables = Vec::with_capacity(framed.len());
1788 for (index, step) in framed.iter().enumerate() {
1789 let height = trace.steps.step(index).stack().len();
1790 let frame_variables = |function: usize| -> Option<FrameVariables> {
1795 let contract_index = step.contract?;
1796 locals_support[contract_index].ok()?;
1797 let contract = &contracts[contract_index];
1798 let function = contract.functions.get(function)?;
1799 let landed_from_function = index > 0
1800 && framed[index - 1].frame_id == step.frame_id
1801 && trace.steps.step(index - 1).op() == opcode::JUMP
1802 && framed[index - 1]
1803 .location
1804 .is_some_and(|location| location.function.is_some());
1805 let params_base = landed_from_function
1806 .then(|| height.checked_sub(parameter_slots(&function.params)))
1807 .flatten();
1808 Some(FrameVariables::new(
1809 function,
1810 params_base,
1811 modifier_slots(contract, function, function.modifiers.len()),
1812 ))
1813 };
1814 let contract_info = step.contract.map(|index| &contracts[index]);
1815 let evm_depth = step.evm_depth as usize;
1816 let mut frame_entry = false;
1817 let new_root = index > 0 && framed[index - 1].root != step.root;
1819 let root_input = roots[step.root].input(trace);
1820 internal.truncate(if new_root { evm_depth } else { evm_depth + 1 });
1821 while internal.len() <= evm_depth {
1822 let named_below = internal.last().map_or(0, InternalFrame::depth);
1823 internal.push(InternalFrame {
1824 named_below,
1825 ..InternalFrame::default()
1826 });
1827 frame_entry = true;
1828 }
1829 let frame = internal.last_mut().expect("the root frame is never popped");
1830
1831 let landed_by_jump = index > 0
1834 && framed[index - 1].frame_id == step.frame_id
1835 && trace.steps.step(index - 1).op() == opcode::JUMP;
1836 let from_function_code = index > 0
1839 && framed[index - 1].frame_id == step.frame_id
1840 && framed[index - 1].location.is_some_and(|location| {
1841 location.function.is_some()
1842 && location.function != step.location.and_then(|own| own.function)
1843 });
1844 let previous_marker = if landed_by_jump {
1845 framed[index - 1].marker
1846 } else {
1847 JumpMarker::None
1848 };
1849 let structured = structured(step.contract);
1850 let mut generated = false;
1851 let mut pending_call = false;
1855 if landed_by_jump {
1856 let pc = trace.steps.step(index).pc();
1857 match previous_marker {
1858 JumpMarker::Call => pending_call = true,
1859 JumpMarker::Return => frame.pop(),
1860 JumpMarker::None if frame.returns_to(pc) => frame.pop(),
1861 JumpMarker::None => {}
1862 }
1863 }
1864 let return_address = |function: usize| -> Option<u64> {
1867 if !landed_by_jump {
1868 return None;
1869 }
1870 let contract = &contracts[step.contract?];
1871 let arguments = contract.functions.get(function)?.params.len();
1872 let word = trace.steps.step(index - 1).stack().peek(1 + arguments)?;
1873 let pc = word.to_u64()?;
1874 contract.is_jumpdest(pc).then_some(pc)
1875 };
1876 let (location, key) = match step.location {
1877 None => {
1878 if pending_call {
1881 frame.push_placeholder();
1882 }
1883 (None, None)
1884 }
1885 Some(own) if !structured => {
1886 frame.statement = Some(own);
1888 (Some(own), Some(own.key))
1889 }
1890 Some(own) => match own.function {
1891 Some(function) => {
1892 let entering = landed_by_jump && step.entry == Some(function);
1893 if landed_by_jump
1897 && previous_marker == JumpMarker::Return
1898 && frame.is_active(function)
1899 {
1900 if let Some(contract) = contract_info {
1901 place_at_entry_landing(
1902 frame, contract, function, trace, root_input, index, own,
1903 height, false,
1904 );
1905 }
1906 }
1907 if entering || (pending_call && !frame.is_active(function)) {
1908 frame.push_function(
1911 function,
1912 return_address(function),
1913 frame_variables(function),
1914 );
1915 frame_entry = true;
1916 } else if pending_call {
1917 if let Some(contract) = contract_info {
1923 place_at_entry_landing(
1924 frame, contract, function, trace, root_input, index, own,
1925 height, true,
1926 );
1927 }
1928 frame.push_placeholder();
1929 } else if !frame.is_active(function) {
1930 match frame
1931 .functions
1932 .iter()
1933 .rposition(|entry| entry.function == Some(function))
1934 {
1935 Some(position) => {
1936 frame.named -= frame.functions[position + 1..]
1939 .iter()
1940 .filter(|entry| entry.function.is_some())
1941 .count();
1942 for left in frame.functions.drain(position + 1..) {
1943 if let (Some(function), Some(variables)) =
1944 (left.function, left.variables)
1945 {
1946 frame.remembered.insert(function, variables);
1947 }
1948 }
1949 }
1950 None => {
1951 let mut variables = frame.remembered.remove(&function);
1952 if variables.is_none() {
1953 variables = frame_variables(function);
1954 if let (Some(variables), Some(contract)) =
1955 (variables.as_mut(), contract_info)
1956 {
1957 let declared = &contract.functions[function];
1958 if declared.placeholder.is_some() {
1959 if variables.params_base.is_none() {
1965 variables.params_base =
1966 modifier_base(frame, contract, function);
1967 }
1968 } else if declared.name != "constructor"
1969 && from_function_code
1970 && step.entry != Some(function)
1971 {
1972 variables.inlined = true;
1976 }
1977 }
1978 }
1979 frame.push_function(function, None, variables);
1980 frame_entry = true;
1981 }
1982 }
1983 }
1984 frame.statement = Some(own);
1985 (Some(own), Some(own.key))
1986 }
1987 None if epilogue[index] => {
1988 frame.clear();
1989 frame.statement = None;
1990 (Some(own), None)
1991 }
1992 None => {
1993 if pending_call {
1994 frame.push_placeholder();
1995 }
1996 match frame.statement {
1997 Some(statement) => {
1998 generated = true;
1999 (Some(statement), Some(statement.key))
2000 }
2001 None => (Some(own), None),
2004 }
2005 }
2006 },
2007 };
2008 let layout = {
2011 let frame = internal.last_mut().expect("the root frame is never popped");
2012 let contract = step.contract.map(|index| &contracts[index]);
2013 let active = frame
2014 .functions
2015 .iter()
2016 .rposition(|entry| entry.function.is_some());
2017 match (active, step.location, contract) {
2018 (Some(position), Some(own), Some(contract))
2019 if own.function.is_some()
2020 && own.function == frame.functions[position].function =>
2021 {
2022 let function_index = own.function.unwrap_or_default();
2023 let function = &contract.functions[function_index];
2024 let span = ByteRange {
2025 start: own.offset,
2026 end: own.offset + own.length,
2027 };
2028 let (below, rest) = frame.functions.split_at_mut(position);
2029 let entry = &mut rest[0];
2030 let layout = entry.variables.as_mut().map(|variables| {
2031 variables.track(function, span, height, &mut variable_layouts)
2032 });
2033 let modifier_base = entry
2038 .variables
2039 .as_ref()
2040 .and_then(|variables| variables.params_base);
2041 let parent = below
2042 .iter_mut()
2043 .rev()
2044 .find(|entry| entry.function.is_some());
2045 if let (Some(modifier_base), Some(parent)) = (modifier_base, parent) {
2046 let parent_function =
2047 &contract.functions[parent.function.unwrap_or_default()];
2048 let position = parent_function
2049 .modifiers
2050 .iter()
2051 .position(|name| *name == function.name);
2052 if let (Some(position), Some(variables)) =
2053 (position, parent.variables.as_mut())
2054 {
2055 if variables.params_base.is_none() {
2056 variables.params_base =
2057 modifier_slots(contract, parent_function, position)
2058 .and_then(|before| {
2059 modifier_base.checked_sub(
2060 parameter_slots(&parent_function.params)
2061 + parent_function.returns.len()
2062 + before,
2063 )
2064 });
2065 }
2066 }
2067 }
2068 layout
2069 }
2070 (Some(position), _, _) => frame.functions[position]
2071 .variables
2072 .as_ref()
2073 .and_then(|variables| variables.layout),
2074 _ => None,
2075 }
2076 };
2077 step_variables.push(layout.unwrap_or(NO_LAYOUT));
2078 let internal_total = internal.last().map_or(0, InternalFrame::depth);
2079 steps.push(StepInfo {
2080 location,
2081 generated,
2082 key,
2083 contract: step.contract,
2084 address: step.address,
2085 storage: step.storage,
2086 evm_depth: step.evm_depth,
2087 starts_root: new_root,
2088 frame_depth: (evm_depth + internal_total) as u32,
2089 frame_entry,
2090 line_start: false,
2091 });
2092 pcs.push(trace.steps.step(index).pc());
2093 }
2094
2095 let mut map = Self {
2096 contracts,
2097 steps,
2098 addresses,
2099 pcs,
2100 reverted,
2101 argument_layouts,
2102 locals_support,
2103 variable_layouts,
2104 step_variables,
2105 roots,
2106 };
2107 if smooth {
2108 Self::smooth_single_step_excursions(&mut map.steps);
2109 }
2110 map.mark_line_starts();
2111 map
2112 }
2113
2114 fn smooth_single_step_excursions(steps: &mut [StepInfo]) {
2122 let mut after = vec![None; steps.len()];
2125 let mut lines = NearestLines::default();
2126 for (index, step) in steps.iter().enumerate().rev() {
2127 let own = step.key.and_then(|_| u32::try_from(index).ok());
2128 after[index] = lines.visit(step.frame_depth, own);
2129 if step.starts_root {
2131 lines.clear();
2132 }
2133 }
2134 let mut lines = NearestLines::default();
2135 for (index, after) in after.into_iter().enumerate() {
2136 let step = &steps[index];
2137 if step.starts_root {
2138 lines.clear();
2139 }
2140 let before = lines.visit(step.frame_depth, step.key);
2141 let Some(key) = step.key else {
2142 continue;
2143 };
2144 let after = after.and_then(|after| steps[after as usize].key);
2145 if before.is_some() && before == after && before != Some(key) {
2146 steps[index].key = before;
2147 }
2148 }
2149 }
2150
2151 fn mark_line_starts(&mut self) {
2154 let mut runs = Vec::<(usize, Option<LineKey>)>::new();
2158 let mut depths = 0;
2159 for step in &mut self.steps {
2160 if step.starts_root {
2161 runs.clear();
2162 depths = 0;
2163 }
2164 let depth = step.frame_depth as usize;
2165 if depths <= depth {
2166 if runs.last().is_none_or(|(_, key)| key.is_some()) {
2168 runs.push((depths, None));
2169 }
2170 depths = depth + 1;
2171 }
2172 let Some(key) = step.key else {
2173 step.line_start = false;
2174 continue;
2175 };
2176 while runs.last().is_some_and(|(from, _)| *from > depth) {
2177 runs.pop();
2178 }
2179 let last = runs.last_mut().expect("a run starts at depth zero");
2180 step.line_start = last.1 != Some(key);
2181 if last.0 == depth {
2182 last.1 = Some(key);
2183 } else {
2184 runs.push((depth, Some(key)));
2185 }
2186 }
2187 }
2188
2189 #[must_use]
2190 pub fn contracts(&self) -> &[ContractDebugInfo] {
2191 &self.contracts
2192 }
2193
2194 #[must_use]
2196 pub fn contract_at_step(&self, step: usize) -> Option<&ContractDebugInfo> {
2197 self.contracts.get(self.steps.get(step)?.contract?)
2198 }
2199
2200 #[must_use]
2201 pub fn step_count(&self) -> usize {
2202 self.steps.len()
2203 }
2204
2205 #[must_use]
2207 pub fn has_source(&self) -> bool {
2208 self.steps.iter().any(|step| step.location.is_some())
2209 }
2210
2211 #[must_use]
2213 pub fn line_key(&self, step: usize) -> Option<LineKey> {
2214 self.steps.get(step)?.key
2215 }
2216
2217 #[must_use]
2218 pub fn frame_depth(&self, step: usize) -> Option<u32> {
2219 self.steps.get(step).map(|step| step.frame_depth)
2220 }
2221
2222 #[must_use]
2223 pub fn is_line_start(&self, step: usize) -> bool {
2224 self.steps.get(step).is_some_and(|step| step.line_start)
2225 }
2226
2227 #[must_use]
2228 pub fn is_frame_entry(&self, step: usize) -> bool {
2229 self.steps.get(step).is_some_and(|step| step.frame_entry)
2230 }
2231
2232 #[must_use]
2234 pub fn function_id(&self, step: usize) -> Option<FunctionId> {
2235 let location = self.steps.get(step)?.location?;
2236 location.function.map(|function| FunctionId {
2237 contract: location.key.contract,
2238 function,
2239 })
2240 }
2241
2242 #[must_use]
2244 pub fn executing_address(&self, step: usize) -> Option<&str> {
2245 let index = self.steps.get(step)?.address?;
2246 self.addresses.get(index).map(String::as_str)
2247 }
2248
2249 #[must_use]
2252 pub fn storage_address(&self, step: usize) -> Option<&str> {
2253 let index = self.storage_context_index(step)?;
2254 self.addresses.get(index).map(String::as_str)
2255 }
2256
2257 #[must_use]
2259 pub fn storage_context_index(&self, step: usize) -> Option<usize> {
2260 self.steps.get(step)?.storage
2261 }
2262
2263 #[must_use]
2266 pub fn reverted_spans(&self) -> &[(usize, usize)] {
2267 &self.reverted
2268 }
2269
2270 #[must_use]
2274 pub fn argument_layout(&self, contract: usize) -> Option<ArgumentLayout> {
2275 self.argument_layouts.get(contract).copied()?.layout()
2276 }
2277
2278 #[must_use]
2288 pub fn locals_at(&self, step: usize) -> LocalsStatus<'_> {
2289 let Some(info) = self.steps.get(step) else {
2290 return LocalsStatus::Unavailable("the step is outside the trace");
2291 };
2292 let Some(contract) = info.contract else {
2293 return LocalsStatus::Unavailable(
2294 "no sources matched the contract executing at this step",
2295 );
2296 };
2297 if let Err(reason) = self.locals_support[contract] {
2298 return LocalsStatus::Unavailable(reason);
2299 }
2300 match self.step_variables.get(step).copied() {
2301 Some(INLINED_LAYOUT) => LocalsStatus::Unavailable(
2302 "this function was inlined by the optimizer, so its variables have no stack \
2303 slots of their own",
2304 ),
2305 Some(id) if id != NO_LAYOUT => {
2306 LocalsStatus::Inferred(&self.variable_layouts[id as usize])
2307 }
2308 _ => LocalsStatus::Unavailable("no function is executing at this step"),
2309 }
2310 }
2311
2312 #[must_use]
2315 pub fn calldata_at<'a>(&self, trace: &'a TransactionTrace, step: usize) -> &'a str {
2316 calldata_for_step(trace, &self.roots, step)
2317 }
2318
2319 pub(crate) fn scope_at_step(&self, step: usize) -> Option<&str> {
2322 let info = self.steps.get(step)?;
2323 let contract = self.contracts.get(info.contract?)?;
2324 let function = contract.functions.get(info.location?.function?)?;
2325 contract
2326 .types
2327 .scope_at(function.source_id, function.declaration_start)
2328 }
2329
2330 #[must_use]
2336 pub fn local_path(
2337 &self,
2338 trace: &TransactionTrace,
2339 step: usize,
2340 words: Option<&StorageWords<'_>>,
2341 path: &str,
2342 ) -> Option<Result<DebugVariable, String>> {
2343 let found = match self.local_place(trace, step, words, path)? {
2344 Ok(found) => found,
2345 Err(reason) => return Some(Err(reason)),
2346 };
2347 let (reader, slot, place) = found;
2348 let (ty, value) = match place {
2349 Ok(place) => (place.ty(), reader.show(&place)),
2350 Err((ty, shown)) => (ty, shown),
2351 };
2352 Some(Ok(DebugVariable {
2353 name: path.trim().to_owned(),
2354 ty,
2355 location: DebugLocation {
2356 kind: "stack".to_owned(),
2357 offset: slot as u64,
2358 },
2359 value,
2360 }))
2361 }
2362
2363 #[must_use]
2366 pub fn local_condition_value(
2367 &self,
2368 trace: &TransactionTrace,
2369 step: usize,
2370 words: Option<&StorageWords<'_>>,
2371 path: &str,
2372 ) -> Option<Result<Value, String>> {
2373 let (reader, _, place) = match self.local_place(trace, step, words, path)? {
2374 Ok(found) => found,
2375 Err(reason) => return Some(Err(reason)),
2376 };
2377 Some(match place {
2378 Ok(place) => reader.condition_value(&place, path.trim()),
2379 Err((_, shown)) => Err(format!("`{}` is {}", path.trim(), shown.display)),
2380 })
2381 }
2382
2383 #[allow(clippy::type_complexity)]
2388 fn local_place<'a>(
2389 &'a self,
2390 trace: &'a TransactionTrace,
2391 step: usize,
2392 words: Option<&'a StorageWords<'a>>,
2393 path: &str,
2394 ) -> Option<Result<(ValueReader<'a>, usize, Result<Place, (String, DebugValue)>), String>> {
2395 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
2396 return None;
2397 };
2398 let segments = parse_path(path).ok()?;
2399 let Some(PathSegment::Name(name)) = segments.first() else {
2400 return None;
2401 };
2402 let variable = layout
2404 .iter()
2405 .rev()
2406 .find(|variable| variable.name == *name)?;
2407 let contract = self.contract_at_step(step)?;
2408 let view = trace.steps.get(step);
2409 let stack = view.map(|view| view.stack().to_vec()).unwrap_or_default();
2410 let reader = ValueReader {
2411 memory: view.and_then(|view| view.memory()),
2412 calldata: self.calldata_at(trace, step),
2413 storage: words,
2414 layout: contract.storage_layout.as_ref(),
2415 types: &contract.types,
2416 scope: self.scope_at_step(step),
2417 };
2418 let Some(slot) = variable.slot else {
2419 return Some(Err(format!(
2420 "`{name}` is in scope but its stack slot could not be placed here"
2421 )));
2422 };
2423 let Some(stack_words) = stack.get(slot..slot + variable.words) else {
2424 return Some(Err(format!(
2425 "`{name}` is in scope but its stack slot is above the stack here"
2426 )));
2427 };
2428 let stack_words = stack_words
2429 .iter()
2430 .map(EvmWord::to_string)
2431 .collect::<Vec<_>>();
2432 let stack_words = stack_words.iter().map(String::as_str).collect::<Vec<_>>();
2433 let mut place = match reader.root(&variable.ty, &stack_words) {
2434 Ok(place) => place,
2435 Err(shown) if segments.len() == 1 => {
2436 return Some(Ok((reader, slot, Err((variable.ty.clone(), shown)))));
2437 }
2438 Err(shown) => return Some(Err(format!("`{name}` is {}", shown.display))),
2439 };
2440 let mut so_far = name.clone();
2441 for segment in &segments[1..] {
2442 place = match reader.follow(place, segment, &so_far) {
2443 Ok(place) => place,
2444 Err(reason) => return Some(Err(reason)),
2445 };
2446 match segment {
2447 PathSegment::Member(member) => so_far = format!("{so_far}.{member}"),
2448 PathSegment::Index(key) => so_far = format!("{so_far}[{key}]"),
2449 PathSegment::Name(_) => {}
2450 }
2451 }
2452 Some(Ok((reader, slot, Ok(place))))
2453 }
2454
2455 #[must_use]
2463 pub fn inferred_variables(
2464 &self,
2465 trace: &TransactionTrace,
2466 step: usize,
2467 words: Option<&StorageWords<'_>>,
2468 ) -> Vec<DebugVariable> {
2469 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
2470 return Vec::new();
2471 };
2472 let Some(contract) = self.contract_at_step(step) else {
2473 return Vec::new();
2474 };
2475 let view = trace.steps.get(step);
2476 let stack = view.map(|view| view.stack().to_vec()).unwrap_or_default();
2477 let reader = ValueReader {
2478 memory: view.and_then(|view| view.memory()),
2479 calldata: self.calldata_at(trace, step),
2480 storage: words,
2481 layout: contract.storage_layout.as_ref(),
2482 types: &contract.types,
2483 scope: self.scope_at_step(step),
2484 };
2485 let unavailable = || DebugValue {
2486 display: "<unavailable>".to_owned(),
2487 raw: None,
2488 status: DebugValueStatus::Unavailable,
2489 };
2490 layout
2491 .iter()
2492 .map(|variable| {
2493 let words = variable
2494 .slot
2495 .and_then(|slot| stack.get(slot..slot + variable.words))
2496 .map(|words| words.iter().map(EvmWord::to_string).collect::<Vec<_>>());
2497 let value = match words {
2498 Some(words) => {
2499 let words = words.iter().map(String::as_str).collect::<Vec<_>>();
2500 reader.variable(&variable.ty, &words)
2501 }
2502 None => unavailable(),
2503 };
2504 DebugVariable {
2505 name: variable.name.clone(),
2506 ty: variable.ty.clone(),
2507 location: DebugLocation {
2508 kind: "stack".to_owned(),
2509 offset: variable.slot.unwrap_or_default() as u64,
2510 },
2511 value,
2512 }
2513 })
2514 .collect()
2515 }
2516
2517 #[must_use]
2525 pub fn frame_arguments(&self, frame: &Frame, state: FrameState<'_>) -> Vec<FrameArgument> {
2526 let Some(info) = self.steps.get(frame.entry_step) else {
2527 return Vec::new();
2528 };
2529 let Some(location) = info.location else {
2530 return Vec::new();
2531 };
2532 let Some(function_index) = location.function else {
2533 return Vec::new();
2534 };
2535 let Some(contract) = self.contracts.get(location.key.contract) else {
2536 return Vec::new();
2537 };
2538 if contract.function_entry_at_pc(self.pcs[frame.entry_step]) != Some(function_index) {
2539 return Vec::new();
2540 }
2541 let Some(function) = contract.functions.get(function_index) else {
2542 return Vec::new();
2543 };
2544 let Some(layout) = self.argument_layout(location.key.contract) else {
2545 return Vec::new();
2546 };
2547 decode_arguments(
2548 &function.params,
2549 state,
2550 layout,
2551 &contract.types,
2552 contract
2553 .types
2554 .scope_at(function.source_id, function.declaration_start),
2555 )
2556 }
2557
2558 #[must_use]
2559 pub fn location(&self, step: usize) -> Option<StepLocation> {
2560 let info = self.steps.get(step)?;
2561 self.describe(&info.location?, info.generated)
2562 }
2563
2564 fn describe(&self, location: &LocationRef, generated: bool) -> Option<StepLocation> {
2565 let contract = self.contracts.get(location.key.contract)?;
2566 let function = location
2567 .function
2568 .and_then(|index| contract.functions.get(index));
2569 Some(StepLocation {
2570 key: location.key,
2571 generated,
2572 contract_name: contract.name.clone(),
2573 path: contract
2574 .source_path(location.key.source_id)
2575 .map(str::to_owned)
2576 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2577 offset: location.offset,
2578 length: location.length,
2579 line: location.key.line,
2580 column: contract
2581 .column_of(location.key.source_id, location.offset)
2582 .unwrap_or(0),
2583 function: location.function.map(|function| FunctionId {
2584 contract: location.key.contract,
2585 function,
2586 }),
2587 function_name: function.map(|function| function.name.clone()),
2588 })
2589 }
2590
2591 #[must_use]
2601 pub fn next_source(&self, step: usize) -> Option<usize> {
2602 let current = *self.steps.get(step)?;
2603 if self.outside_functions(¤t) {
2604 return self.step_into(step);
2605 }
2606 let key = current.key;
2607 ((step + 1)..self.steps.len()).find(|&index| {
2608 let candidate = &self.steps[index];
2609 if candidate.frame_depth > current.frame_depth {
2610 return false;
2611 }
2612 candidate.frame_depth < current.frame_depth
2613 || candidate
2614 .key
2615 .is_some_and(|candidate| Some(candidate) != key)
2616 })
2617 }
2618
2619 #[must_use]
2622 pub fn step_into(&self, step: usize) -> Option<usize> {
2623 let current = *self.steps.get(step)?;
2624 let key = current.key;
2625 ((step + 1)..self.steps.len()).find(|&index| {
2626 let candidate = &self.steps[index];
2627 candidate.frame_depth != current.frame_depth
2628 || candidate
2629 .key
2630 .is_some_and(|candidate| Some(candidate) != key)
2631 })
2632 }
2633
2634 #[must_use]
2637 pub fn finish(&self, step: usize) -> Option<usize> {
2638 let current = *self.steps.get(step)?;
2639 ((step + 1)..self.steps.len()).find(|&index| {
2640 let candidate = &self.steps[index];
2641 candidate.frame_depth < current.frame_depth || candidate.starts_root
2642 })
2643 }
2644
2645 #[must_use]
2650 pub fn previous_source(&self, step: usize) -> Option<usize> {
2651 let current = *self.steps.get(step)?;
2652 if self.outside_functions(¤t) {
2653 return self.reverse_step_into(step);
2654 }
2655 self.previous_line_start(step, true)
2656 }
2657
2658 #[must_use]
2660 pub fn reverse_step_into(&self, step: usize) -> Option<usize> {
2661 self.steps.get(step)?;
2662 self.previous_line_start(step, false)
2663 }
2664
2665 fn previous_line_start(&self, step: usize, skip_deeper: bool) -> Option<usize> {
2666 let current = self.steps[step];
2667 let anchor = if current.key.is_some() {
2669 step
2670 } else {
2671 self.previous_mapped(step, current.frame_depth, skip_deeper)?
2672 };
2673 let start = self.run_start(anchor, skip_deeper);
2674 if start < step {
2675 return Some(start);
2676 }
2677 let previous = self.previous_mapped(start, current.frame_depth, skip_deeper)?;
2678 Some(self.run_start(previous, skip_deeper))
2679 }
2680
2681 #[must_use]
2684 pub fn reverse_finish(&self, step: usize) -> Option<usize> {
2685 let current = *self.steps.get(step)?;
2686 (0..step).rev().find(|&index| {
2687 let candidate = &self.steps[index];
2688 candidate.frame_depth < current.frame_depth || candidate.starts_root
2689 })
2690 }
2691
2692 fn run_start(&self, step: usize, skip_deeper: bool) -> usize {
2696 let info = self.steps[step];
2697 let key = info.key;
2698 let root = root_index(&self.roots, step);
2699 let mut current = step;
2700 while let Some(previous) = self.previous_mapped(current, info.frame_depth, skip_deeper) {
2701 let candidate = self.steps[previous];
2702 if candidate.frame_depth != info.frame_depth || candidate.key != key {
2703 break;
2704 }
2705 if root_index(&self.roots, previous) != root {
2706 break;
2707 }
2708 current = previous;
2709 }
2710 current
2711 }
2712
2713 fn outside_functions(&self, info: &StepInfo) -> bool {
2716 info.location.is_some() && info.key.is_none()
2717 }
2718
2719 fn previous_mapped(&self, step: usize, depth: u32, skip_deeper: bool) -> Option<usize> {
2722 (0..step).rev().find(|&index| {
2723 let candidate = &self.steps[index];
2724 candidate.key.is_some() && (!skip_deeper || candidate.frame_depth <= depth)
2725 })
2726 }
2727
2728 pub fn resolve_line(&self, file: Option<&str>, line: u64) -> Result<Vec<ResolvedLine>, String> {
2731 let mut sources = Vec::<(usize, u64, String)>::new();
2732 for (contract_index, contract) in self.contracts.iter().enumerate() {
2733 for (source_id, path) in &contract.info.sources {
2734 if file.is_none_or(|file| source_path_matches(path, file)) {
2735 sources.push((contract_index, *source_id, path.clone()));
2736 }
2737 }
2738 }
2739 if sources.is_empty() {
2740 return Err(match file {
2741 Some(file) => format!("source file not found: {file}"),
2742 None => "no source files are available".to_owned(),
2743 });
2744 }
2745 if file.is_none() {
2746 let mut paths = sources.iter().map(|(_, _, path)| path).collect::<Vec<_>>();
2747 paths.sort();
2748 paths.dedup();
2749 if paths.len() > 1 {
2750 return Err("line breakpoint is ambiguous; use break <file>:<line>".to_owned());
2751 }
2752 }
2753
2754 let mut resolved = sources
2755 .into_iter()
2756 .filter_map(|(contract_index, source_id, path)| {
2757 let effective = self.contracts[contract_index].effective_line(source_id, line)?;
2758 Some(ResolvedLine {
2759 key: LineKey {
2760 contract: contract_index,
2761 source_id,
2762 line: effective,
2763 },
2764 path,
2765 requested_line: line,
2766 })
2767 })
2768 .collect::<Vec<_>>();
2769 if resolved.is_empty() {
2770 return Err(match file {
2771 Some(file) => format!("no instruction maps to {file}:{line}"),
2772 None => format!("no instruction maps to line {line}"),
2773 });
2774 }
2775 if resolved.iter().any(|candidate| candidate.key.line == line) {
2780 resolved.retain(|candidate| candidate.key.line == line);
2781 }
2782 Ok(resolved)
2783 }
2784
2785 pub fn resolve_function(&self, name: &str) -> Result<Vec<ResolvedFunction>, String> {
2788 let (contract_name, function_name) = match name.rsplit_once('.') {
2789 Some((contract, function)) => (Some(contract), function),
2790 None => (None, name),
2791 };
2792 let resolved = self
2793 .contracts
2794 .iter()
2795 .enumerate()
2796 .filter(|(_, contract)| contract_name.is_none_or(|name| contract.name == name))
2797 .flat_map(|(contract_index, contract)| {
2798 contract
2799 .functions
2800 .iter()
2801 .enumerate()
2802 .filter(move |(_, function)| function.name == function_name)
2803 .map(move |(function_index, function)| ResolvedFunction {
2804 id: FunctionId {
2805 contract: contract_index,
2806 function: function_index,
2807 },
2808 name: function.name.clone(),
2809 contract_name: contract.name.clone(),
2810 path: contract
2811 .source_path(function.source_id)
2812 .map(str::to_owned)
2813 .unwrap_or_else(|| format!("source:{}", function.source_id)),
2814 line: function.declaration_line,
2815 })
2816 })
2817 .collect::<Vec<_>>();
2818 if resolved.is_empty() {
2819 return Err(format!("no function named `{name}` in the loaded sources"));
2820 }
2821 Ok(resolved)
2822 }
2823
2824 #[must_use]
2826 pub fn frames(&self, step: usize) -> Vec<Frame> {
2827 struct Record {
2828 entry_step: usize,
2829 last_step: usize,
2830 external: bool,
2831 }
2832 if step >= self.steps.len() {
2833 return Vec::new();
2834 }
2835 let mut stack = Vec::<Record>::new();
2836 for (index, info) in self.steps.iter().enumerate().take(step + 1) {
2837 let depth = info.frame_depth as usize;
2838 stack.truncate(if info.starts_root { 0 } else { depth + 1 });
2839 let entered_call =
2842 index == 0 || info.starts_root || info.evm_depth > self.steps[index - 1].evm_depth;
2843 let mut first_new = true;
2844 while stack.len() <= depth {
2845 stack.push(Record {
2846 entry_step: index,
2847 last_step: index,
2848 external: first_new && entered_call,
2849 });
2850 first_new = false;
2851 }
2852 if let Some(top) = stack.last_mut() {
2853 top.last_step = index;
2854 }
2855 }
2856
2857 stack
2858 .iter()
2859 .enumerate()
2860 .rev()
2861 .map(|(depth, record)| {
2862 let info = self.steps[record.last_step];
2863 let location = info
2864 .location
2865 .and_then(|location| self.describe(&location, info.generated));
2866 Frame {
2867 depth: depth as u32,
2868 external: record.external,
2869 address: info
2870 .address
2871 .and_then(|index| self.addresses.get(index))
2872 .cloned(),
2873 contract_name: info
2874 .contract
2875 .and_then(|index| self.contracts.get(index))
2876 .map(|contract| contract.name.clone()),
2877 function_name: location
2878 .as_ref()
2879 .and_then(|location| location.function_name.clone()),
2880 entry_step: record.entry_step,
2881 step: record.last_step,
2882 pc: self.pcs[record.last_step],
2883 location,
2884 arguments: Vec::new(),
2885 kind: self.root_kind(depth, record.entry_step),
2886 }
2887 })
2888 .collect()
2889 }
2890
2891 fn root_kind(&self, depth: usize, entry_step: usize) -> Option<String> {
2894 if depth != 0 {
2895 return None;
2896 }
2897 let root = self.roots.get(root_index(&self.roots, entry_step))?;
2898 if root.entry_step != entry_step {
2899 return None;
2900 }
2901 root.kind.clone()
2902 }
2903
2904 #[must_use]
2906 pub fn source_listing(&self, step: usize, radius: u64) -> Option<SourceListing> {
2907 let location = self.steps.get(step)?.location?;
2908 let contract = self.contracts.get(location.key.contract)?;
2909 let count = contract.line_count(location.key.source_id)?;
2910 let first = location.key.line.saturating_sub(radius).max(1);
2911 let last = location.key.line.saturating_add(radius).min(count);
2912 let lines = (first..=last)
2913 .filter_map(|line| {
2914 contract
2915 .line_text(location.key.source_id, line)
2916 .map(|text| (line, text.to_owned()))
2917 })
2918 .collect();
2919 Some(SourceListing {
2920 path: contract
2921 .source_path(location.key.source_id)
2922 .map(str::to_owned)
2923 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2924 current_line: location.key.line,
2925 lines,
2926 })
2927 }
2928}
2929
2930fn prove_argument_layouts(
2945 trace: &TransactionTrace,
2946 contracts: &[ContractDebugInfo],
2947 framed: &[FramedStep],
2948) -> Vec<Evidence> {
2949 let mut evidence = vec![Evidence::Unknown; contracts.len()];
2950 if contracts.is_empty() || framed.is_empty() {
2951 return evidence;
2952 }
2953 let mut calldata_by_entry = FxHashMap::<usize, &str>::default();
2956 calldata_by_entry.insert(0, trace.input_data.as_str());
2957 for call in &trace.artifacts.calls {
2958 if let Some(entry_step) = call.entry_step {
2959 calldata_by_entry.insert(entry_step, call.input.as_str());
2960 }
2961 }
2962
2963 let mut examined = std::collections::HashSet::<u32>::new();
2964 let mut frame_calldata = FxHashMap::<u32, &str>::default();
2965 for (index, step) in framed.iter().enumerate() {
2966 let entered = index == 0 || framed[index - 1].frame_id != step.frame_id;
2967 if entered {
2968 if let Some(calldata) = calldata_by_entry.get(&index) {
2969 frame_calldata.insert(step.frame_id, calldata);
2970 }
2971 }
2972 let (Some(contract_index), Some(function_index)) = (step.contract, step.entry) else {
2973 continue;
2974 };
2975 if !examined.insert(step.frame_id) {
2978 continue;
2979 }
2980 let Some(calldata) = frame_calldata.get(&step.frame_id) else {
2981 continue;
2982 };
2983 let Some(function) = contracts[contract_index].functions.get(function_index) else {
2984 continue;
2985 };
2986 let stack = trace.steps.step(index).stack().to_vec();
2987 let observed = argument_evidence(function, calldata, &stack);
2988 evidence[contract_index] = evidence[contract_index].merge(observed);
2989 }
2990 evidence
2991}
2992
2993fn argument_evidence(function: &SourceFunction, calldata: &str, stack: &[EvmWord]) -> Evidence {
3001 let count = function.params.len();
3002 if count == 0 || stack.len() < count {
3003 return Evidence::Unknown;
3004 }
3005 if !function.params.iter().all(readable_parameter) {
3007 return Evidence::Unknown;
3008 }
3009 let signature = format!(
3010 "{}({})",
3011 function.name,
3012 function
3013 .params
3014 .iter()
3015 .map(|param| canonical_value_type(¶m.ty))
3016 .collect::<Vec<_>>()
3017 .join(",")
3018 );
3019 let Ok(selector) = function_selector(&signature) else {
3020 return Evidence::Unknown;
3021 };
3022 let data = calldata.trim_start_matches("0x");
3023 let Some((encoded_selector, arguments)) = data.split_at_checked(8) else {
3024 return Evidence::Unknown;
3025 };
3026 if encoded_selector.to_ascii_lowercase() != hex_bytes(&selector) {
3028 return Evidence::Unknown;
3029 }
3030 let expected = (0..count)
3031 .map(|index| {
3032 arguments
3033 .get(index * 64..(index + 1) * 64)
3034 .map(str::to_ascii_lowercase)
3035 })
3036 .collect::<Option<Vec<_>>>();
3037 let Some(expected) = expected else {
3038 return Evidence::Unknown;
3039 };
3040 let checkable = (0..count)
3043 .filter(|index| is_value_type(&function.params[*index].ty))
3044 .collect::<Vec<_>>();
3045 if checkable.is_empty() {
3046 return Evidence::Unknown;
3047 }
3048 let matches = |order: ArgumentOrder| {
3049 checkable.iter().all(|index| {
3050 let candidate = &stack[order.word_index(*index, count, stack.len())];
3051 hex_bytes(candidate.as_bytes()) == expected[*index]
3052 })
3053 };
3054 let distinguishing = checkable.iter().any(|index| {
3056 ArgumentOrder::FirstOnTop.word_index(*index, count, stack.len())
3057 != ArgumentOrder::LastOnTop.word_index(*index, count, stack.len())
3058 });
3059 let first = matches(ArgumentOrder::FirstOnTop);
3060 let last = matches(ArgumentOrder::LastOnTop);
3061 match (first, last) {
3062 (true, true) => Evidence::TopWords,
3065 (true, false) if distinguishing => Evidence::Ordered(ArgumentOrder::FirstOnTop),
3066 (false, true) if distinguishing => Evidence::Ordered(ArgumentOrder::LastOnTop),
3067 (true, false) | (false, true) => Evidence::TopWords,
3069 (false, false) => Evidence::Contradicted,
3071 }
3072}
3073
3074fn canonical_value_type(ty: &str) -> String {
3077 match ty {
3078 "uint" => "uint256".to_owned(),
3079 "int" => "int256".to_owned(),
3080 "address payable" => "address".to_owned(),
3081 other => other.to_owned(),
3082 }
3083}
3084
3085fn hex_bytes(bytes: &[u8]) -> String {
3086 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
3087}
3088
3089#[must_use]
3091pub fn call_target(step: StepView<'_>) -> Option<String> {
3092 if !opcode::is_call(step.op()) {
3093 return None;
3094 }
3095 address_from_word(&step.stack().peek(1)?.to_string())
3096}
3097
3098#[must_use]
3101pub fn address_from_word(word: &str) -> Option<String> {
3102 let hex = word.trim_start_matches("0x");
3103 if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
3104 return None;
3105 }
3106 let padded = format!("{hex:0>40}");
3107 let address = padded.get(padded.len() - 40..)?;
3108 if address.bytes().all(|byte| byte == b'0') {
3109 return None;
3110 }
3111 Some(format!("0x{}", address.to_ascii_lowercase()))
3112}
3113
3114#[cfg(test)]
3115mod tests {
3116 use std::collections::BTreeMap;
3117
3118 use serde_json::json;
3119 use soldb_core::{ContractCreation, ExecutionCall, StepSnapshot, TraceStep, TransactionTrace};
3120 use soldb_ethdebug::{
3121 function_selector, source_range, CodeGenerator, EthdebugInfo, Instruction,
3122 };
3123
3124 use soldb_core::{EvmWord, Word as StackWord};
3125
3126 use crate::FrameState;
3127
3128 use super::{
3129 address_from_word, normalize_address, ArgumentLayout, ArgumentOrder, ContractDebugInfo,
3130 InferredVariable, JumpMarker, LineKey, LocalsStatus, NearestLines, SourceCache, StepInfo,
3131 StepMap, VariableKind,
3132 };
3133
3134 const SOURCE: &str = "\
3136contract C {
3137 function outer(uint256 a) public {
3138 uint256 b = a + 1;
3139 inner(b);
3140 b = 0;
3141 }
3142 function inner(uint256 x) internal {
3143 x += 1;
3144 }
3145}
3146";
3147
3148 fn offset_of(needle: &str) -> u64 {
3149 SOURCE.find(needle).expect(needle) as u64
3150 }
3151
3152 fn instruction(pc: u64, offset: u64, length: u64) -> Instruction {
3153 instruction_with(pc, offset, length, json!({}))
3154 }
3155
3156 fn instruction_with(
3158 pc: u64,
3159 offset: u64,
3160 length: u64,
3161 extra: serde_json::Value,
3162 ) -> Instruction {
3163 let mut context = json!({"code": source_range(0, offset, length)});
3164 for (key, value) in extra.as_object().expect("object") {
3165 context[key] = value.clone();
3166 }
3167 Instruction::new(pc, "JUMPDEST", Some(context))
3168 }
3169
3170 fn contract(address: Option<&str>) -> ContractDebugInfo {
3171 let whole = SOURCE.len() as u64;
3172 let info = EthdebugInfo {
3173 compilation: serde_json::Value::Null,
3174 contract_name: "C".to_owned(),
3175 environment: "runtime".to_owned(),
3176 instructions: vec![
3177 instruction(0, 0, whole),
3179 instruction(1, 0, whole),
3180 instruction(10, offset_of("function outer"), 90),
3182 instruction(11, offset_of("uint256 b = a + 1;"), 18),
3183 instruction(12, offset_of("inner(b);"), 9),
3184 instruction(13, offset_of("inner(b);"), 9),
3185 instruction(14, offset_of("b = 0;"), 6),
3186 instruction(20, offset_of("function inner"), 50),
3188 instruction(21, offset_of("x += 1;"), 7),
3189 instruction(30, 0, whole),
3191 instruction(40, 0, whole),
3193 ],
3194 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
3195 variable_locations: BTreeMap::new(),
3196 };
3197 ContractDebugInfo::new(address, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]))
3198 }
3199
3200 fn step(pc: u64, depth: u64, op: &str, stack: &[&str]) -> TraceStep {
3201 TraceStep {
3202 pc,
3203 op: op.into(),
3204 gas: 0,
3205 gas_cost: 0,
3206 depth,
3207 stack: stack.iter().map(|word| StackWord::from(*word)).collect(),
3208 memory: None,
3209 storage: None,
3210 error: None,
3211 snapshot: StepSnapshot::default(),
3212 }
3213 }
3214
3215 fn trace(steps: Vec<TraceStep>) -> TransactionTrace {
3216 TransactionTrace {
3217 tx_hash: None,
3218 from_addr: "0x1".to_owned(),
3219 to_addr: Some("0xAAaa000000000000000000000000000000000001".to_owned()),
3220 value: "0x0".to_owned(),
3221 input_data: "0x".to_owned(),
3222 gas_used: 0,
3223 output: "0x".to_owned(),
3224 success: true,
3225 error: None,
3226 debug_trace_available: true,
3227 contract_address: None,
3228 backend: None,
3229 capabilities: Default::default(),
3230 artifacts: Default::default(),
3231 steps: steps.into(),
3232 }
3233 }
3234
3235 fn outer_calls_inner() -> TransactionTrace {
3238 trace(vec![
3239 step(0, 1, "PUSH1", &[]),
3240 step(1, 1, "JUMPI", &[]),
3241 step(10, 1, "JUMPDEST", &[]),
3242 step(11, 1, "ADD", &[]),
3243 step(12, 1, "PUSH1", &[]),
3244 step(20, 1, "JUMPDEST", &[]),
3245 step(21, 1, "ADD", &[]),
3246 step(21, 1, "SWAP1", &[]),
3247 step(13, 1, "JUMPDEST", &[]),
3248 step(14, 1, "POP", &[]),
3249 step(30, 1, "STOP", &[]),
3250 ])
3251 }
3252
3253 const PAY_SOURCE: &str = "\
3256contract P {
3257 function pay(address to, uint256 amount) public {
3258 total(to, amount);
3259 }
3260 function total(address to, uint256 amount) internal {
3261 amount += 1;
3262 }
3263}
3264";
3265
3266 fn pay_contract() -> ContractDebugInfo {
3267 let whole = PAY_SOURCE.len() as u64;
3268 let at = |needle: &str| PAY_SOURCE.find(needle).expect(needle) as u64;
3269 let info = EthdebugInfo {
3270 compilation: serde_json::Value::Null,
3271 contract_name: "P".to_owned(),
3272 environment: "runtime".to_owned(),
3273 instructions: vec![
3274 instruction(0, 0, whole),
3275 instruction(10, at("function pay"), 80),
3276 instruction(11, at("total(to, amount);"), 18),
3277 instruction(20, at("function total"), 70),
3278 instruction(21, at("amount += 1;"), 12),
3279 ],
3280 sources: BTreeMap::from([(0, "P.sol".to_owned())]),
3281 variable_locations: BTreeMap::new(),
3282 };
3283 ContractDebugInfo::new(
3284 None,
3285 "P",
3286 info,
3287 BTreeMap::from([(0, PAY_SOURCE.to_owned())]),
3288 )
3289 }
3290
3291 fn evm_words(stack: &[StackWord]) -> Vec<EvmWord> {
3293 stack
3294 .iter()
3295 .map(|word| EvmWord::parse(word).expect("test word"))
3296 .collect()
3297 }
3298
3299 fn word_of(value: &str) -> StackWord {
3300 StackWord::from(format!("0x{:0>64}", value.trim_start_matches("0x")).as_str())
3301 }
3302
3303 fn pay_trace(entry_stack: &[StackWord], inner_stack: &[StackWord]) -> TransactionTrace {
3306 let selector = soldb_ethdebug::function_selector("pay(address,uint256)").expect("selector");
3307 let calldata = format!(
3308 "0x{}{}{}",
3309 selector
3310 .iter()
3311 .map(|byte| format!("{byte:02x}"))
3312 .collect::<String>(),
3313 &word_of(PAY_TO)[2..],
3314 &word_of(PAY_AMOUNT)[2..]
3315 );
3316 fn borrow(stack: &[StackWord]) -> Vec<&str> {
3317 stack.iter().map(|word| &**word).collect()
3318 }
3319 let mut trace = trace(vec![
3320 step(0, 1, "PUSH1", &[]),
3321 step(10, 1, "JUMPDEST", &borrow(entry_stack)),
3322 step(11, 1, "JUMP", &borrow(inner_stack)),
3323 step(20, 1, "JUMPDEST", &borrow(inner_stack)),
3324 step(21, 1, "ADD", &[]),
3325 ]);
3326 trace.input_data = calldata;
3327 trace
3328 }
3329
3330 const PAY_TO: &str = "00000000000000000000000000000000000000aa";
3331 const PAY_AMOUNT: &str = "7";
3332
3333 #[test]
3334 fn the_trace_proves_which_end_the_first_argument_is_at() {
3335 let to = word_of(PAY_TO);
3336 let amount = word_of(PAY_AMOUNT);
3337 let tag = word_of("2a");
3338
3339 let first_on_top = [tag.clone(), amount.clone(), to.clone()];
3341 let trace = pay_trace(&first_on_top, &first_on_top);
3342 let map = StepMap::new(&trace, vec![pay_contract()]);
3343 assert_eq!(
3344 map.argument_layout(0),
3345 Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))
3346 );
3347 let frames = map.frames(4);
3348 let inner = frames.first().expect("innermost frame");
3349 assert_eq!(inner.function_name.as_deref(), Some("total"));
3350 let arguments = map.frame_arguments(
3351 inner,
3352 FrameState {
3353 stack: &evm_words(&first_on_top),
3354 memory: None,
3355 },
3356 );
3357 assert_eq!(
3358 arguments
3359 .iter()
3360 .map(|argument| format!("{} = {}", argument.name, argument.value.display))
3361 .collect::<Vec<_>>(),
3362 [
3363 "to = 0x00000000000000000000000000000000000000aa".to_owned(),
3364 "amount = 7".to_owned()
3365 ]
3366 );
3367
3368 let last_on_top = [tag.clone(), to.clone(), amount.clone()];
3370 let trace = pay_trace(&last_on_top, &last_on_top);
3371 let map = StepMap::new(&trace, vec![pay_contract()]);
3372 assert_eq!(
3373 map.argument_layout(0),
3374 Some(ArgumentLayout::Ordered(ArgumentOrder::LastOnTop))
3375 );
3376 let frames = map.frames(4);
3377 let arguments = map.frame_arguments(
3378 frames.first().expect("frame"),
3379 FrameState {
3380 stack: &evm_words(&last_on_top),
3381 memory: None,
3382 },
3383 );
3384 assert_eq!(
3385 arguments[0].value.display,
3386 "0x00000000000000000000000000000000000000aa"
3387 );
3388 assert_eq!(arguments[1].value.display, "7");
3389
3390 let wrapper = [tag.clone(), word_of("dead"), word_of("beef")];
3393 let trace = pay_trace(&wrapper, &first_on_top);
3394 let map = StepMap::new(&trace, vec![pay_contract()]);
3395 assert_eq!(map.argument_layout(0), None);
3396 let frames = map.frames(4);
3397 assert!(map
3398 .frame_arguments(
3399 frames.first().expect("frame"),
3400 FrameState {
3401 stack: &evm_words(&first_on_top),
3402 memory: None
3403 }
3404 )
3405 .is_empty());
3406
3407 let mut unknown = pay_trace(&first_on_top, &first_on_top);
3409 unknown.input_data = "0x".to_owned();
3410 let map = StepMap::new(&unknown, vec![pay_contract()]);
3411 assert_eq!(map.argument_layout(0), None);
3412 let frames = map.frames(4);
3413 assert!(map
3414 .frame_arguments(
3415 frames.first().expect("frame"),
3416 FrameState {
3417 stack: &evm_words(&first_on_top),
3418 memory: None
3419 }
3420 )
3421 .is_empty());
3422 }
3423
3424 #[test]
3425 fn one_parameter_is_on_top_under_either_order() {
3426 let mut trace = outer_calls_inner();
3429 let selector = soldb_ethdebug::function_selector("outer(uint256)").expect("selector");
3430 let amount = word_of("5");
3431 trace.input_data = format!(
3432 "0x{}{}",
3433 selector
3434 .iter()
3435 .map(|byte| format!("{byte:02x}"))
3436 .collect::<String>(),
3437 &amount[2..]
3438 );
3439 let stack = [word_of("2a"), amount.clone()];
3440 let mut steps = trace.steps.to_steps();
3441 steps[2].snapshot =
3442 StepSnapshot::new(stack.to_vec(), None, BTreeMap::new(), BTreeMap::new());
3443 trace.steps = steps.into();
3444 let map = StepMap::new(&trace, vec![contract(None)]);
3445 assert_eq!(map.argument_layout(0), Some(ArgumentLayout::TopWords));
3446 let frames = map.frames(3);
3447 let outer = frames
3448 .iter()
3449 .find(|frame| frame.function_name.as_deref() == Some("outer"))
3450 .expect("outer frame");
3451 let arguments = map.frame_arguments(
3452 outer,
3453 FrameState {
3454 stack: &evm_words(&stack),
3455 memory: None,
3456 },
3457 );
3458 assert_eq!(arguments.len(), 1);
3459 assert_eq!(arguments[0].name, "a");
3460 assert_eq!(arguments[0].value.display, "5");
3461
3462 let map = StepMap::new(&trace, vec![pay_contract()]);
3464 assert_eq!(map.argument_layout(0), None);
3465 }
3466
3467 fn key(line: u64) -> Option<LineKey> {
3468 Some(LineKey {
3469 contract: 0,
3470 source_id: 0,
3471 line,
3472 })
3473 }
3474
3475 #[test]
3476 fn maps_steps_to_lines_and_infers_internal_frames() {
3477 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3478 assert!(map.has_source());
3479 assert_eq!(map.line_key(0), None);
3481 assert_eq!(map.location(0).expect("location").line, 1);
3482 assert!(!map.location(0).expect("location").generated);
3483 assert_eq!(map.line_key(2), key(2));
3484 assert_eq!(map.line_key(3), key(3));
3485 assert_eq!(map.line_key(4), key(4));
3486 assert_eq!(map.line_key(5), key(7));
3487 assert_eq!(map.line_key(6), key(8));
3488 assert_eq!(map.line_key(8), key(4));
3489 assert_eq!(map.line_key(10), None);
3490
3491 let depths = (0..11)
3493 .map(|step| map.frame_depth(step).expect("depth"))
3494 .collect::<Vec<_>>();
3495 assert_eq!(depths, vec![0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 0]);
3496 assert!(map.is_frame_entry(2));
3497 assert!(map.is_frame_entry(5));
3498 assert!(!map.is_frame_entry(8));
3499 assert_eq!(
3500 map.location(6).expect("location").function_name.as_deref(),
3501 Some("inner")
3502 );
3503 assert_eq!(map.location(6).expect("location").path, "C.sol");
3504 assert_eq!(map.location(6).expect("location").column, 9);
3505 }
3506
3507 #[test]
3508 fn next_steps_over_calls_and_step_enters_them() {
3509 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3510 assert_eq!(map.next_source(0), Some(2));
3512 assert_eq!(map.next_source(2), Some(3));
3513 assert_eq!(map.next_source(4), Some(9));
3515 assert_eq!(map.step_into(4), Some(5));
3516 assert_eq!(map.step_into(5), Some(6));
3517 assert_eq!(map.next_source(6), Some(8));
3519 assert_eq!(map.finish(6), Some(8));
3520 assert_eq!(map.next_source(9), Some(10));
3522 assert_eq!(map.finish(9), Some(10));
3523 assert_eq!(map.next_source(10), None);
3524 assert_eq!(map.finish(10), None);
3525 }
3526
3527 #[test]
3528 fn reverse_stepping_lands_on_line_starts() {
3529 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3530 assert_eq!(map.previous_source(9), Some(4));
3532 assert_eq!(map.previous_source(7), Some(6));
3534 assert_eq!(map.previous_source(6), Some(5));
3535 assert_eq!(map.reverse_step_into(9), Some(8));
3537 assert_eq!(map.reverse_step_into(8), Some(6));
3538 assert_eq!(map.reverse_finish(6), Some(4));
3540 assert_eq!(map.reverse_finish(0), None);
3541 assert_eq!(map.previous_source(0), None);
3542 assert_eq!(map.previous_source(10), Some(9));
3545 }
3546
3547 #[test]
3548 fn line_starts_skip_returns_into_the_middle_of_a_line() {
3549 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3550 assert!(map.is_line_start(3));
3551 assert!(map.is_line_start(4));
3552 assert!(map.is_line_start(6));
3553 assert!(!map.is_line_start(7));
3554 assert!(!map.is_line_start(8));
3556 assert!(map.is_line_start(9));
3557 }
3558
3559 #[test]
3560 fn resolves_line_and_function_breakpoints() {
3561 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3562 let resolved = map.resolve_line(Some("C.sol"), 4).expect("line 4");
3563 assert_eq!(resolved.len(), 1);
3564 assert_eq!(resolved[0].key, key(4).unwrap());
3565 assert_eq!(resolved[0].path, "C.sol");
3566 assert_eq!(
3568 map.resolve_line(None, 8).expect("line 8")[0].key,
3569 key(8).unwrap()
3570 );
3571 assert_eq!(
3574 map.resolve_line(None, 10).expect("line 10")[0].key,
3575 key(1).unwrap()
3576 );
3577 assert_eq!(
3578 map.resolve_line(None, 99).expect_err("past the end"),
3579 "no instruction maps to line 99"
3580 );
3581 assert_eq!(
3582 map.resolve_line(Some("D.sol"), 1)
3583 .expect_err("unknown file"),
3584 "source file not found: D.sol"
3585 );
3586
3587 let inner = map.resolve_function("inner").expect("inner");
3588 assert_eq!(inner.len(), 1);
3589 assert_eq!(inner[0].line, 7);
3590 assert_eq!(inner[0].contract_name, "C");
3591 assert_eq!(
3592 map.resolve_function("C.outer").expect("qualified")[0].line,
3593 2
3594 );
3595 assert!(map.resolve_function("D.outer").is_err());
3596 assert!(map.resolve_function("missing").is_err());
3597 }
3598
3599 #[test]
3600 fn frames_list_the_call_structure_innermost_first() {
3601 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3602 let frames = map.frames(6);
3603 assert_eq!(frames.len(), 3);
3604 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3605 assert_eq!(frames[0].depth, 2);
3606 assert!(!frames[0].external);
3607 assert_eq!(frames[0].step, 6);
3608 assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
3609 assert_eq!(
3610 frames[1].location.as_ref().map(|location| location.line),
3611 Some(4)
3612 );
3613 assert_eq!(frames[1].step, 4);
3614 assert!(frames[2].external);
3615 assert_eq!(frames[2].depth, 0);
3616 assert_eq!(
3617 frames[2].address.as_deref(),
3618 Some("0xaaaa000000000000000000000000000000000001")
3619 );
3620 assert!(map.frames(99).is_empty());
3621
3622 let listing = map.source_listing(6, 1).expect("listing");
3623 assert_eq!(listing.current_line, 8);
3624 assert_eq!(listing.lines.len(), 3);
3625 assert_eq!(listing.lines[1].0, 8);
3626 assert_eq!(listing.lines[1].1.trim(), "x += 1;");
3627 }
3628
3629 #[test]
3630 fn external_calls_map_through_the_callee_contract() {
3631 let callee = "0xbbbb000000000000000000000000000000000002";
3633 let word = format!("0x{:0>64}", callee.trim_start_matches("0x"));
3634 let steps = vec![
3635 step(0, 1, "PUSH1", &[]),
3636 step(
3637 12,
3638 1,
3639 "CALL",
3640 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3641 ),
3642 step(21, 2, "JUMPDEST", &[]),
3643 step(21, 2, "ADD", &[]),
3644 step(13, 1, "JUMPDEST", &[]),
3645 step(30, 1, "STOP", &[]),
3646 ];
3647 let root = contract(Some("0xAAAA000000000000000000000000000000000001"));
3648 let map = StepMap::new(&trace(steps), vec![root, contract(Some(callee))]);
3649 assert_eq!(
3650 map.executing_address(1),
3651 Some("0xaaaa000000000000000000000000000000000001")
3652 );
3653 assert_eq!(map.executing_address(2), Some(callee));
3654 assert_eq!(
3655 map.line_key(2),
3656 Some(LineKey {
3657 contract: 1,
3658 source_id: 0,
3659 line: 8
3660 })
3661 );
3662 assert_eq!(map.frame_depth(1), Some(1));
3663 assert_eq!(map.frame_depth(2), Some(3));
3665 assert!(map.is_frame_entry(2));
3666 assert_eq!(map.frame_depth(4), Some(1));
3667 assert_eq!(map.next_source(1), Some(5));
3669 assert_eq!(map.step_into(1), Some(2));
3670 let frames = map.frames(2);
3671 assert_eq!(frames.len(), 4);
3672 assert!(frames[1].external);
3673 assert_eq!(frames[1].address.as_deref(), Some(callee));
3674 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3675 }
3676
3677 fn program(address: &str, environment: &str, spans: &[(u64, &str)]) -> ContractDebugInfo {
3680 let info = EthdebugInfo {
3681 compilation: serde_json::Value::Null,
3682 contract_name: "C".to_owned(),
3683 environment: environment.to_owned(),
3684 instructions: spans
3685 .iter()
3686 .map(|(pc, needle)| instruction(*pc, offset_of(needle), needle.len() as u64))
3687 .collect(),
3688 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
3689 variable_locations: BTreeMap::new(),
3690 };
3691 ContractDebugInfo::new(
3692 Some(address),
3693 "C",
3694 info,
3695 BTreeMap::from([(0, SOURCE.to_owned())]),
3696 )
3697 }
3698
3699 const ROOT: &str = "0xaaaa000000000000000000000000000000000001";
3700 const CREATED: &str = "0xcccc000000000000000000000000000000000003";
3701
3702 fn creation(entry_step: usize, exit_step: usize, address: Option<&str>) -> ContractCreation {
3703 ContractCreation {
3704 id: 0,
3705 parent_id: None,
3706 depth: 2,
3707 entry_step: Some(entry_step),
3708 exit_step: Some(exit_step),
3709 create_type: "CREATE".to_owned(),
3710 caller: ROOT.to_owned(),
3711 address: address.map(str::to_owned),
3712 value: "0x0".to_owned(),
3713 init_code: "0x".to_owned(),
3714 gas_limit: 0,
3715 gas_used: None,
3716 output: None,
3717 success: Some(address.is_some()),
3718 error: None,
3719 code_hash: None,
3720 }
3721 }
3722
3723 fn call(
3724 id: usize,
3725 entry_step: usize,
3726 exit_step: usize,
3727 call_type: &str,
3728 bytecode_address: &str,
3729 ) -> ExecutionCall {
3730 ExecutionCall {
3731 id,
3732 parent_id: None,
3733 depth: 2,
3734 entry_step: Some(entry_step),
3735 exit_step: Some(exit_step),
3736 call_type: call_type.to_owned(),
3737 from: ROOT.to_owned(),
3738 to: ROOT.to_owned(),
3739 bytecode_address: bytecode_address.to_owned(),
3740 value: "0x0".to_owned(),
3741 input: "0x".to_owned(),
3742 gas_limit: 0,
3743 gas_used: None,
3744 output: None,
3745 success: Some(true),
3746 error: None,
3747 code_hash: None,
3748 }
3749 }
3750
3751 #[test]
3752 fn frames_map_through_the_program_with_their_code_hash() {
3753 let word = format!("0x{:0>64}", CREATED.trim_start_matches("0x"));
3756 let call_created = ["0x0", "0x0", "0x0", "0x0", "0x0", word.as_str(), "0x0"];
3757 let mut trace = trace(vec![
3758 step(0, 1, "PUSH1", &[]),
3759 step(12, 1, "CALL", &call_created),
3760 step(21, 2, "ADD", &[]),
3761 step(13, 1, "CALL", &call_created),
3762 step(21, 2, "ADD", &[]),
3763 step(14, 1, "CALL", &call_created),
3764 step(21, 2, "ADD", &[]),
3765 step(30, 1, "STOP", &[]),
3766 ]);
3767 let mut first = call(0, 2, 3, "CALL", CREATED);
3768 first.code_hash = Some("0x01".to_owned());
3769 let mut second = call(1, 4, 5, "CALL", CREATED);
3770 second.code_hash = Some("0x02".to_owned());
3771 let third = call(2, 6, 7, "CALL", CREATED);
3773 trace.artifacts.calls = vec![first, second, third];
3774 let mut original = program(CREATED, "call", &[(21, "x += 1;")]);
3775 original.code_hash = Some("0x01".to_owned());
3776 let mut replaced = program(CREATED, "call", &[(21, "b = 0;")]);
3777 replaced.code_hash = Some("0x02".to_owned());
3778 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), original, replaced]);
3779 let line = |step| map.line_key(step).map(|key| (key.contract, key.line));
3780 assert_eq!(line(2), Some((1, 8)));
3781 assert_eq!(line(4), Some((2, 5)));
3782 assert_eq!(line(6), Some((1, 8)));
3783 }
3784
3785 fn root_creates_then_calls() -> TransactionTrace {
3788 let word = format!("0x{:0>64}", CREATED.trim_start_matches("0x"));
3789 let mut trace = trace(vec![
3790 step(0, 1, "PUSH1", &[]),
3791 step(12, 1, "CREATE", &["0x0", "0x0", "0x0"]),
3793 step(21, 2, "JUMPDEST", &[]),
3794 step(21, 2, "RETURN", &[]),
3795 step(13, 1, "JUMPDEST", &[]),
3796 step(
3797 14,
3798 1,
3799 "CALL",
3800 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3801 ),
3802 step(21, 2, "JUMPDEST", &[]),
3803 step(30, 1, "STOP", &[]),
3804 ]);
3805 trace
3806 .artifacts
3807 .creations
3808 .push(creation(2, 4, Some(CREATED)));
3809 trace.artifacts.calls.push(call(0, 6, 7, "CALL", CREATED));
3810 trace
3811 }
3812
3813 #[test]
3814 fn creations_map_through_the_created_contracts_creation_program() {
3815 let trace = root_creates_then_calls();
3816 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3820 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3821 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed, creation]);
3822
3823 assert_eq!(map.executing_address(2), Some(CREATED));
3824 assert_eq!(map.storage_address(2), Some(CREATED));
3825 assert_eq!(
3826 map.line_key(2).map(|key| (key.contract, key.line)),
3827 Some((2, 8))
3828 );
3829 assert_eq!(
3830 map.line_key(6).map(|key| (key.contract, key.line)),
3831 Some((1, 5))
3832 );
3833 let frames = map.frames(2);
3835 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3836 assert!(frames[1].external);
3837 assert_eq!(frames[1].address.as_deref(), Some(CREATED));
3838 assert_eq!(map.frame_depth(4), Some(1));
3839 }
3840
3841 #[test]
3842 fn a_line_resolves_only_in_the_programs_that_map_it() {
3843 let trace = root_creates_then_calls();
3847 let whole = SOURCE.len() as u64;
3848 let mut deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3849 deployed.info.instructions.push(instruction(20, 0, whole));
3850 let mut creation = program(CREATED, "create", &[(21, "x += 1;")]);
3851 creation.info.instructions.push(instruction(20, 0, whole));
3852 let map = StepMap::new(&trace, vec![deployed, creation]);
3853 let resolved = map.resolve_line(Some("C.sol"), 8).expect("line 8");
3854 assert_eq!(resolved.len(), 1);
3855 assert_eq!(resolved[0].key.contract, 1);
3856 assert_eq!(resolved[0].key.line, 8);
3857 let resolved = map.resolve_line(Some("C.sol"), 10).expect("line 10");
3859 assert_eq!(resolved.len(), 2);
3860 assert!(resolved.iter().all(|line| line.key.line == 1));
3861 }
3862
3863 #[test]
3864 fn a_creation_uses_the_only_program_at_its_address() {
3865 let trace = root_creates_then_calls();
3868 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3869 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed]);
3870 assert_eq!(
3871 map.line_key(2).map(|key| (key.contract, key.line)),
3872 Some((1, 5))
3873 );
3874 }
3875
3876 #[test]
3877 fn a_creation_the_backend_did_not_record_has_no_contract() {
3878 let mut trace = root_creates_then_calls();
3879 trace.artifacts.creations.clear();
3880 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3881 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), creation]);
3882 assert_eq!(map.executing_address(2), None);
3883 assert_eq!(map.line_key(2), None);
3884 assert_eq!(map.executing_address(6), Some(CREATED));
3886 }
3887
3888 #[test]
3889 fn a_deployment_root_prefers_the_creation_program() {
3890 let mut trace = trace(vec![step(21, 1, "JUMPDEST", &[]), step(30, 1, "STOP", &[])]);
3891 trace.to_addr = None;
3892 trace.contract_address = Some(CREATED.to_owned());
3893 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3894 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3895 let map = StepMap::new(&trace, vec![deployed, creation]);
3896 assert_eq!(
3897 map.line_key(0).map(|key| (key.contract, key.line)),
3898 Some((1, 8))
3899 );
3900 }
3901
3902 #[test]
3903 fn frames_are_resolved_from_recorded_calls_before_the_stack() {
3904 let callee = "0xbbbb000000000000000000000000000000000002";
3905 let precompile = "0x0000000000000000000000000000000000000001";
3906 let mut trace = trace(vec![
3908 step(0, 1, "PUSH1", &[]),
3909 step(11, 1, "STATICCALL", &[]),
3910 step(12, 1, "DELEGATECALL", &[]),
3911 step(21, 2, "JUMPDEST", &[]),
3912 step(21, 2, "ADD", &[]),
3913 step(13, 1, "JUMPDEST", &[]),
3914 ]);
3915 trace
3918 .artifacts
3919 .calls
3920 .push(call(0, 2, 2, "STATICCALL", precompile));
3921 trace
3922 .artifacts
3923 .calls
3924 .push(call(1, 3, 5, "DELEGATECALL", callee));
3925 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), contract(Some(callee))]);
3926
3927 assert_eq!(map.executing_address(2), Some(ROOT));
3928 assert_eq!(map.frame_depth(2), Some(1));
3929 assert_eq!(map.executing_address(3), Some(callee));
3930 assert_eq!(map.line_key(3).map(|key| key.contract), Some(1));
3931 assert_eq!(map.storage_address(3), Some(ROOT));
3933 }
3934
3935 const LEGACY_SOURCE: &str = "contract L {
3938 uint256 total;
3939 modifier tracked(uint256 tag) {
3940 uint256 before = total;
3941 _;
3942 total = before + tag;
3943 }
3944 function outer(uint256 a) public returns (uint256 sum) {
3945 uint256 twice = a * 2;
3946 if (twice > 2) {
3947 uint256 inner = twice - 2;
3948 sum = inner;
3949 }
3950 sum = twice + helper(twice);
3951 }
3952 function helper(uint256 x) internal pure returns (uint256 r) {
3953 uint256 y = x + 1;
3954 r = y;
3955 }
3956 function guarded(uint256 v) public tracked(9) {
3957 uint256 kept = v;
3958 total = kept;
3959 }
3960 function set(uint256 v) public {
3961 total = v;
3962 }
3963 function take(bytes calldata data, uint256[] calldata items, uint256 n) public {
3964 total = n;
3965 }
3966}
3967";
3968
3969 fn legacy_offset(needle: &str) -> u64 {
3970 LEGACY_SOURCE.find(needle).expect(needle) as u64
3971 }
3972
3973 fn legacy_contract(code_generator: Option<CodeGenerator>) -> ContractDebugInfo {
3977 let spans: &[(u64, &str)] = &[
3978 (0, "contract L"),
3979 (10, "function outer"),
3982 (11, "uint256 sum"),
3983 (12, "uint256 twice"),
3984 (13, "a * 2"),
3985 (14, "twice > 2"),
3986 (15, "uint256 inner"),
3987 (16, "sum = inner"),
3988 (17, "helper(twice)"),
3989 (18, "sum = twice + helper(twice)"),
3990 (19, "function outer"),
3991 (20, "function helper"),
3993 (21, "uint256 y"),
3994 (22, "r = y"),
3995 (23, "function helper"),
3996 (24, "uint256 r"),
3997 (30, "function guarded"),
4001 (31, "uint256 before"),
4002 (32, "uint256 kept"),
4003 (33, "total = kept"),
4004 (34, "total = before + tag"),
4005 (35, "function guarded"),
4006 (36, "function guarded"),
4007 (40, "function set"),
4010 (41, "total = v"),
4011 (42, "function set"),
4012 (43, "function set"),
4013 (50, "function take"),
4015 (51, "total = n"),
4016 ];
4017 let instructions = spans
4018 .iter()
4019 .map(|(pc, needle)| {
4020 if matches!(pc, 35 | 42) {
4022 instruction_with(
4023 *pc,
4024 legacy_offset(needle),
4025 needle.len() as u64,
4026 json!({"invoke": {}}),
4027 )
4028 } else {
4029 instruction(*pc, legacy_offset(needle), needle.len() as u64)
4030 }
4031 })
4032 .collect();
4033 let info = EthdebugInfo {
4034 compilation: serde_json::Value::Null,
4035 contract_name: "L".to_owned(),
4036 environment: "call".to_owned(),
4037 instructions,
4038 sources: BTreeMap::from([(0, "L.sol".to_owned())]),
4039 variable_locations: BTreeMap::new(),
4040 };
4041 ContractDebugInfo::new(
4042 Some(ROOT),
4043 "L",
4044 info,
4045 BTreeMap::from([(0, LEGACY_SOURCE.to_owned())]),
4046 )
4047 .with_code_generator(code_generator)
4048 }
4049
4050 fn outer_trace() -> TransactionTrace {
4053 let ret = "0x9";
4054 trace(vec![
4055 step(0, 1, "PUSH1", &[]),
4056 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
4057 step(11, 1, "PUSH0", &[ret, "0x5"]),
4058 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
4059 step(13, 1, "MUL", &[ret, "0x5", "0x0", "0x0"]),
4060 step(14, 1, "GT", &[ret, "0x5", "0x0", "0xa"]),
4061 step(15, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
4062 step(16, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0x8"]),
4063 step(18, 1, "DUP2", &[ret, "0x5", "0x8", "0xa"]),
4064 step(17, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
4065 step(
4066 20,
4067 1,
4068 "JUMPDEST",
4069 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"],
4070 ),
4071 step(24, 1, "PUSH0", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
4072 step(
4073 21,
4074 1,
4075 "PUSH0",
4076 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0"],
4077 ),
4078 step(
4079 22,
4080 1,
4081 "DUP1",
4082 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0", "0xb"],
4083 ),
4084 step(23, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0xb", "0x12"]),
4085 step(18, 1, "ADD", &[ret, "0x5", "0x8", "0xa", "0xb"]),
4086 step(19, 1, "JUMP", &[ret, "0x5", "0x15", "0xa"]),
4087 step(0, 1, "STOP", &[]),
4088 ])
4089 }
4090
4091 fn named(variables: &[InferredVariable]) -> Vec<(&str, VariableKind, Option<usize>)> {
4092 variables
4093 .iter()
4094 .map(|variable| (variable.name.as_str(), variable.kind, variable.slot))
4095 .collect()
4096 }
4097
4098 #[test]
4099 fn locals_are_inferred_from_the_legacy_stack_layout() {
4100 let map = StepMap::new(
4101 &outer_trace(),
4102 vec![legacy_contract(Some(CodeGenerator::Legacy))],
4103 );
4104 let trace = outer_trace();
4105
4106 assert_eq!(
4108 map.locals_at(0),
4109 LocalsStatus::Unavailable("no function is executing at this step")
4110 );
4111 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
4115 panic!("{:?}", map.locals_at(1));
4116 };
4117 assert_eq!(named(at_entry), [("sum", VariableKind::Return, None)]);
4118 let LocalsStatus::Inferred(at_return) = map.locals_at(2) else {
4119 panic!("{:?}", map.locals_at(2));
4120 };
4121 assert_eq!(
4122 named(at_return),
4123 [
4124 ("a", VariableKind::Parameter, Some(1)),
4125 ("sum", VariableKind::Return, Some(2))
4126 ]
4127 );
4128 let LocalsStatus::Inferred(at_twice) = map.locals_at(5) else {
4130 panic!("{:?}", map.locals_at(5));
4131 };
4132 assert_eq!(
4133 named(at_twice),
4134 [
4135 ("a", VariableKind::Parameter, Some(1)),
4136 ("sum", VariableKind::Return, Some(2)),
4137 ("twice", VariableKind::Local, Some(3))
4138 ]
4139 );
4140 let values = map.inferred_variables(&trace, 5, None);
4141 assert_eq!(values[0].value.display, "5");
4142 assert_eq!(values[2].value.display, "10");
4143 assert_eq!(values[2].location.offset, 3);
4144 let LocalsStatus::Inferred(in_block) = map.locals_at(7) else {
4146 panic!("{:?}", map.locals_at(7));
4147 };
4148 assert_eq!(
4149 named(in_block).last(),
4150 Some(&("inner", VariableKind::Local, Some(4)))
4151 );
4152 assert_eq!(
4153 map.inferred_variables(&trace, 7, None)[3].value.display,
4154 "8"
4155 );
4156 let LocalsStatus::Inferred(after_block) = map.locals_at(8) else {
4157 panic!("{:?}", map.locals_at(8));
4158 };
4159 assert_eq!(after_block.len(), 3);
4160 assert_eq!(
4161 map.inferred_variables(&trace, 8, None)[1].value.display,
4162 "8"
4163 );
4164
4165 let LocalsStatus::Inferred(helper_entry) = map.locals_at(10) else {
4170 panic!("{:?}", map.locals_at(10));
4171 };
4172 assert_eq!(
4173 named(helper_entry),
4174 [
4175 ("x", VariableKind::Parameter, Some(5)),
4176 ("r", VariableKind::Return, Some(6))
4177 ]
4178 );
4179 let at_entry = map.inferred_variables(&trace, 10, None);
4180 assert_eq!(
4181 at_entry[1].value.status,
4182 crate::DebugValueStatus::Unavailable
4183 );
4184 let helper_values = map.inferred_variables(&trace, 13, None);
4185 assert_eq!(
4186 helper_values
4187 .iter()
4188 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
4189 .collect::<Vec<_>>(),
4190 [("x", "10"), ("r", "0"), ("y", "11")]
4191 );
4192 assert_eq!(helper_values[2].location.offset, 7);
4193 let LocalsStatus::Inferred(back) = map.locals_at(15) else {
4194 panic!("{:?}", map.locals_at(15));
4195 };
4196 assert_eq!(named(back).len(), 3);
4197 assert_eq!(
4198 map.inferred_variables(&trace, 15, None)[2].value.display,
4199 "10"
4200 );
4201 }
4202
4203 #[test]
4204 fn a_modifier_places_itself_and_the_function_it_runs_for() {
4205 let ret = "0x9";
4206 let trace = trace(vec![
4207 step(30, 1, "JUMPDEST", &[ret, "0x7"]),
4208 step(31, 1, "PUSH0", &[ret, "0x7", "0x9"]),
4211 step(32, 1, "PUSH0", &[ret, "0x7", "0x9", "0x3"]),
4213 step(33, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3", "0x7"]),
4214 step(34, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3"]),
4216 ]);
4217 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4218 let LocalsStatus::Inferred(modifier) = map.locals_at(1) else {
4219 panic!("{:?}", map.locals_at(1));
4220 };
4221 assert_eq!(
4222 named(modifier),
4223 [
4224 ("tag", VariableKind::Parameter, Some(2)),
4225 ("before", VariableKind::Local, Some(3))
4226 ]
4227 );
4228 let LocalsStatus::Inferred(body) = map.locals_at(3) else {
4231 panic!("{:?}", map.locals_at(3));
4232 };
4233 assert_eq!(
4234 named(body),
4235 [
4236 ("v", VariableKind::Parameter, Some(1)),
4237 ("kept", VariableKind::Local, Some(4))
4238 ]
4239 );
4240 assert_eq!(
4241 map.inferred_variables(&trace, 3, None)[1].value.display,
4242 "7"
4243 );
4244 let LocalsStatus::Inferred(resumed) = map.locals_at(4) else {
4245 panic!("{:?}", map.locals_at(4));
4246 };
4247 assert_eq!(
4248 named(resumed),
4249 [
4250 ("tag", VariableKind::Parameter, Some(2)),
4251 ("before", VariableKind::Local, Some(3))
4252 ]
4253 );
4254 assert_eq!(
4255 map.inferred_variables(&trace, 4, None)[1].value.display,
4256 "3"
4257 );
4258 }
4259
4260 #[test]
4261 fn calldata_slices_read_the_frames_calldata() {
4262 let ret = "0x9";
4267 let mut trace = trace(vec![
4268 step(
4269 50,
4270 1,
4271 "JUMPDEST",
4272 &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"],
4273 ),
4274 step(51, 1, "PUSH0", &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"]),
4275 ]);
4276 trace.input_data = format!(
4277 "0xaabbccdd{}{}6162",
4278 "0000000000000000000000000000000000000000000000000000000000000003",
4279 "0000000000000000000000000000000000000000000000000000000000000004"
4280 );
4281 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4282 let LocalsStatus::Inferred(layout) = map.locals_at(1) else {
4283 panic!("{:?}", map.locals_at(1));
4284 };
4285 assert_eq!(
4286 layout
4287 .iter()
4288 .map(|variable| (variable.name.as_str(), variable.slot, variable.words))
4289 .collect::<Vec<_>>(),
4290 [
4291 ("data", Some(1), 2),
4292 ("items", Some(3), 2),
4293 ("n", Some(5), 1)
4294 ]
4295 );
4296 let values = map.inferred_variables(&trace, 1, None);
4297 assert_eq!(
4298 values
4299 .iter()
4300 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
4301 .collect::<Vec<_>>(),
4302 [("data", "0x6162"), ("items", "[3, 4]"), ("n", "5")]
4303 );
4304 assert_eq!(values[0].value.raw.as_deref(), Some("0x6162"));
4305 }
4306
4307 #[test]
4308 fn parameters_are_placed_by_the_first_statement_of_a_body_without_locals() {
4309 let trace = trace(vec![
4313 step(40, 1, "JUMPDEST", &["0x9", "0x7"]),
4314 step(41, 1, "DUP1", &["0x9", "0x7"]),
4315 step(41, 1, "SSTORE", &["0x9", "0x7", "0x7", "0x0"]),
4316 ]);
4317 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4318 let LocalsStatus::Inferred(at_entry) = map.locals_at(0) else {
4319 panic!("{:?}", map.locals_at(0));
4320 };
4321 assert!(at_entry.is_empty());
4322 let LocalsStatus::Inferred(in_body) = map.locals_at(2) else {
4323 panic!("{:?}", map.locals_at(2));
4324 };
4325 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(1))]);
4326 assert_eq!(
4327 map.inferred_variables(&trace, 2, None)[0].value.display,
4328 "7"
4329 );
4330 }
4331
4332 #[test]
4333 fn the_dispatchers_jump_to_the_body_places_a_public_function() {
4334 let selector = "0x9c";
4339 let mut trace = trace(vec![
4340 step(0, 1, "PUSH1", &[]),
4341 step(40, 1, "JUMPDEST", &[selector]),
4342 step(42, 1, "JUMP", &[selector, "0x9", "0x7", "0x2b"]),
4343 step(43, 1, "JUMPDEST", &[selector, "0x9", "0x7"]),
4344 step(41, 1, "SSTORE", &[selector, "0x9", "0x7", "0x0"]),
4345 ]);
4346 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4347 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
4348 panic!("{:?}", map.locals_at(1));
4349 };
4350 assert!(at_entry.is_empty());
4351 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
4352 panic!("{:?}", map.locals_at(4));
4353 };
4354 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
4355 assert_eq!(
4356 map.inferred_variables(&trace, 4, None)[0].value.display,
4357 "7"
4358 );
4359
4360 let selector = super::hex_bytes(&function_selector("set(uint256)").expect("selector"));
4364 trace.input_data = format!("0x{selector}{:064x}", 8);
4365 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4366 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
4367 panic!("{:?}", map.locals_at(4));
4368 };
4369 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(3))]);
4370 trace.input_data = format!("0x{selector}{:064x}", 7);
4372 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4373 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
4374 panic!("{:?}", map.locals_at(4));
4375 };
4376 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
4377 }
4378
4379 #[test]
4380 fn a_placed_function_places_its_modifiers() {
4381 let selector = "0x9c";
4387 let ret = "0x9";
4388 let trace = trace(vec![
4389 step(0, 1, "PUSH1", &[]),
4390 step(30, 1, "JUMPDEST", &[selector]),
4391 step(35, 1, "JUMP", &[selector, ret, "0x5", "0x24"]),
4392 step(36, 1, "JUMPDEST", &[selector, ret, "0x5"]),
4393 step(31, 1, "DUP1", &[selector, ret, "0x5", "0x9", "0x9"]),
4394 step(32, 1, "PUSH0", &[selector, ret, "0x5", "0x9", "0x9"]),
4395 step(
4396 33,
4397 1,
4398 "SSTORE",
4399 &[selector, ret, "0x5", "0x9", "0x9", "0x5"],
4400 ),
4401 step(34, 1, "SSTORE", &[selector, ret, "0x5", "0x9", "0x9"]),
4402 ]);
4403 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4404 let LocalsStatus::Inferred(modifier) = map.locals_at(4) else {
4405 panic!("{:?}", map.locals_at(4));
4406 };
4407 assert_eq!(
4408 named(modifier),
4409 [
4410 ("tag", VariableKind::Parameter, Some(3)),
4411 ("before", VariableKind::Local, Some(4))
4412 ]
4413 );
4414 let LocalsStatus::Inferred(body) = map.locals_at(6) else {
4415 panic!("{:?}", map.locals_at(6));
4416 };
4417 assert_eq!(
4418 named(body),
4419 [
4420 ("v", VariableKind::Parameter, Some(2)),
4421 ("kept", VariableKind::Local, Some(5))
4422 ]
4423 );
4424 assert_eq!(
4425 map.inferred_variables(&trace, 6, None)[1].value.display,
4426 "5"
4427 );
4428 let LocalsStatus::Inferred(resumed) = map.locals_at(7) else {
4429 panic!("{:?}", map.locals_at(7));
4430 };
4431 assert_eq!(
4432 named(resumed),
4433 [
4434 ("tag", VariableKind::Parameter, Some(3)),
4435 ("before", VariableKind::Local, Some(4))
4436 ]
4437 );
4438 let values = map.inferred_variables(&trace, 7, None);
4439 assert_eq!(values[0].value.display, "9");
4440 assert_eq!(values[1].value.display, "9");
4441 }
4442
4443 #[test]
4444 fn a_function_reached_by_fallthrough_was_inlined_and_has_no_slots() {
4445 let ret = "0x9";
4449 let trace = trace(vec![
4450 step(0, 1, "PUSH1", &[]),
4451 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
4452 step(11, 1, "PUSH0", &[ret, "0x5"]),
4453 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
4454 step(21, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
4455 step(22, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0xb"]),
4456 step(18, 1, "ADD", &[ret, "0x5", "0x0", "0xa", "0xb"]),
4457 ]);
4458 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4459 assert_eq!(
4460 map.location(4).and_then(|location| location.function_name),
4461 Some("helper".to_owned())
4462 );
4463 assert_eq!(
4464 map.locals_at(4),
4465 LocalsStatus::Unavailable(
4466 "this function was inlined by the optimizer, so its variables have no stack \
4467 slots of their own"
4468 )
4469 );
4470 assert_eq!(
4471 map.location(6).and_then(|location| location.function_name),
4472 Some("outer".to_owned())
4473 );
4474 let LocalsStatus::Inferred(back) = map.locals_at(6) else {
4475 panic!("{:?}", map.locals_at(6));
4476 };
4477 assert_eq!(
4478 named(back),
4479 [
4480 ("a", VariableKind::Parameter, Some(1)),
4481 ("sum", VariableKind::Return, Some(2)),
4482 ("twice", VariableKind::Local, Some(3))
4483 ]
4484 );
4485 }
4486
4487 #[test]
4488 fn locals_are_not_inferred_for_the_via_ir_pipeline_or_an_unknown_one() {
4489 let via_ir = StepMap::new(
4490 &outer_trace(),
4491 vec![legacy_contract(Some(CodeGenerator::ViaIr))],
4492 );
4493 assert!(matches!(
4494 via_ir.locals_at(5),
4495 LocalsStatus::Unavailable(reason) if reason.contains("via-IR pipeline")
4496 ));
4497 assert!(via_ir
4498 .inferred_variables(&outer_trace(), 5, None)
4499 .is_empty());
4500 let unknown = StepMap::new(&outer_trace(), vec![legacy_contract(None)]);
4501 assert!(matches!(
4502 unknown.locals_at(5),
4503 LocalsStatus::Unavailable(reason) if reason.contains("not known")
4504 ));
4505 }
4506
4507 #[test]
4508 fn steps_without_debug_info_have_no_location() {
4509 let map = StepMap::new(&outer_calls_inner(), Vec::new());
4510 assert!(!map.has_source());
4511 assert_eq!(map.line_key(3), None);
4512 assert_eq!(map.frame_depth(3), Some(0));
4513 assert_eq!(map.next_source(3), None);
4515 assert_eq!(map.previous_source(3), None);
4516 assert!(map.resolve_line(None, 3).is_err());
4517 assert_eq!(map.frames(3).len(), 1);
4518 }
4519
4520 #[test]
4521 fn steps_without_source_are_transparent_to_line_stepping() {
4522 let map = StepMap::new(
4524 &trace(vec![
4525 step(0, 1, "PUSH1", &[]),
4526 step(10, 1, "JUMPDEST", &[]),
4527 step(11, 1, "ADD", &[]),
4528 step(99, 1, "MUL", &[]),
4529 step(11, 1, "SWAP1", &[]),
4530 step(14, 1, "POP", &[]),
4531 step(30, 1, "STOP", &[]),
4532 ]),
4533 vec![contract(None)],
4534 );
4535 assert_eq!(map.line_key(3), None);
4536 assert_eq!(map.frame_depth(3), Some(1));
4537 assert_eq!(map.next_source(2), Some(5));
4539 assert_eq!(map.step_into(2), Some(5));
4540 assert!(!map.is_line_start(3));
4541 assert!(!map.is_line_start(4));
4542 assert_eq!(map.previous_source(5), Some(2));
4544 assert_eq!(map.previous_source(4), Some(2));
4545 assert_eq!(map.previous_source(3), Some(2));
4546 assert_eq!(map.previous_source(2), Some(1));
4547 }
4548
4549 #[test]
4550 fn generated_code_belongs_to_the_statement_and_the_epilogue_pops_the_function() {
4551 let map = StepMap::new(
4554 &trace(vec![
4555 step(0, 1, "PUSH1", &[]),
4556 step(10, 1, "JUMPDEST", &[]),
4557 step(11, 1, "ADD", &[]),
4558 step(40, 1, "JUMPDEST", &[]),
4559 step(40, 1, "SSTORE", &[]),
4560 step(11, 1, "SWAP1", &[]),
4561 step(14, 1, "POP", &[]),
4562 step(30, 1, "STOP", &[]),
4563 ]),
4564 vec![contract(None)],
4565 );
4566 let helper = map.location(4).expect("location");
4567 assert!(helper.generated);
4568 assert_eq!(helper.line, 3);
4569 assert_eq!(helper.function_name.as_deref(), Some("outer"));
4570 assert_eq!(map.line_key(4), key(3));
4571 assert_eq!(map.function_id(4).map(|id| id.function), Some(0));
4572 assert!(!map.is_line_start(4));
4573 assert!(!map.is_line_start(5));
4574 assert_eq!(map.frame_depth(4), Some(1));
4576 assert_eq!(map.frame_depth(7), Some(0));
4577 assert_eq!(map.line_key(7), None);
4578 assert!(!map.location(7).expect("location").generated);
4579 assert_eq!(map.next_source(2), Some(6));
4580 assert_eq!(map.finish(2), Some(7));
4581 assert_eq!(map.reverse_finish(4), Some(0));
4582 assert_eq!(map.previous_source(6), Some(2));
4583 }
4584
4585 #[test]
4586 fn a_lone_instruction_on_another_line_belongs_to_the_line_around_it() {
4587 let map = StepMap::new(
4590 &trace(vec![
4591 step(0, 1, "PUSH1", &[]),
4592 step(10, 1, "JUMPDEST", &[]),
4593 step(12, 1, "PUSH2", &[]),
4594 step(10, 1, "SWAP1", &[]),
4595 step(11, 1, "ADD", &[]),
4596 step(14, 1, "POP", &[]),
4597 step(30, 1, "STOP", &[]),
4598 ]),
4599 vec![contract(None)],
4600 );
4601 assert_eq!(map.line_key(2), key(2));
4602 assert_eq!(map.location(2).expect("location").line, 4);
4603 assert!(!map.is_line_start(2));
4604 assert!(!map.is_line_start(3));
4605 assert_eq!(map.next_source(1), Some(4));
4606 assert_eq!(map.previous_source(4), Some(1));
4607 assert_eq!(map.line_key(5), key(5));
4609 assert!(map.is_line_start(5));
4610 }
4611
4612 #[test]
4613 fn a_jump_onto_a_function_entry_is_a_call_even_when_it_is_active() {
4614 let map = StepMap::new(
4618 &trace(vec![
4619 step(0, 1, "PUSH1", &[]),
4620 step(1, 1, "JUMP", &[]),
4621 step(10, 1, "JUMPDEST", &[]),
4622 step(12, 1, "JUMP", &[]),
4623 step(20, 1, "JUMPDEST", &[]),
4624 step(21, 1, "JUMP", &[]),
4625 step(10, 1, "JUMPDEST", &[]),
4626 step(14, 1, "JUMP", &[]),
4627 step(21, 1, "SWAP1", &[]),
4628 step(21, 1, "JUMP", &[]),
4629 step(13, 1, "JUMPDEST", &[]),
4630 step(14, 1, "POP", &[]),
4631 step(30, 1, "STOP", &[]),
4632 ]),
4633 vec![contract(None)],
4634 );
4635 let contract = &map.contracts()[0];
4636 assert_eq!(contract.function_entry_at_pc(10), Some(0));
4637 assert_eq!(contract.function_entry_at_pc(20), Some(1));
4638 assert_eq!(contract.function_entry_at_pc(11), None);
4639 let depths = (0..13)
4640 .map(|step| map.frame_depth(step).expect("depth"))
4641 .collect::<Vec<_>>();
4642 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 1, 1, 0]);
4643 assert!(map.is_frame_entry(6));
4644 let frames = map.frames(6);
4645 assert_eq!(
4646 frames
4647 .iter()
4648 .map(|frame| frame.function_name.as_deref().unwrap_or("-"))
4649 .collect::<Vec<_>>(),
4650 vec!["outer", "inner", "outer", "-"]
4651 );
4652 assert_eq!(map.finish(6), Some(8));
4653 assert_eq!(map.reverse_finish(6), Some(5));
4654 assert_eq!(map.finish(4), Some(10));
4655 }
4656
4657 #[test]
4658 fn a_recursive_call_returns_at_the_address_read_off_the_stack() {
4659 let map = StepMap::new(
4664 &trace(vec![
4665 step(0, 1, "PUSH1", &[]),
4666 step(1, 1, "JUMP", &[]),
4667 step(10, 1, "JUMPDEST", &[]),
4668 step(12, 1, "JUMP", &["0xd", "0x1", "0xa"]),
4669 step(10, 1, "JUMPDEST", &["0x1"]),
4670 step(11, 1, "ADD", &["0x2"]),
4671 step(14, 1, "JUMP", &["0xd"]),
4672 step(13, 1, "JUMPDEST", &[]),
4673 step(14, 1, "POP", &[]),
4674 step(30, 1, "STOP", &[]),
4675 ]),
4676 vec![contract(None)],
4677 );
4678 let depths = (0..10)
4679 .map(|step| map.frame_depth(step).expect("depth"))
4680 .collect::<Vec<_>>();
4681 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 2, 1, 1, 0]);
4682 assert!(map.is_frame_entry(4));
4683 assert_eq!(map.finish(4), Some(7));
4684 assert_eq!(map.frames(5).len(), 3);
4685 assert_eq!(map.frames(7).len(), 2);
4686 assert!(map.contracts()[0].is_jumpdest(13));
4687 assert!(!map.contracts()[0].is_jumpdest(99));
4688 }
4689
4690 #[test]
4691 fn jump_markers_decide_calls_and_returns_without_entry_points() {
4692 let whole = SOURCE.len() as u64;
4697 let info = EthdebugInfo {
4698 compilation: serde_json::Value::Null,
4699 contract_name: "C".to_owned(),
4700 environment: "runtime".to_owned(),
4701 instructions: vec![
4702 instruction(0, 0, whole),
4703 instruction(1, 0, whole),
4704 instruction(10, offset_of("function outer"), 90),
4705 instruction(11, offset_of("uint256 b = a + 1;"), 18),
4706 instruction_with(
4707 12,
4708 offset_of("inner(b);"),
4709 9,
4710 json!({"invoke": {"identifier": "outer"}}),
4711 ),
4712 instruction(13, offset_of("inner(b);"), 9),
4713 instruction_with(14, offset_of("b = 0;"), 6, json!({"return": {}})),
4714 instruction(30, 0, whole),
4715 ],
4716 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4717 variable_locations: BTreeMap::new(),
4718 };
4719 let marked =
4720 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4721 assert_eq!(marked.jump_marker_at_pc(12), JumpMarker::Call);
4722 assert_eq!(marked.jump_marker_at_pc(14), JumpMarker::Return);
4723 assert_eq!(marked.jump_marker_at_pc(13), JumpMarker::None);
4724 let map = StepMap::new(
4725 &trace(vec![
4726 step(0, 1, "PUSH1", &[]),
4727 step(1, 1, "JUMP", &[]),
4728 step(10, 1, "JUMPDEST", &[]),
4729 step(12, 1, "JUMP", &[]),
4730 step(11, 1, "ADD", &[]),
4731 step(14, 1, "JUMP", &[]),
4732 step(13, 1, "JUMPDEST", &[]),
4733 step(14, 1, "POP", &[]),
4734 step(30, 1, "STOP", &[]),
4735 ]),
4736 vec![marked],
4737 );
4738 let depths = (0..9)
4739 .map(|step| map.frame_depth(step).expect("depth"))
4740 .collect::<Vec<_>>();
4741 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 1, 0]);
4742 assert!(!map.is_frame_entry(4));
4743 assert_eq!(map.frames(4).len(), 2);
4744 assert_eq!(map.finish(4), Some(8));
4745 let map = StepMap::new(
4747 &trace(vec![
4748 step(0, 1, "PUSH1", &[]),
4749 step(1, 1, "JUMP", &[]),
4750 step(10, 1, "JUMPDEST", &[]),
4751 step(12, 1, "JUMP", &[]),
4752 step(20, 1, "JUMPDEST", &[]),
4753 step(21, 1, "JUMP", &[]),
4754 step(13, 1, "JUMPDEST", &[]),
4755 step(30, 1, "STOP", &[]),
4756 ]),
4757 vec![contract(None)],
4758 );
4759 assert_eq!(map.frame_depth(4), Some(2));
4760 assert_eq!(map.frame_depth(6), Some(1));
4761 }
4762
4763 #[test]
4764 fn marked_jumps_into_generated_helpers_do_not_pop_the_function() {
4765 let whole = SOURCE.len() as u64;
4769 let info = EthdebugInfo {
4770 compilation: serde_json::Value::Null,
4771 contract_name: "C".to_owned(),
4772 environment: "runtime".to_owned(),
4773 instructions: vec![
4774 instruction(0, 0, whole),
4775 instruction(1, 0, whole),
4776 instruction(10, offset_of("function outer"), 90),
4777 instruction_with(
4778 11,
4779 offset_of("uint256 b = a + 1;"),
4780 18,
4781 json!({"invoke": {}}),
4782 ),
4783 instruction_with(40, 0, whole, json!({"return": {}})),
4784 instruction(14, offset_of("b = 0;"), 6),
4785 instruction(30, 0, whole),
4786 ],
4787 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4788 variable_locations: BTreeMap::new(),
4789 };
4790 let contract =
4791 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4792 let map = StepMap::new(
4793 &trace(vec![
4794 step(0, 1, "PUSH1", &[]),
4795 step(1, 1, "JUMP", &[]),
4796 step(10, 1, "JUMPDEST", &[]),
4797 step(11, 1, "JUMP", &[]),
4798 step(40, 1, "JUMPDEST", &[]),
4799 step(40, 1, "JUMP", &[]),
4800 step(14, 1, "JUMPDEST", &[]),
4801 step(30, 1, "STOP", &[]),
4802 ]),
4803 vec![contract],
4804 );
4805 let depths = (0..8)
4806 .map(|step| map.frame_depth(step).expect("depth"))
4807 .collect::<Vec<_>>();
4808 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 0]);
4809 assert!(map.location(4).expect("location").generated);
4810 assert_eq!(
4811 map.location(6).expect("location").function_name.as_deref(),
4812 Some("outer")
4813 );
4814 assert_eq!(map.next_source(3), Some(6));
4815 assert_eq!(map.finish(3), Some(7));
4816 }
4817
4818 fn random_steps(seed: u64, count: usize) -> Vec<StepInfo> {
4820 let mut state = seed;
4821 let mut next = |bound: u64| {
4822 state = state
4823 .wrapping_mul(6_364_136_223_846_793_005)
4824 .wrapping_add(1_442_695_040_888_963_407);
4825 (state >> 33) % bound
4826 };
4827 (0..count)
4828 .map(|_| {
4829 let line = next(5);
4830 StepInfo {
4831 location: None,
4832 generated: false,
4833 key: (line > 0).then_some(LineKey {
4834 contract: 0,
4835 source_id: 0,
4836 line,
4837 }),
4838 contract: None,
4839 address: None,
4840 storage: None,
4841 evm_depth: 0,
4842 starts_root: next(40) == 0,
4843 frame_depth: next(6) as u32,
4844 frame_entry: false,
4845 line_start: false,
4846 }
4847 })
4848 .collect()
4849 }
4850
4851 #[test]
4852 fn line_passes_match_their_definitions() {
4853 for seed in 0..200 {
4854 let steps = random_steps(seed, 300);
4855
4856 let mut smoothed = steps.clone();
4858 StepMap::smooth_single_step_excursions(&mut smoothed);
4859 let mut expected = steps.clone();
4860 let mut after = vec![None; expected.len()];
4861 let mut lines = NearestLines::default();
4862 for (index, step) in expected.iter().enumerate().rev() {
4863 after[index] = lines.visit(step.frame_depth, step.key);
4864 if step.starts_root {
4865 lines.clear();
4866 }
4867 }
4868 let mut lines = NearestLines::default();
4869 for (step, after) in expected.iter_mut().zip(after) {
4870 if step.starts_root {
4871 lines.clear();
4872 }
4873 let before = lines.visit(step.frame_depth, step.key);
4874 let Some(key) = step.key else {
4875 continue;
4876 };
4877 if before.is_some() && before == after && before != Some(key) {
4878 step.key = before;
4879 }
4880 }
4881 assert_eq!(smoothed, expected, "smoothing, seed {seed}");
4882
4883 let mut map = StepMap::new(&trace(Vec::new()), Vec::new());
4886 map.steps = steps.clone();
4887 map.mark_line_starts();
4888 let mut last = Vec::<Option<LineKey>>::new();
4889 for (index, step) in steps.iter().enumerate() {
4890 if step.starts_root {
4891 last.clear();
4892 }
4893 let depth = step.frame_depth as usize;
4894 if last.len() <= depth {
4895 last.resize(depth + 1, None);
4896 }
4897 let Some(key) = step.key else {
4898 assert!(!map.steps[index].line_start, "seed {seed} step {index}");
4899 continue;
4900 };
4901 assert_eq!(
4902 map.steps[index].line_start,
4903 last[depth] != Some(key),
4904 "line start, seed {seed} step {index}"
4905 );
4906 for entry in last.iter_mut().skip(depth) {
4907 *entry = Some(key);
4908 }
4909 }
4910 }
4911 }
4912
4913 #[test]
4914 fn contracts_sharing_a_source_read_it_once() {
4915 let built = contract(Some("0xabcd"));
4916 let mut cache = SourceCache::default();
4917 let mut through_cache = || {
4918 ContractDebugInfo::with_source_cache(
4919 Some("0xabcd"),
4920 "C",
4921 built.info.clone(),
4922 built.source_contents.clone(),
4923 &mut cache,
4924 )
4925 };
4926 let first = through_cache();
4927 let second = through_cache();
4928 assert_eq!(first, built);
4929 assert_eq!(second, built);
4930 assert_eq!(cache.sources[&0].len(), 1);
4931
4932 let other = ContractDebugInfo::with_source_cache(
4935 None,
4936 "D",
4937 built.info.clone(),
4938 BTreeMap::from([(0, "contract D { function g() public {} }".to_owned())]),
4939 &mut cache,
4940 );
4941 assert_eq!(other.functions[0].name, "g");
4942 assert_eq!(cache.sources[&0].len(), 2);
4943 }
4944
4945 #[test]
4946 fn contract_info_indexes_lines_and_functions() {
4947 let contract = contract(Some("0xABCD"));
4948 assert_eq!(contract.address.as_deref(), Some("0xabcd"));
4949 assert_eq!(contract.line_of(0, 0), Some(1));
4950 assert_eq!(
4951 contract.line_of(0, SOURCE.find("inner(b)").unwrap() as u64),
4952 Some(4)
4953 );
4954 assert_eq!(contract.line_count(0), Some(11));
4955 assert_eq!(contract.line_text(0, 8).map(str::trim), Some("x += 1;"));
4956 assert_eq!(contract.line_text(0, 99), None);
4957 assert_eq!(
4958 contract.function_at_pc(21).map(|f| f.name.as_str()),
4959 Some("inner")
4960 );
4961 assert_eq!(contract.function_at_pc(0), None);
4962 assert_eq!(contract.function_at_pc(999), None);
4963 assert_eq!(contract.functions[0].declaration_line, 2);
4964 assert_eq!(contract.effective_line(0, 3), Some(3));
4965 assert_eq!(contract.effective_line(0, 6), Some(1));
4966 assert_eq!(contract.effective_line(0, 0), None);
4967 assert_eq!(contract.effective_line(0, 12), None);
4968 assert_eq!(contract.effective_line(7, 1), None);
4969
4970 assert_eq!(normalize_address("0XAbC"), "0xabc");
4971 assert_eq!(
4972 address_from_word("0x000000000000000000000000AAAA000000000000000000000000000000000001"),
4973 Some("0xaaaa000000000000000000000000000000000001".to_owned())
4974 );
4975 assert_eq!(address_from_word("0x0"), None);
4976 assert_eq!(address_from_word("zz"), None);
4977 }
4978
4979 #[test]
4981 fn prepared_instruction_facts_match_the_instructions() {
4982 let whole = SOURCE.len() as u64;
4983 let outer = offset_of("function outer");
4984 let statement = offset_of("x += 1;");
4985 let instructions = vec![
4986 Instruction::new(0, "PUSH1", Some(json!({"code": source_range(0, 0, whole)}))),
4987 Instruction::new(
4989 21,
4990 "JUMP",
4991 Some(json!({"code": source_range(0, statement, 7), "invoke": {"identifier": "f"}})),
4992 ),
4993 Instruction::new(
4994 22,
4995 "JUMP",
4996 Some(json!({"code": source_range(0, statement, 7), "return": {}})),
4997 ),
4998 Instruction::new(
4999 23,
5000 "REVERT",
5001 Some(json!({"code": source_range(0, statement, 7), "revert": {}})),
5002 ),
5003 Instruction::new(30, "STOP", None),
5005 Instruction::new(31, "POP", Some(json!({}))),
5006 Instruction::new(
5009 40,
5010 "JUMP",
5011 Some(json!({"pick": [
5012 {"code": source_range(0, statement, 7), "invoke": {}},
5013 {"code": source_range(0, outer, 90)}
5014 ]})),
5015 ),
5016 Instruction::new(
5017 42,
5018 "ADD",
5019 Some(json!({"pick": [
5020 {"code": source_range(0, statement, 7)},
5021 {"code": source_range(0, statement, 7)}
5022 ]})),
5023 ),
5024 Instruction::new(
5026 50,
5027 "ADD",
5028 Some(json!({"code": source_range(7, statement, 7)})),
5029 ),
5030 Instruction::new(60, "ADD", Some(json!({"code": source_range(0, 0, whole)}))),
5032 Instruction::new(
5033 60,
5034 "JUMPDEST",
5035 Some(json!({"code": source_range(0, statement, 7)})),
5036 ),
5037 Instruction::new(
5039 1 << 40,
5040 "JUMPDEST",
5041 Some(json!({"code": source_range(0, statement, 7), "return": {}})),
5042 ),
5043 ];
5044 let info = EthdebugInfo {
5045 compilation: serde_json::Value::Null,
5046 contract_name: "C".to_owned(),
5047 environment: "call".to_owned(),
5048 instructions,
5049 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
5050 variable_locations: BTreeMap::new(),
5051 };
5052 let contract =
5053 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
5054 let line = |pc| contract.facts_at_pc(pc).and_then(|facts| facts.line);
5055 let function = |pc| {
5056 contract
5057 .function_at_pc(pc)
5058 .map(|function| function.name.as_str())
5059 };
5060
5061 assert_eq!(contract.jump_marker_at_pc(21), JumpMarker::Call);
5062 assert_eq!(contract.jump_marker_at_pc(22), JumpMarker::Return);
5063 assert_eq!(contract.jump_marker_at_pc(23), JumpMarker::None);
5064 assert_eq!(contract.jump_marker_at_pc(40), JumpMarker::Call);
5065 assert_eq!(contract.location_at_pc(30), None);
5066 assert_eq!(contract.location_at_pc(31), None);
5067 assert_eq!(contract.location_at_pc(40), None);
5068 assert_eq!(contract.location_at_pc(42), contract.location_at_pc(21));
5069 assert_eq!(line(21), contract.line_of(0, statement));
5070 assert_eq!(function(21), Some("inner"));
5071 assert!(contract.location_at_pc(50).is_some());
5072 assert_eq!(line(50), None);
5073 assert_eq!(function(60), Some("inner"));
5074 assert!(contract.is_jumpdest(60));
5075 assert_eq!(contract.jump_marker_at_pc(1 << 40), JumpMarker::Return);
5076 assert!(contract.pc_index.sparse.contains_key(&(1 << 40)));
5077 for pc in [1, 61, (1 << 40) + 1, u64::MAX] {
5079 assert_eq!(contract.instruction_at_pc(pc), None, "pc {pc}");
5080 assert_eq!(contract.location_at_pc(pc), None, "pc {pc}");
5081 }
5082 }
5083
5084 #[test]
5085 fn function_entries_are_the_earliest_jumpdest_at_each_declaration() {
5086 let source = "\
5089contract D {
5090 function a() public { uint256 x = 1; }
5091 function b() public { uint256 y = 2; }
5092 function c() public { uint256 z = 3; }
5093 function d() public { uint256 w = 4; }
5094}
5095";
5096 let start = |needle: &str| source.find(needle).expect(needle) as u64;
5097 let (a, b, c) = (
5098 start("function a"),
5099 start("function b"),
5100 start("function c"),
5101 );
5102 let instructions = vec![
5103 Instruction::new(3, "JUMP", Some(json!({"code": source_range(0, a, 10)}))),
5105 Instruction::new(
5107 30,
5108 "JUMPDEST",
5109 Some(json!({"code": source_range(0, a, 10)})),
5110 ),
5111 Instruction::new(
5112 10,
5113 "JUMPDEST",
5114 Some(json!({"code": source_range(0, a, 10)})),
5115 ),
5116 Instruction::new(
5117 40,
5118 "JUMPDEST",
5119 Some(json!({"code": source_range(0, b, 10)})),
5120 ),
5121 Instruction::new(50, "JUMP", Some(json!({"code": source_range(0, c, 10)}))),
5124 Instruction::new(
5125 51,
5126 "JUMPDEST",
5127 Some(json!({"code": source_range(0, c + 22, 10)})),
5128 ),
5129 Instruction::new(5, "JUMPDEST", Some(json!({"code": source_range(1, a, 10)}))),
5131 Instruction::new(
5133 20,
5134 "JUMPDEST",
5135 Some(json!({"code": source_range(1, b, 10)})),
5136 ),
5137 Instruction::new(20, "ADD", Some(json!({"code": source_range(1, b, 10)}))),
5138 ];
5139 let info = EthdebugInfo {
5140 compilation: serde_json::Value::Null,
5141 contract_name: "D".to_owned(),
5142 environment: "call".to_owned(),
5143 instructions,
5144 sources: BTreeMap::from([(0, "D.sol".to_owned()), (1, "E.sol".to_owned())]),
5145 variable_locations: BTreeMap::new(),
5146 };
5147 let contract = ContractDebugInfo::new(
5148 None,
5149 "D",
5150 info,
5151 BTreeMap::from([(0, source.to_owned()), (1, source.to_owned())]),
5152 );
5153 let names = contract
5154 .functions
5155 .iter()
5156 .map(|function| (function.source_id, function.name.as_str()))
5157 .collect::<Vec<_>>();
5158 assert_eq!(
5159 names,
5160 [
5161 (0, "a"),
5162 (0, "b"),
5163 (0, "c"),
5164 (0, "d"),
5165 (1, "a"),
5166 (1, "b"),
5167 (1, "c"),
5168 (1, "d")
5169 ]
5170 );
5171 let entries = [3, 5, 10, 20, 30, 40, 50, 51, 999]
5172 .into_iter()
5173 .map(|pc| (pc, contract.function_entry_at_pc(pc)))
5174 .collect::<Vec<_>>();
5175 assert_eq!(
5176 entries,
5177 [
5178 (3, None),
5179 (5, Some(4)),
5180 (10, Some(0)),
5181 (20, Some(5)),
5182 (30, None),
5183 (40, Some(1)),
5184 (50, None),
5185 (51, None),
5186 (999, None)
5187 ]
5188 );
5189 assert!(!contract.is_jumpdest(20));
5190 }
5191
5192 const SECOND_ROOT: &str = "0xbbbb000000000000000000000000000000000002";
5193
5194 fn two_roots() -> TransactionTrace {
5198 let mut trace = outer_calls_inner();
5199 let again = trace.steps.to_steps();
5200 trace.steps.extend(again);
5201 let mut first = call(0, 0, 11, "CALL", ROOT);
5202 first.input = "0x01".to_owned();
5203 let mut second = call(1, 11, 22, "CALL", SECOND_ROOT);
5204 second.input = "0x02".to_owned();
5205 trace.artifacts.calls = vec![first, second];
5206 trace
5207 }
5208
5209 #[test]
5210 fn each_root_call_maps_through_its_own_contract() {
5211 let trace = two_roots();
5212 let map = StepMap::new(
5213 &trace,
5214 vec![contract(Some(ROOT)), contract(Some(SECOND_ROOT))],
5215 );
5216 let address_at = |step: usize| {
5217 map.contract_at_step(step)
5218 .and_then(|contract| contract.address.clone())
5219 };
5220 assert_eq!(address_at(3).as_deref(), Some(ROOT));
5221 assert_eq!(address_at(14).as_deref(), Some(SECOND_ROOT));
5222
5223 let frames = map.frames(17);
5225 assert_eq!(frames.len(), 3);
5226 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
5227 assert!(frames[2].external);
5228 assert_eq!(frames[2].entry_step, 11);
5229 assert_eq!(frames[2].address.as_deref(), Some(SECOND_ROOT));
5230
5231 let next = map.next_source(10).expect("a line in the second call");
5233 assert!(next > 10);
5234 assert_eq!(address_at(next).as_deref(), Some(SECOND_ROOT));
5235
5236 assert_eq!(map.calldata_at(&trace, 3), "0x01");
5237 assert_eq!(map.calldata_at(&trace, 14), "0x02");
5238 }
5239
5240 #[test]
5241 fn finish_from_a_root_frame_stops_where_the_next_root_starts() {
5242 let map = StepMap::new(
5243 &two_roots(),
5244 vec![contract(Some(ROOT)), contract(Some(SECOND_ROOT))],
5245 );
5246 assert_eq!(map.finish(1), Some(11));
5248 assert_eq!(map.finish(10), Some(11));
5249 assert_eq!(map.reverse_finish(12), Some(11));
5251 assert_eq!(map.finish(12), None);
5253 assert_eq!(map.reverse_finish(1), None);
5254 }
5255
5256 #[test]
5257 fn reverse_next_stays_within_a_root() {
5258 let mut trace = outer_calls_inner();
5261 trace.steps.truncate(10);
5262 trace.steps.push(step(10, 1, "JUMP", &[]));
5263 trace.steps.extend(outer_calls_inner().steps.to_steps());
5264 trace.artifacts.calls = vec![call(0, 0, 11, "CALL", ROOT), call(1, 11, 22, "CALL", ROOT)];
5265 let map = StepMap::new(&trace, vec![contract(Some(ROOT))]);
5266 assert_eq!(map.line_key(10), map.line_key(13));
5267 assert_eq!(map.previous_source(14), Some(13));
5268 }
5269
5270 #[test]
5271 fn a_single_contract_describes_only_a_single_root() {
5272 let map = StepMap::new(&two_roots(), vec![contract(None)]);
5273 assert!(map.contract_at_step(3).is_none());
5274 assert!(map.contract_at_step(14).is_none());
5275 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
5276 assert!(map.contract_at_step(3).is_some());
5277 }
5278
5279 #[test]
5280 fn smoothing_stays_within_a_root() {
5281 let mut trace = outer_calls_inner();
5284 trace.steps.truncate(10);
5285 trace.steps.push(step(10, 1, "JUMPDEST", &[]));
5286 trace.steps.push(step(14, 1, "POP", &[]));
5287 trace.steps.push(step(30, 1, "STOP", &[]));
5288 trace.artifacts.calls = vec![call(0, 0, 10, "CALL", ROOT), call(1, 10, 13, "CALL", ROOT)];
5289 let map = StepMap::new(&trace, vec![contract(Some(ROOT))]);
5290 assert_eq!(map.frames(10)[0].function_name.as_deref(), Some("outer"));
5291 assert_ne!(map.line_key(10), map.line_key(9));
5292 assert_eq!(map.line_key(9), map.line_key(11));
5293 }
5294
5295 fn step_info(depth: u32, line: Option<u64>, starts_root: bool) -> StepInfo {
5297 StepInfo {
5298 location: None,
5299 generated: false,
5300 key: line.and_then(key),
5301 contract: None,
5302 address: None,
5303 storage: None,
5304 evm_depth: 0,
5305 starts_root,
5306 frame_depth: depth,
5307 frame_entry: false,
5308 line_start: false,
5309 }
5310 }
5311
5312 #[test]
5313 fn smoothing_looks_past_deeper_frames_only() {
5314 let mut steps = vec![
5315 step_info(1, Some(1), false),
5316 step_info(1, Some(2), false),
5319 step_info(2, Some(3), false),
5320 step_info(3, Some(4), false),
5321 step_info(4, Some(5), false),
5322 step_info(3, Some(6), false),
5323 step_info(1, None, false),
5324 step_info(1, Some(1), false),
5325 step_info(0, Some(7), false),
5327 step_info(1, Some(8), false),
5328 step_info(1, Some(1), false),
5329 step_info(1, Some(9), false),
5331 step_info(0, Some(7), false),
5332 step_info(1, Some(1), false),
5333 step_info(1, Some(2), false),
5335 step_info(1, Some(1), true),
5336 ];
5337 let mut expected = steps
5338 .iter()
5339 .map(|step| step.key.map(|key| key.line))
5340 .collect::<Vec<_>>();
5341 expected[1] = Some(1);
5342 StepMap::smooth_single_step_excursions(&mut steps);
5343 let lines = steps
5344 .iter()
5345 .map(|step| step.key.map(|key| key.line))
5346 .collect::<Vec<_>>();
5347 assert_eq!(lines, expected);
5348 }
5349}