1use std::collections::{BTreeMap, HashMap};
33
34use soldb_core::{TransactionTrace, Word as StackWord};
35use soldb_ethdebug::{
36 function_selector, parse_path, CodeGenerator, EthdebugInfo, FunctionExit, Instruction,
37 PathSegment, SourceLocation, StorageLayout,
38};
39
40use crate::condition::Value;
41use crate::decode::{Place, ValueReader};
42use crate::roots::{root_calls, root_index, RootCall};
43use crate::state::StorageWords;
44use crate::types::SourceTypes;
45use crate::{
46 decode_arguments, is_value_type, parse_source_functions, readable_parameter, ArgumentLayout,
47 ArgumentOrder, ByteRange, DebugLocation, DebugValue, DebugValueStatus, DebugVariable,
48 FrameArgument, FrameState, SourceFunction, SourceParam,
49};
50
51pub const INFERRED_LOCALS_WARNING: &str = "local variables are inferred from the legacy source \
53map and the stack layout of solc's legacy code generator, not from compiler-reported \
54variable locations";
55
56pub const INFERRED_LOCALS_NOTE: &str = "values can be wrong under the optimizer, and a \
58variable whose frame could not be placed shows as unavailable; ETHDebug variable \
59information will replace this once compilers emit it";
60
61const CALL_OPCODES: [&str; 4] = ["CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"];
62
63#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ContractDebugInfo {
68 pub address: Option<String>,
70 pub name: String,
71 pub info: EthdebugInfo,
72 pub source_contents: BTreeMap<u64, String>,
73 pub functions: Vec<SourceFunction>,
74 pub types: SourceTypes,
77 pub storage_layout: Option<StorageLayout>,
80 pub transient_storage_layout: Option<StorageLayout>,
82 pub code_generator: Option<CodeGenerator>,
86 line_starts: BTreeMap<u64, Vec<usize>>,
88 pc_index: HashMap<u64, usize>,
90 function_entries: HashMap<u64, usize>,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum CodeEnvironment {
99 Create,
100 Call,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum JumpMarker {
106 None,
107 Call,
109 Return,
111}
112
113impl ContractDebugInfo {
114 #[must_use]
118 pub fn new(
119 address: Option<&str>,
120 name: &str,
121 info: EthdebugInfo,
122 source_contents: BTreeMap<u64, String>,
123 ) -> Self {
124 let functions = source_contents
125 .iter()
126 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
127 .collect::<Vec<SourceFunction>>();
128 let mut types = SourceTypes::default();
129 for (source_id, source) in &source_contents {
130 types.add_source(*source_id, source);
131 }
132 let line_starts = source_contents
133 .iter()
134 .map(|(source_id, source)| (*source_id, line_starts(source)))
135 .collect();
136 let pc_index = info
137 .instructions
138 .iter()
139 .enumerate()
140 .map(|(index, instruction)| (instruction.offset, index))
141 .collect();
142 let function_entries = functions
143 .iter()
144 .enumerate()
145 .filter_map(|(function_index, function)| {
146 info.instructions
147 .iter()
148 .filter(|instruction| instruction.mnemonic() == Some("JUMPDEST"))
149 .filter(|instruction| {
150 instruction.source_location().is_some_and(|location| {
151 location.source_id == function.source_id
152 && location.offset == function.declaration_start
153 })
154 })
155 .map(|instruction| instruction.offset)
156 .min()
157 .map(|pc| (pc, function_index))
158 })
159 .collect();
160 Self {
161 address: address.map(normalize_address),
162 name: name.to_owned(),
163 info,
164 source_contents,
165 functions,
166 types,
167 storage_layout: None,
168 transient_storage_layout: None,
169 code_generator: None,
170 line_starts,
171 pc_index,
172 function_entries,
173 }
174 }
175
176 #[must_use]
178 pub const fn with_code_generator(mut self, code_generator: Option<CodeGenerator>) -> Self {
179 self.code_generator = code_generator;
180 self
181 }
182
183 #[must_use]
185 pub fn with_storage_layout(mut self, storage_layout: Option<StorageLayout>) -> Self {
186 self.storage_layout = self.with_enum_variants(storage_layout);
187 self
188 }
189
190 #[must_use]
192 pub fn with_transient_storage_layout(mut self, layout: Option<StorageLayout>) -> Self {
193 self.transient_storage_layout = self.with_enum_variants(layout);
194 self
195 }
196
197 fn with_enum_variants(&self, layout: Option<StorageLayout>) -> Option<StorageLayout> {
199 layout.map(|mut layout| {
200 layout.enum_variants = self.types.enum_variants_by_name();
201 layout
202 })
203 }
204
205 fn code_environment(&self) -> Option<CodeEnvironment> {
208 match self.info.environment.as_str() {
209 "create" => Some(CodeEnvironment::Create),
210 "call" => Some(CodeEnvironment::Call),
211 _ => None,
212 }
213 }
214
215 #[must_use]
217 pub fn function_entry_at_pc(&self, pc: u64) -> Option<usize> {
218 self.function_entries.get(&pc).copied()
219 }
220
221 #[must_use]
223 pub fn is_jumpdest(&self, pc: u64) -> bool {
224 self.pc_index
225 .get(&pc)
226 .and_then(|index| self.info.instructions.get(*index))
227 .is_some_and(|instruction| instruction.mnemonic() == Some("JUMPDEST"))
228 }
229
230 #[must_use]
232 pub fn jump_marker_at_pc(&self, pc: u64) -> JumpMarker {
233 let Some(instruction) = self
234 .pc_index
235 .get(&pc)
236 .and_then(|index| self.info.instructions.get(*index))
237 else {
238 return JumpMarker::None;
239 };
240 if !instruction.function_invocations().is_empty() {
241 JumpMarker::Call
242 } else if instruction.function_exit() == Some(FunctionExit::Return) {
243 JumpMarker::Return
244 } else {
245 JumpMarker::None
246 }
247 }
248
249 #[must_use]
250 pub fn source_path(&self, source_id: u64) -> Option<&str> {
251 self.info.sources.get(&source_id).map(String::as_str)
252 }
253
254 #[must_use]
256 pub fn line_of(&self, source_id: u64, offset: u64) -> Option<u64> {
257 let starts = self.line_starts.get(&source_id)?;
258 let offset = usize::try_from(offset).ok()?;
259 Some(starts.partition_point(|start| *start <= offset) as u64)
260 }
261
262 #[must_use]
263 pub fn column_of(&self, source_id: u64, offset: u64) -> Option<u64> {
264 let starts = self.line_starts.get(&source_id)?;
265 let line = self.line_of(source_id, offset)?;
266 let line_start = *starts.get(usize::try_from(line).ok()?.checked_sub(1)?)?;
267 Some(offset.saturating_sub(line_start as u64) + 1)
268 }
269
270 #[must_use]
271 pub fn line_count(&self, source_id: u64) -> Option<u64> {
272 self.line_starts
273 .get(&source_id)
274 .map(|starts| starts.len() as u64)
275 }
276
277 #[must_use]
279 pub fn line_text(&self, source_id: u64, line: u64) -> Option<&str> {
280 let source = self.source_contents.get(&source_id)?;
281 let index = usize::try_from(line).ok()?.checked_sub(1)?;
282 source.lines().nth(index)
283 }
284
285 #[must_use]
287 pub fn location_at_pc(&self, pc: u64) -> Option<SourceLocation> {
288 let index = *self.pc_index.get(&pc)?;
289 self.info.instructions.get(index)?.source_location()
290 }
291
292 #[must_use]
294 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
295 self.info.instructions.get(*self.pc_index.get(&pc)?)
296 }
297
298 #[must_use]
300 pub fn modifier_depth_at_pc(&self, pc: u64) -> Option<i64> {
301 let index = *self.pc_index.get(&pc)?;
302 self.info.instructions.get(index)?.modifier_depth()
303 }
304
305 #[must_use]
307 pub fn function_for_location(&self, location: &SourceLocation) -> Option<usize> {
308 self.functions
309 .iter()
310 .enumerate()
311 .filter(|(_, function)| {
312 function.source_id == location.source_id
313 && function.declaration_start <= location.offset
314 && location.offset <= function.body_end
315 })
316 .min_by_key(|(_, function)| {
317 function.body_end.saturating_sub(function.declaration_start)
318 })
319 .map(|(index, _)| index)
320 }
321
322 #[must_use]
323 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
324 let location = self.location_at_pc(pc)?;
325 self.function_for_location(&location)
326 .and_then(|index| self.functions.get(index))
327 }
328
329 #[must_use]
336 pub fn effective_line(&self, source_id: u64, line: u64) -> Option<u64> {
337 if line == 0 || line > self.line_count(source_id)? {
338 return None;
339 }
340 let mut narrowest = None::<(u64, u64)>;
341 for instruction in &self.info.instructions {
342 let Some(location) = instruction.source_location() else {
343 continue;
344 };
345 if location.source_id != source_id {
346 continue;
347 }
348 let start = self.line_of(source_id, location.offset)?;
349 let end = self.line_of(
350 source_id,
351 location
352 .offset
353 .saturating_add(location.length.saturating_sub(1)),
354 )?;
355 if start > line || line > end {
356 continue;
357 }
358 if start == line {
359 return Some(line);
360 }
361 if narrowest.is_none_or(|(length, _)| location.length < length) {
362 narrowest = Some((location.length, start));
363 }
364 }
365 narrowest.map(|(_, start)| start)
366 }
367}
368
369#[must_use]
372pub fn normalize_address(value: &str) -> String {
373 let hex = value.trim_start_matches("0x").trim_start_matches("0X");
374 format!("0x{}", hex.to_ascii_lowercase())
375}
376
377#[must_use]
381pub fn source_path_matches(source_path: &str, requested: &str) -> bool {
382 fn ends_with_path(longer: &str, shorter: &str) -> bool {
383 longer.ends_with(shorter)
384 && longer
385 .as_bytes()
386 .get(longer.len() - shorter.len() - 1)
387 .is_none_or(|byte| *byte == b'/')
388 }
389 fn file_name(path: &str) -> &str {
390 path.rsplit('/').next().unwrap_or(path)
391 }
392 source_path == requested
393 || ends_with_path(source_path, requested)
394 || ends_with_path(requested, source_path)
395 || (!requested.is_empty() && file_name(source_path) == file_name(requested))
396}
397
398fn line_starts(source: &str) -> Vec<usize> {
399 let mut starts = vec![0];
400 starts.extend(
401 source
402 .bytes()
403 .enumerate()
404 .filter(|(_, byte)| *byte == b'\n')
405 .map(|(index, _)| index + 1),
406 );
407 starts
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
412pub struct LineKey {
413 pub contract: usize,
414 pub source_id: u64,
415 pub line: u64,
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
420pub struct FunctionId {
421 pub contract: usize,
422 pub function: usize,
423}
424
425#[derive(Debug, Clone, PartialEq, Eq)]
427pub struct StepLocation {
428 pub key: LineKey,
429 pub generated: bool,
432 pub contract_name: String,
433 pub path: String,
434 pub offset: u64,
435 pub length: u64,
436 pub line: u64,
437 pub column: u64,
438 pub function: Option<FunctionId>,
439 pub function_name: Option<String>,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct ResolvedLine {
445 pub key: LineKey,
446 pub path: String,
447 pub requested_line: u64,
449}
450
451#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct ResolvedFunction {
454 pub id: FunctionId,
455 pub name: String,
456 pub contract_name: String,
457 pub path: String,
458 pub line: u64,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct Frame {
464 pub depth: u32,
466 pub external: bool,
469 pub address: Option<String>,
470 pub contract_name: Option<String>,
471 pub function_name: Option<String>,
472 pub entry_step: usize,
474 pub step: usize,
477 pub pc: u64,
478 pub location: Option<StepLocation>,
479 pub arguments: Vec<FrameArgument>,
483 pub kind: Option<String>,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct SourceListing {
490 pub path: String,
491 pub current_line: u64,
492 pub lines: Vec<(u64, String)>,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497struct LocationRef {
498 key: LineKey,
499 offset: u64,
500 length: u64,
501 function: Option<usize>,
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505struct StepInfo {
506 location: Option<LocationRef>,
509 generated: bool,
511 key: Option<LineKey>,
514 contract: Option<usize>,
516 address: Option<usize>,
518 storage: Option<usize>,
521 evm_depth: u32,
523 starts_root: bool,
525 frame_depth: u32,
527 frame_entry: bool,
529 line_start: bool,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq)]
536pub struct StepMap {
537 contracts: Vec<ContractDebugInfo>,
538 steps: Vec<StepInfo>,
539 addresses: Vec<String>,
540 pcs: Vec<u64>,
541 reverted: Vec<(usize, usize)>,
544 argument_layouts: Vec<Evidence>,
547 locals_support: Vec<Result<(), &'static str>>,
549 variable_layouts: Vec<Vec<InferredVariable>>,
552 step_variables: Vec<u32>,
554 roots: Vec<RootCall>,
556}
557
558const NO_LAYOUT: u32 = u32::MAX;
559const INLINED_LAYOUT: u32 = u32::MAX - 1;
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564pub enum VariableKind {
565 Parameter,
566 Return,
567 Local,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq)]
572pub struct InferredVariable {
573 pub name: String,
574 pub ty: String,
576 pub kind: VariableKind,
577 pub slot: Option<usize>,
580 pub words: usize,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587pub enum LocalsStatus<'a> {
588 Inferred(&'a [InferredVariable]),
590 Unavailable(&'static str),
592}
593
594#[derive(Debug, Clone, Copy, PartialEq, Eq)]
598enum Evidence {
599 Unknown,
601 Contradicted,
603 TopWords,
605 Ordered(ArgumentOrder),
607}
608
609impl Evidence {
610 fn merge(self, other: Self) -> Self {
613 match (self, other) {
614 (Self::Contradicted, _) | (_, Self::Contradicted) => Self::Contradicted,
615 (Self::Ordered(order), _) | (_, Self::Ordered(order)) => Self::Ordered(order),
616 (Self::TopWords, _) | (_, Self::TopWords) => Self::TopWords,
617 _ => Self::Unknown,
618 }
619 }
620
621 fn layout(self) -> Option<ArgumentLayout> {
622 match self {
623 Self::Ordered(order) => Some(ArgumentLayout::Ordered(order)),
624 Self::TopWords => Some(ArgumentLayout::TopWords),
625 Self::Unknown | Self::Contradicted => None,
626 }
627 }
628}
629
630#[derive(Debug, Clone, Copy)]
632struct FramedStep {
633 evm_depth: u32,
634 frame_id: u32,
635 root: usize,
637 contract: Option<usize>,
638 address: Option<usize>,
639 storage: Option<usize>,
640 location: Option<LocationRef>,
641 entry: Option<usize>,
643 marker: JumpMarker,
645}
646
647struct EvmFrame {
648 id: u32,
649 contract: Option<usize>,
650 address: Option<usize>,
651 storage: Option<usize>,
653 entry_step: usize,
655}
656
657struct FrameEntered {
659 address: Option<String>,
662 environment: CodeEnvironment,
663 delegated: bool,
665}
666
667impl FrameEntered {
668 fn recorded(trace: &TransactionTrace, step: usize) -> Option<Self> {
672 let spans_steps = |entry: Option<usize>, exit: Option<usize>| {
673 entry == Some(step) && exit.is_none_or(|exit| exit > step)
674 };
675 let artifacts = &trace.artifacts;
676 if let Some(call) = artifacts
677 .calls
678 .iter()
679 .find(|call| spans_steps(call.entry_step, call.exit_step))
680 {
681 return Some(Self {
682 address: Some(call.bytecode_address.clone()),
683 environment: CodeEnvironment::Call,
684 delegated: matches!(call.call_type.as_str(), "DELEGATECALL" | "CALLCODE"),
685 });
686 }
687 let creation = artifacts
688 .creations
689 .iter()
690 .find(|creation| spans_steps(creation.entry_step, creation.exit_step))?;
691 Some(Self {
692 address: creation.address.clone(),
693 environment: CodeEnvironment::Create,
694 delegated: false,
695 })
696 }
697
698 fn from_call(trace: &TransactionTrace, caller: Option<usize>) -> Self {
700 let op = caller.map(|caller| &*trace.steps[caller].op);
701 Self {
702 address: caller.and_then(|caller| call_target(&trace.steps[caller])),
703 environment: if matches!(op, Some("CREATE" | "CREATE2")) {
704 CodeEnvironment::Create
705 } else {
706 CodeEnvironment::Call
707 },
708 delegated: matches!(op, Some("DELEGATECALL" | "CALLCODE")),
709 }
710 }
711}
712
713#[derive(Debug, Clone, PartialEq, Eq)]
717struct FrameEntry {
718 function: Option<usize>,
719 return_pc: Option<u64>,
724 variables: Option<FrameVariables>,
727}
728
729#[derive(Debug, Clone, PartialEq, Eq)]
745struct FrameVariables {
746 params_base: Option<usize>,
748 returns: Vec<Option<usize>>,
750 locals_base: Option<usize>,
752 body_seen: bool,
754 layout: Option<u32>,
757 modifier_slots: Option<usize>,
761 inlined: bool,
766}
767
768impl FrameVariables {
769 fn new(
770 function: &SourceFunction,
771 params_base: Option<usize>,
772 modifier_slots: Option<usize>,
773 ) -> Self {
774 Self {
775 params_base,
776 returns: vec![None; function.returns.len()],
777 locals_base: None,
778 body_seen: false,
779 layout: None,
780 modifier_slots,
781 inlined: false,
782 }
783 }
784
785 fn place_from_parameters(&mut self, function: &SourceFunction) {
788 let Some(base) = self.params_base else {
789 return;
790 };
791 let returns_base = base + parameter_slots(&function.params);
792 for (index, slot) in self.returns.iter_mut().enumerate() {
793 slot.get_or_insert(returns_base + index);
794 }
795 if self.locals_base.is_none() {
796 if let Some(modifier_slots) = self.modifier_slots {
797 self.locals_base = Some(returns_base + function.returns.len() + modifier_slots);
798 }
799 }
800 }
801
802 fn track(
805 &mut self,
806 function: &SourceFunction,
807 span: ByteRange,
808 height: usize,
809 layouts: &mut Vec<Vec<InferredVariable>>,
810 ) -> u32 {
811 if self.inlined {
812 self.layout = Some(INLINED_LAYOUT);
813 return INLINED_LAYOUT;
814 }
815 let length = span.end - span.start;
816 let param_slots = parameter_slots(&function.params);
817 self.place_from_parameters(function);
819 let at_beginning = function
824 .placeholder
825 .is_none_or(|placeholder| span.start < placeholder);
826 if !self.body_seen
827 && at_beginning
828 && span.start >= function.body_start
829 && span.start < function.body_end
830 {
831 self.body_seen = true;
832 if self.locals_base.is_none() {
833 self.locals_base = Some(height);
834 }
835 if self.params_base.is_none() && !function.has_modifiers {
836 self.params_base = height.checked_sub(param_slots + function.returns.len());
837 }
838 }
839 for (index, parameter) in function.returns.iter().enumerate() {
842 if self.returns[index].is_none() && parameter.declaration.covers(span.start, length) {
843 self.returns[index] = Some(height);
844 if index == 0 && self.params_base.is_none() {
845 self.params_base = height.checked_sub(param_slots);
846 }
847 }
848 }
849 if self.locals_base.is_none() {
852 if let Some((index, _)) = function
853 .locals
854 .iter()
855 .enumerate()
856 .find(|(_, local)| local.statement.covers(span.start, length))
857 {
858 self.locals_base = height.checked_sub(live_index(function, index, span.start));
859 }
860 }
861 self.place_from_parameters(function);
863
864 let mut layout = Vec::new();
865 if let Some(base) = self.params_base {
866 let mut slot = base;
867 for parameter in &function.params {
868 let size = parameter_stack_slots(parameter);
869 layout.push(InferredVariable {
870 name: parameter.name.clone(),
871 ty: match ¶meter.location {
872 Some(location) => format!("{} {location}", parameter.ty),
873 None => parameter.ty.clone(),
874 },
875 kind: VariableKind::Parameter,
876 slot: Some(slot),
877 words: size,
878 });
879 slot += size;
880 }
881 }
882 for (index, parameter) in function.returns.iter().enumerate() {
883 layout.push(InferredVariable {
884 name: parameter.name.clone(),
885 ty: parameter.declared_type(),
886 kind: VariableKind::Return,
887 slot: self.returns[index],
888 words: 1,
889 });
890 }
891 for (index, local) in function.locals.iter().enumerate() {
892 if local.scope.contains(span.start) {
893 layout.push(InferredVariable {
894 name: local.name.clone(),
895 ty: local.declared_type(),
896 kind: VariableKind::Local,
897 slot: self
898 .locals_base
899 .map(|base| base + live_index(function, index, span.start)),
900 words: 1,
901 });
902 }
903 }
904 let id = match self.layout {
905 Some(id) if layouts[id as usize] == layout => id,
906 _ => {
907 layouts.push(layout);
908 (layouts.len() - 1) as u32
909 }
910 };
911 self.layout = Some(id);
912 id
913 }
914}
915
916fn live_index(function: &SourceFunction, index: usize, offset: u64) -> usize {
919 let declaration = function.locals[index].declaration.start;
920 function
921 .locals
922 .iter()
923 .filter(|other| other.scope.contains(offset) && other.declaration.start < declaration)
924 .count()
925}
926
927pub(crate) fn calldata_for_step<'a>(
931 trace: &'a TransactionTrace,
932 roots: &[RootCall],
933 step: usize,
934) -> &'a str {
935 let root_input = roots
936 .get(root_index(roots, step))
937 .map_or("", |root| root.input(trace));
938 frame_calldata(trace, step, root_input)
939}
940
941fn frame_calldata<'a>(trace: &'a TransactionTrace, step: usize, root_input: &'a str) -> &'a str {
943 let root_depth = trace.steps.first().map_or(0, |first| first.depth);
944 let depth = trace
945 .steps
946 .get(step)
947 .map_or(root_depth, |current| current.depth);
948 if depth == root_depth {
949 return root_input;
950 }
951 trace
952 .artifacts
953 .calls
954 .iter()
955 .filter(|call| {
956 call.entry_step.is_some_and(|entry| entry <= step)
957 && call.exit_step.is_none_or(|exit| step < exit)
958 })
959 .max_by_key(|call| call.depth)
960 .map_or("", |call| call.input.as_str())
961}
962
963fn parameter_stack_slots(parameter: &SourceParam) -> usize {
967 let dynamic =
968 parameter.ty == "bytes" || parameter.ty == "string" || parameter.ty.ends_with("[]");
969 if parameter.location.as_deref() == Some("calldata") && dynamic
970 || parameter.ty.starts_with("function")
971 {
972 2
973 } else {
974 1
975 }
976}
977
978fn parameter_slots(parameters: &[SourceParam]) -> usize {
979 parameters.iter().map(parameter_stack_slots).sum()
980}
981
982fn modifier_slots(
987 contract: &ContractDebugInfo,
988 function: &SourceFunction,
989 count: usize,
990) -> Option<usize> {
991 let mut slots = 0;
992 for name in function.modifiers.iter().take(count) {
993 let modifier = contract.functions.iter().find(|candidate| {
994 candidate.placeholder.is_some()
995 && candidate.name == *name
996 && candidate.source_id == function.source_id
997 });
998 let modifier = modifier.or_else(|| {
999 contract
1000 .functions
1001 .iter()
1002 .find(|candidate| candidate.placeholder.is_some() && candidate.name == *name)
1003 })?;
1004 let placeholder = modifier.placeholder?;
1005 let live = modifier
1006 .locals
1007 .iter()
1008 .filter(|local| {
1009 local.declaration.start < placeholder && local.scope.contains(placeholder)
1010 })
1011 .count();
1012 slots += parameter_slots(&modifier.params) + live;
1013 }
1014 Some(slots)
1015}
1016
1017fn locals_support(contract: &ContractDebugInfo, evidence: Evidence) -> Result<(), &'static str> {
1019 match (contract.code_generator, evidence.layout()) {
1020 (_, Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))) => Err(
1021 "this trace shows the via-IR calling convention, whose stack layout cannot be \
1022 recovered without compiler-reported variable locations",
1023 ),
1024 (Some(CodeGenerator::ViaIr), _) => Err(
1025 "this contract was compiled through the via-IR pipeline, whose stack layout cannot \
1026 be recovered without compiler-reported variable locations",
1027 ),
1028 (Some(CodeGenerator::Legacy), _) | (None, Some(ArgumentLayout::Ordered(_))) => Ok(()),
1029 (None, _) => Err(
1030 "the code generator is not known; only solc's legacy pipeline keeps variables at \
1031 fixed stack slots",
1032 ),
1033 }
1034}
1035
1036#[derive(Default)]
1038struct InternalFrame {
1039 functions: Vec<FrameEntry>,
1041 statement: Option<LocationRef>,
1043 remembered: HashMap<usize, FrameVariables>,
1046}
1047
1048impl InternalFrame {
1049 fn active(&self) -> Option<usize> {
1051 self.functions.iter().rev().find_map(|entry| entry.function)
1052 }
1053
1054 fn is_active(&self, function: usize) -> bool {
1055 self.active() == Some(function)
1056 }
1057
1058 fn push_function(
1059 &mut self,
1060 function: usize,
1061 return_pc: Option<u64>,
1062 variables: Option<FrameVariables>,
1063 ) {
1064 self.functions.push(FrameEntry {
1065 function: Some(function),
1066 return_pc,
1067 variables,
1068 });
1069 }
1070
1071 fn push_placeholder(&mut self) {
1072 self.functions.push(FrameEntry {
1073 function: None,
1074 return_pc: None,
1075 variables: None,
1076 });
1077 }
1078
1079 fn returns_to(&self, pc: u64) -> bool {
1081 self.functions
1082 .last()
1083 .is_some_and(|entry| entry.return_pc == Some(pc))
1084 }
1085}
1086
1087#[allow(clippy::too_many_arguments)]
1096fn place_at_entry_landing(
1097 frame: &mut InternalFrame,
1098 contract: &ContractDebugInfo,
1099 function: usize,
1100 trace: &TransactionTrace,
1101 root_input: &str,
1102 index: usize,
1103 own: LocationRef,
1104 height: usize,
1105 from_call: bool,
1106) {
1107 let Some(declared) = contract.functions.get(function) else {
1108 return;
1109 };
1110 let at_declaration =
1111 contract.is_jumpdest(trace.steps[index].pc) && own.offset == declared.declaration_start;
1112 let from_dispatcher =
1113 !from_call || (index > 0 && framed_declaration(trace, contract, index - 1, declared));
1114 if !at_declaration || !from_dispatcher {
1115 return;
1116 }
1117 let entry = frame
1118 .functions
1119 .iter_mut()
1120 .rev()
1121 .find(|entry| entry.function == Some(function));
1122 let Some(variables) = entry.and_then(|entry| entry.variables.as_mut()) else {
1123 return;
1124 };
1125 if variables.params_base.is_some() || variables.body_seen {
1126 return;
1127 }
1128 let stack = trace.steps[index].snapshot_ref().stack;
1129 let calldata = frame_calldata(trace, index, root_input);
1130 if argument_evidence(declared, calldata, stack) == Evidence::Contradicted {
1131 return;
1132 }
1133 variables.params_base = height.checked_sub(parameter_slots(&declared.params));
1134}
1135
1136fn framed_declaration(
1139 trace: &TransactionTrace,
1140 contract: &ContractDebugInfo,
1141 index: usize,
1142 function: &SourceFunction,
1143) -> bool {
1144 if &*trace.steps[index].op != "JUMP" {
1145 return false;
1146 }
1147 contract
1148 .location_at_pc(trace.steps[index].pc)
1149 .is_some_and(|location| {
1150 location.source_id == function.source_id
1151 && location.offset == function.declaration_start
1152 })
1153}
1154
1155fn modifier_base(
1159 frame: &InternalFrame,
1160 contract: &ContractDebugInfo,
1161 function: usize,
1162) -> Option<usize> {
1163 let modifier = contract.functions.get(function)?;
1164 modifier.placeholder?;
1165 let parent = frame
1166 .functions
1167 .iter()
1168 .rev()
1169 .find(|entry| entry.function.is_some())?;
1170 let parent_function = contract.functions.get(parent.function?)?;
1171 let position = parent_function
1172 .modifiers
1173 .iter()
1174 .position(|name| *name == modifier.name)?;
1175 let parent_base = parent.variables.as_ref()?.params_base?;
1176 let before = modifier_slots(contract, parent_function, position)?;
1177 Some(
1178 parent_base
1179 + parameter_slots(&parent_function.params)
1180 + parent_function.returns.len()
1181 + before,
1182 )
1183}
1184
1185impl StepMap {
1186 #[must_use]
1195 pub fn new(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1196 Self::build(trace, contracts, true)
1197 }
1198
1199 #[must_use]
1204 pub fn for_debug_diff(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1205 Self::build(trace, contracts, false)
1206 }
1207
1208 fn build(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>, smooth: bool) -> Self {
1209 let mut addresses = Vec::<String>::new();
1210 let mut address_index = HashMap::<String, usize>::new();
1211 let mut intern = |address: &str| -> usize {
1212 let address = normalize_address(address);
1213 *address_index.entry(address.clone()).or_insert_with(|| {
1214 addresses.push(address);
1215 addresses.len() - 1
1216 })
1217 };
1218 let roots = root_calls(trace);
1224 let single_root = roots.len() == 1;
1225 let contract_for =
1226 |address: Option<&str>, environment: CodeEnvironment, root: bool| -> Option<usize> {
1227 let address = address.map(normalize_address);
1228 if let Some(address) = &address {
1229 let mut at_address = contracts
1230 .iter()
1231 .enumerate()
1232 .filter(|(_, contract)| contract.address.as_deref() == Some(address));
1233 let exact = at_address.clone().find(|(_, contract)| {
1234 contract
1235 .code_environment()
1236 .is_none_or(|candidate| candidate == environment)
1237 });
1238 if let Some((index, _)) = exact.or_else(|| at_address.next()) {
1239 return Some(index);
1240 }
1241 }
1242 (root && single_root && contracts.len() == 1).then_some(0)
1243 };
1244
1245 let root_depth = trace.steps.first().map_or(0, |step| step.depth);
1247 let mut next_frame_id = 0_u32;
1248 let mut evm_frames = Vec::<EvmFrame>::new();
1249 let mut upcoming_roots = roots.iter().enumerate().peekable();
1250 let mut current_root = 0;
1251 let mut reverted = Vec::new();
1252 let mut framed = Vec::with_capacity(trace.steps.len());
1253 for (index, step) in trace.steps.iter().enumerate() {
1254 let evm_depth = step.depth.saturating_sub(root_depth) as usize;
1255 if let Some((position, root)) =
1256 upcoming_roots.next_if(|(_, root)| root.entry_step == index)
1257 {
1258 current_root = position;
1259 let environment = if root.creation(trace) {
1261 CodeEnvironment::Create
1262 } else {
1263 CodeEnvironment::Call
1264 };
1265 let root_address = root.address(trace);
1266 let address = root_address.map(&mut intern);
1267 evm_frames.clear();
1268 evm_frames.push(EvmFrame {
1269 id: next_frame_id,
1270 contract: contract_for(root_address, environment, true),
1271 address,
1272 storage: address,
1273 entry_step: index,
1274 });
1275 next_frame_id += 1;
1276 }
1277 evm_frames.truncate(evm_depth + 1);
1278 while evm_frames.len() <= evm_depth {
1279 let call = (evm_frames.len() == evm_depth)
1284 .then(|| index.checked_sub(1))
1285 .flatten();
1286 let entered = FrameEntered::recorded(trace, index)
1287 .filter(|_| evm_frames.len() == evm_depth)
1288 .unwrap_or_else(|| FrameEntered::from_call(trace, call));
1289 let address = entered.address.as_deref().map(&mut intern);
1290 let storage = if entered.delegated {
1293 evm_frames.last().and_then(|frame| frame.storage)
1294 } else {
1295 address
1296 };
1297 evm_frames.push(EvmFrame {
1298 id: next_frame_id,
1299 contract: contract_for(entered.address.as_deref(), entered.environment, false),
1300 address,
1301 storage,
1302 entry_step: index,
1303 });
1304 next_frame_id += 1;
1305 }
1306 if &*step.op == "REVERT" || step.error.is_some() {
1307 let entry_step = evm_frames
1308 .last()
1309 .expect("the root frame is never popped")
1310 .entry_step;
1311 reverted.push((entry_step, index));
1312 }
1313 let frame = evm_frames.last().expect("the root frame is never popped");
1314 let location = frame.contract.and_then(|contract_index| {
1315 let contract = &contracts[contract_index];
1316 let location = contract.location_at_pc(step.pc)?;
1317 let line = contract.line_of(location.source_id, location.offset)?;
1318 Some(LocationRef {
1319 key: LineKey {
1320 contract: contract_index,
1321 source_id: location.source_id,
1322 line,
1323 },
1324 offset: location.offset,
1325 length: location.length,
1326 function: contract.function_for_location(&location),
1327 })
1328 });
1329 let contract = frame.contract.map(|index| &contracts[index]);
1330 framed.push(FramedStep {
1331 evm_depth: evm_depth as u32,
1332 frame_id: frame.id,
1333 root: current_root,
1334 contract: frame.contract,
1335 address: frame.address,
1336 storage: frame.storage,
1337 location,
1338 entry: contract.and_then(|contract| contract.function_entry_at_pc(step.pc)),
1339 marker: contract.map_or(JumpMarker::None, |contract| {
1340 contract.jump_marker_at_pc(step.pc)
1341 }),
1342 });
1343 }
1344
1345 let structured = |contract: Option<usize>| {
1349 contract.is_some_and(|index| !contracts[index].functions.is_empty())
1350 };
1351 let mut function_follows = std::collections::HashSet::<u32>::new();
1352 let mut epilogue = vec![false; framed.len()];
1353 for (index, step) in framed.iter().enumerate().rev() {
1354 match step.location.map(|location| location.function) {
1355 Some(Some(_)) => {
1356 function_follows.insert(step.frame_id);
1357 }
1358 Some(None) if structured(step.contract) => {
1359 epilogue[index] = !function_follows.contains(&step.frame_id);
1360 }
1361 _ => {}
1362 }
1363 }
1364
1365 let argument_layouts = prove_argument_layouts(trace, &contracts, &framed);
1366 let locals_support = contracts
1367 .iter()
1368 .zip(&argument_layouts)
1369 .map(|(contract, evidence)| locals_support(contract, *evidence))
1370 .collect::<Vec<_>>();
1371
1372 let mut internal = Vec::<InternalFrame>::new();
1375 let mut steps = Vec::with_capacity(framed.len());
1376 let mut pcs = Vec::with_capacity(framed.len());
1377 let mut variable_layouts = Vec::<Vec<InferredVariable>>::new();
1378 let mut step_variables = Vec::with_capacity(framed.len());
1379 for (index, step) in framed.iter().enumerate() {
1380 let height = trace.steps[index].snapshot_ref().stack.len();
1381 let frame_variables = |function: usize| -> Option<FrameVariables> {
1386 let contract_index = step.contract?;
1387 locals_support[contract_index].ok()?;
1388 let contract = &contracts[contract_index];
1389 let function = contract.functions.get(function)?;
1390 let landed_from_function = index > 0
1391 && framed[index - 1].frame_id == step.frame_id
1392 && &*trace.steps[index - 1].op == "JUMP"
1393 && framed[index - 1]
1394 .location
1395 .is_some_and(|location| location.function.is_some());
1396 let params_base = landed_from_function
1397 .then(|| height.checked_sub(parameter_slots(&function.params)))
1398 .flatten();
1399 Some(FrameVariables::new(
1400 function,
1401 params_base,
1402 modifier_slots(contract, function, function.modifiers.len()),
1403 ))
1404 };
1405 let contract_info = step.contract.map(|index| &contracts[index]);
1406 let evm_depth = step.evm_depth as usize;
1407 let mut frame_entry = false;
1408 let new_root = index > 0 && framed[index - 1].root != step.root;
1410 let root_input = roots[step.root].input(trace);
1411 internal.truncate(if new_root { evm_depth } else { evm_depth + 1 });
1412 while internal.len() <= evm_depth {
1413 internal.push(InternalFrame::default());
1414 frame_entry = true;
1415 }
1416 let frame = internal.last_mut().expect("the root frame is never popped");
1417
1418 let landed_by_jump = index > 0
1421 && framed[index - 1].frame_id == step.frame_id
1422 && &*trace.steps[index - 1].op == "JUMP";
1423 let from_function_code = index > 0
1426 && framed[index - 1].frame_id == step.frame_id
1427 && framed[index - 1].location.is_some_and(|location| {
1428 location.function.is_some()
1429 && location.function != step.location.and_then(|own| own.function)
1430 });
1431 let previous_marker = if landed_by_jump {
1432 framed[index - 1].marker
1433 } else {
1434 JumpMarker::None
1435 };
1436 let structured = structured(step.contract);
1437 let mut generated = false;
1438 let mut pending_call = false;
1442 if landed_by_jump {
1443 let pc = trace.steps[index].pc;
1444 match previous_marker {
1445 JumpMarker::Call => pending_call = true,
1446 JumpMarker::Return => {
1447 frame.functions.pop();
1448 }
1449 JumpMarker::None if frame.returns_to(pc) => {
1450 frame.functions.pop();
1451 }
1452 JumpMarker::None => {}
1453 }
1454 }
1455 let return_address = |function: usize| -> Option<u64> {
1458 if !landed_by_jump {
1459 return None;
1460 }
1461 let contract = &contracts[step.contract?];
1462 let arguments = contract.functions.get(function)?.params.len();
1463 let stack = trace.steps[index - 1].snapshot_ref().stack;
1464 let word = stack.get(stack.len().checked_sub(2 + arguments)?)?;
1465 let pc = u64::from_str_radix(word.trim_start_matches("0x"), 16).ok()?;
1466 contract.is_jumpdest(pc).then_some(pc)
1467 };
1468 let (location, key) = match step.location {
1469 None => {
1470 if pending_call {
1473 frame.push_placeholder();
1474 }
1475 (None, None)
1476 }
1477 Some(own) if !structured => {
1478 frame.statement = Some(own);
1480 (Some(own), Some(own.key))
1481 }
1482 Some(own) => match own.function {
1483 Some(function) => {
1484 let entering = landed_by_jump && step.entry == Some(function);
1485 if landed_by_jump
1489 && previous_marker == JumpMarker::Return
1490 && frame.is_active(function)
1491 {
1492 if let Some(contract) = contract_info {
1493 place_at_entry_landing(
1494 frame, contract, function, trace, root_input, index, own,
1495 height, false,
1496 );
1497 }
1498 }
1499 if entering || (pending_call && !frame.is_active(function)) {
1500 frame.push_function(
1503 function,
1504 return_address(function),
1505 frame_variables(function),
1506 );
1507 frame_entry = true;
1508 } else if pending_call {
1509 if let Some(contract) = contract_info {
1515 place_at_entry_landing(
1516 frame, contract, function, trace, root_input, index, own,
1517 height, true,
1518 );
1519 }
1520 frame.push_placeholder();
1521 } else if !frame.is_active(function) {
1522 match frame
1523 .functions
1524 .iter()
1525 .rposition(|entry| entry.function == Some(function))
1526 {
1527 Some(position) => {
1528 for left in frame.functions.drain(position + 1..) {
1531 if let (Some(function), Some(variables)) =
1532 (left.function, left.variables)
1533 {
1534 frame.remembered.insert(function, variables);
1535 }
1536 }
1537 }
1538 None => {
1539 let mut variables = frame.remembered.remove(&function);
1540 if variables.is_none() {
1541 variables = frame_variables(function);
1542 if let (Some(variables), Some(contract)) =
1543 (variables.as_mut(), contract_info)
1544 {
1545 let declared = &contract.functions[function];
1546 if declared.placeholder.is_some() {
1547 if variables.params_base.is_none() {
1553 variables.params_base =
1554 modifier_base(frame, contract, function);
1555 }
1556 } else if declared.name != "constructor"
1557 && from_function_code
1558 && step.entry != Some(function)
1559 {
1560 variables.inlined = true;
1564 }
1565 }
1566 }
1567 frame.push_function(function, None, variables);
1568 frame_entry = true;
1569 }
1570 }
1571 }
1572 frame.statement = Some(own);
1573 (Some(own), Some(own.key))
1574 }
1575 None if epilogue[index] => {
1576 frame.functions.clear();
1577 frame.statement = None;
1578 (Some(own), None)
1579 }
1580 None => {
1581 if pending_call {
1582 frame.push_placeholder();
1583 }
1584 match frame.statement {
1585 Some(statement) => {
1586 generated = true;
1587 (Some(statement), Some(statement.key))
1588 }
1589 None => (Some(own), None),
1592 }
1593 }
1594 },
1595 };
1596 let layout = {
1599 let frame = internal.last_mut().expect("the root frame is never popped");
1600 let contract = step.contract.map(|index| &contracts[index]);
1601 let active = frame
1602 .functions
1603 .iter()
1604 .rposition(|entry| entry.function.is_some());
1605 match (active, step.location, contract) {
1606 (Some(position), Some(own), Some(contract))
1607 if own.function.is_some()
1608 && own.function == frame.functions[position].function =>
1609 {
1610 let function_index = own.function.unwrap_or_default();
1611 let function = &contract.functions[function_index];
1612 let span = ByteRange {
1613 start: own.offset,
1614 end: own.offset + own.length,
1615 };
1616 let (below, rest) = frame.functions.split_at_mut(position);
1617 let entry = &mut rest[0];
1618 let layout = entry.variables.as_mut().map(|variables| {
1619 variables.track(function, span, height, &mut variable_layouts)
1620 });
1621 let modifier_base = entry
1626 .variables
1627 .as_ref()
1628 .and_then(|variables| variables.params_base);
1629 let parent = below
1630 .iter_mut()
1631 .rev()
1632 .find(|entry| entry.function.is_some());
1633 if let (Some(modifier_base), Some(parent)) = (modifier_base, parent) {
1634 let parent_function =
1635 &contract.functions[parent.function.unwrap_or_default()];
1636 let position = parent_function
1637 .modifiers
1638 .iter()
1639 .position(|name| *name == function.name);
1640 if let (Some(position), Some(variables)) =
1641 (position, parent.variables.as_mut())
1642 {
1643 if variables.params_base.is_none() {
1644 variables.params_base =
1645 modifier_slots(contract, parent_function, position)
1646 .and_then(|before| {
1647 modifier_base.checked_sub(
1648 parameter_slots(&parent_function.params)
1649 + parent_function.returns.len()
1650 + before,
1651 )
1652 });
1653 }
1654 }
1655 }
1656 layout
1657 }
1658 (Some(position), _, _) => frame.functions[position]
1659 .variables
1660 .as_ref()
1661 .and_then(|variables| variables.layout),
1662 _ => None,
1663 }
1664 };
1665 step_variables.push(layout.unwrap_or(NO_LAYOUT));
1666 let internal_total = internal
1667 .iter()
1668 .map(|frame| {
1669 frame
1670 .functions
1671 .iter()
1672 .filter(|entry| entry.function.is_some())
1673 .count()
1674 })
1675 .sum::<usize>();
1676 steps.push(StepInfo {
1677 location,
1678 generated,
1679 key,
1680 contract: step.contract,
1681 address: step.address,
1682 storage: step.storage,
1683 evm_depth: step.evm_depth,
1684 starts_root: new_root,
1685 frame_depth: (evm_depth + internal_total) as u32,
1686 frame_entry,
1687 line_start: false,
1688 });
1689 pcs.push(trace.steps[index].pc);
1690 }
1691
1692 let mut map = Self {
1693 contracts,
1694 steps,
1695 addresses,
1696 pcs,
1697 reverted,
1698 argument_layouts,
1699 locals_support,
1700 variable_layouts,
1701 step_variables,
1702 roots,
1703 };
1704 if smooth {
1705 map.smooth_single_step_excursions();
1706 }
1707 map.mark_line_starts();
1708 map
1709 }
1710
1711 fn smooth_single_step_excursions(&mut self) {
1716 let neighbours =
1717 |steps: &[StepInfo], index: usize, forward: bool| -> Option<Option<LineKey>> {
1718 let depth = steps[index].frame_depth;
1719 let range: Box<dyn Iterator<Item = usize>> = if forward {
1720 Box::new((index + 1)..steps.len())
1721 } else {
1722 Box::new((0..index).rev())
1723 };
1724 for candidate in range {
1725 let boundary = if forward { candidate } else { candidate + 1 };
1727 if steps[boundary].starts_root {
1728 return None;
1729 }
1730 let step = &steps[candidate];
1731 if step.frame_depth > depth {
1732 continue;
1733 }
1734 if step.frame_depth < depth {
1735 return None;
1736 }
1737 if step.key.is_some() {
1738 return Some(step.key);
1739 }
1740 }
1741 None
1742 };
1743 let mut smoothed = Vec::new();
1744 for index in 0..self.steps.len() {
1745 let Some(key) = self.steps[index].key else {
1746 continue;
1747 };
1748 let Some(before) = neighbours(&self.steps, index, false) else {
1749 continue;
1750 };
1751 let Some(after) = neighbours(&self.steps, index, true) else {
1752 continue;
1753 };
1754 if before == after && before.is_some() && before != Some(key) {
1755 smoothed.push((index, before));
1756 }
1757 }
1758 for (index, key) in smoothed {
1759 self.steps[index].key = key;
1760 }
1761 }
1762
1763 fn mark_line_starts(&mut self) {
1766 let mut last_key_at_or_below = Vec::<Option<LineKey>>::new();
1767 for step in &mut self.steps {
1768 if step.starts_root {
1769 last_key_at_or_below.clear();
1770 }
1771 let depth = step.frame_depth as usize;
1772 if last_key_at_or_below.len() <= depth {
1773 last_key_at_or_below.resize(depth + 1, None);
1774 }
1775 let Some(key) = step.key else {
1776 step.line_start = false;
1777 continue;
1778 };
1779 step.line_start = last_key_at_or_below[depth] != Some(key);
1780 for entry in last_key_at_or_below.iter_mut().skip(depth) {
1781 *entry = Some(key);
1782 }
1783 }
1784 }
1785
1786 #[must_use]
1787 pub fn contracts(&self) -> &[ContractDebugInfo] {
1788 &self.contracts
1789 }
1790
1791 #[must_use]
1793 pub fn contract_at_step(&self, step: usize) -> Option<&ContractDebugInfo> {
1794 self.contracts.get(self.steps.get(step)?.contract?)
1795 }
1796
1797 #[must_use]
1798 pub fn step_count(&self) -> usize {
1799 self.steps.len()
1800 }
1801
1802 #[must_use]
1804 pub fn has_source(&self) -> bool {
1805 self.steps.iter().any(|step| step.location.is_some())
1806 }
1807
1808 #[must_use]
1810 pub fn line_key(&self, step: usize) -> Option<LineKey> {
1811 self.steps.get(step)?.key
1812 }
1813
1814 #[must_use]
1815 pub fn frame_depth(&self, step: usize) -> Option<u32> {
1816 self.steps.get(step).map(|step| step.frame_depth)
1817 }
1818
1819 #[must_use]
1820 pub fn is_line_start(&self, step: usize) -> bool {
1821 self.steps.get(step).is_some_and(|step| step.line_start)
1822 }
1823
1824 #[must_use]
1825 pub fn is_frame_entry(&self, step: usize) -> bool {
1826 self.steps.get(step).is_some_and(|step| step.frame_entry)
1827 }
1828
1829 #[must_use]
1831 pub fn function_id(&self, step: usize) -> Option<FunctionId> {
1832 let location = self.steps.get(step)?.location?;
1833 location.function.map(|function| FunctionId {
1834 contract: location.key.contract,
1835 function,
1836 })
1837 }
1838
1839 #[must_use]
1841 pub fn executing_address(&self, step: usize) -> Option<&str> {
1842 let index = self.steps.get(step)?.address?;
1843 self.addresses.get(index).map(String::as_str)
1844 }
1845
1846 #[must_use]
1849 pub fn storage_address(&self, step: usize) -> Option<&str> {
1850 let index = self.storage_context_index(step)?;
1851 self.addresses.get(index).map(String::as_str)
1852 }
1853
1854 #[must_use]
1856 pub fn storage_context_index(&self, step: usize) -> Option<usize> {
1857 self.steps.get(step)?.storage
1858 }
1859
1860 #[must_use]
1863 pub fn reverted_spans(&self) -> &[(usize, usize)] {
1864 &self.reverted
1865 }
1866
1867 #[must_use]
1871 pub fn argument_layout(&self, contract: usize) -> Option<ArgumentLayout> {
1872 self.argument_layouts.get(contract).copied()?.layout()
1873 }
1874
1875 #[must_use]
1885 pub fn locals_at(&self, step: usize) -> LocalsStatus<'_> {
1886 let Some(info) = self.steps.get(step) else {
1887 return LocalsStatus::Unavailable("the step is outside the trace");
1888 };
1889 let Some(contract) = info.contract else {
1890 return LocalsStatus::Unavailable(
1891 "no sources matched the contract executing at this step",
1892 );
1893 };
1894 if let Err(reason) = self.locals_support[contract] {
1895 return LocalsStatus::Unavailable(reason);
1896 }
1897 match self.step_variables.get(step).copied() {
1898 Some(INLINED_LAYOUT) => LocalsStatus::Unavailable(
1899 "this function was inlined by the optimizer, so its variables have no stack \
1900 slots of their own",
1901 ),
1902 Some(id) if id != NO_LAYOUT => {
1903 LocalsStatus::Inferred(&self.variable_layouts[id as usize])
1904 }
1905 _ => LocalsStatus::Unavailable("no function is executing at this step"),
1906 }
1907 }
1908
1909 #[must_use]
1912 pub fn calldata_at<'a>(&self, trace: &'a TransactionTrace, step: usize) -> &'a str {
1913 calldata_for_step(trace, &self.roots, step)
1914 }
1915
1916 pub(crate) fn scope_at_step(&self, step: usize) -> Option<&str> {
1919 let info = self.steps.get(step)?;
1920 let contract = self.contracts.get(info.contract?)?;
1921 let function = contract.functions.get(info.location?.function?)?;
1922 contract
1923 .types
1924 .scope_at(function.source_id, function.declaration_start)
1925 }
1926
1927 #[must_use]
1933 pub fn local_path(
1934 &self,
1935 trace: &TransactionTrace,
1936 step: usize,
1937 words: Option<&StorageWords<'_>>,
1938 path: &str,
1939 ) -> Option<Result<DebugVariable, String>> {
1940 let found = match self.local_place(trace, step, words, path)? {
1941 Ok(found) => found,
1942 Err(reason) => return Some(Err(reason)),
1943 };
1944 let (reader, slot, place) = found;
1945 let (ty, value) = match place {
1946 Ok(place) => (place.ty(), reader.show(&place)),
1947 Err((ty, shown)) => (ty, shown),
1948 };
1949 Some(Ok(DebugVariable {
1950 name: path.trim().to_owned(),
1951 ty,
1952 location: DebugLocation {
1953 kind: "stack".to_owned(),
1954 offset: slot as u64,
1955 },
1956 value,
1957 }))
1958 }
1959
1960 #[must_use]
1963 pub fn local_condition_value(
1964 &self,
1965 trace: &TransactionTrace,
1966 step: usize,
1967 words: Option<&StorageWords<'_>>,
1968 path: &str,
1969 ) -> Option<Result<Value, String>> {
1970 let (reader, _, place) = match self.local_place(trace, step, words, path)? {
1971 Ok(found) => found,
1972 Err(reason) => return Some(Err(reason)),
1973 };
1974 Some(match place {
1975 Ok(place) => reader.condition_value(&place, path.trim()),
1976 Err((_, shown)) => Err(format!("`{}` is {}", path.trim(), shown.display)),
1977 })
1978 }
1979
1980 #[allow(clippy::type_complexity)]
1985 fn local_place<'a>(
1986 &'a self,
1987 trace: &'a TransactionTrace,
1988 step: usize,
1989 words: Option<&'a StorageWords<'a>>,
1990 path: &str,
1991 ) -> Option<Result<(ValueReader<'a>, usize, Result<Place, (String, DebugValue)>), String>> {
1992 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
1993 return None;
1994 };
1995 let segments = parse_path(path).ok()?;
1996 let Some(PathSegment::Name(name)) = segments.first() else {
1997 return None;
1998 };
1999 let variable = layout
2001 .iter()
2002 .rev()
2003 .find(|variable| variable.name == *name)?;
2004 let contract = self.contract_at_step(step)?;
2005 let snapshot = trace
2006 .steps
2007 .get(step)
2008 .map(soldb_core::TraceStep::snapshot_ref);
2009 let stack = snapshot.map_or(&[][..], |snapshot| snapshot.stack);
2010 let reader = ValueReader {
2011 memory: snapshot.and_then(|snapshot| snapshot.memory),
2012 calldata: self.calldata_at(trace, step),
2013 storage: words,
2014 layout: contract.storage_layout.as_ref(),
2015 types: &contract.types,
2016 scope: self.scope_at_step(step),
2017 };
2018 let Some(slot) = variable.slot else {
2019 return Some(Err(format!(
2020 "`{name}` is in scope but its stack slot could not be placed here"
2021 )));
2022 };
2023 let Some(stack_words) = stack.get(slot..slot + variable.words) else {
2024 return Some(Err(format!(
2025 "`{name}` is in scope but its stack slot is above the stack here"
2026 )));
2027 };
2028 let stack_words = stack_words.iter().map(|word| &**word).collect::<Vec<_>>();
2029 let mut place = match reader.root(&variable.ty, &stack_words) {
2030 Ok(place) => place,
2031 Err(shown) if segments.len() == 1 => {
2032 return Some(Ok((reader, slot, Err((variable.ty.clone(), shown)))));
2033 }
2034 Err(shown) => return Some(Err(format!("`{name}` is {}", shown.display))),
2035 };
2036 let mut so_far = name.clone();
2037 for segment in &segments[1..] {
2038 place = match reader.follow(place, segment, &so_far) {
2039 Ok(place) => place,
2040 Err(reason) => return Some(Err(reason)),
2041 };
2042 match segment {
2043 PathSegment::Member(member) => so_far = format!("{so_far}.{member}"),
2044 PathSegment::Index(key) => so_far = format!("{so_far}[{key}]"),
2045 PathSegment::Name(_) => {}
2046 }
2047 }
2048 Some(Ok((reader, slot, Ok(place))))
2049 }
2050
2051 #[must_use]
2059 pub fn inferred_variables(
2060 &self,
2061 trace: &TransactionTrace,
2062 step: usize,
2063 words: Option<&StorageWords<'_>>,
2064 ) -> Vec<DebugVariable> {
2065 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
2066 return Vec::new();
2067 };
2068 let Some(contract) = self.contract_at_step(step) else {
2069 return Vec::new();
2070 };
2071 let snapshot = trace
2072 .steps
2073 .get(step)
2074 .map(soldb_core::TraceStep::snapshot_ref);
2075 let stack = snapshot.map_or(&[][..], |snapshot| snapshot.stack);
2076 let reader = ValueReader {
2077 memory: snapshot.and_then(|snapshot| snapshot.memory),
2078 calldata: self.calldata_at(trace, step),
2079 storage: words,
2080 layout: contract.storage_layout.as_ref(),
2081 types: &contract.types,
2082 scope: self.scope_at_step(step),
2083 };
2084 let unavailable = || DebugValue {
2085 display: "<unavailable>".to_owned(),
2086 raw: None,
2087 status: DebugValueStatus::Unavailable,
2088 };
2089 layout
2090 .iter()
2091 .map(|variable| {
2092 let words = variable
2093 .slot
2094 .and_then(|slot| stack.get(slot..slot + variable.words))
2095 .map(|words| words.iter().map(|word| &**word).collect::<Vec<_>>());
2096 let value = match words {
2097 Some(words) => reader.variable(&variable.ty, &words),
2098 None => unavailable(),
2099 };
2100 DebugVariable {
2101 name: variable.name.clone(),
2102 ty: variable.ty.clone(),
2103 location: DebugLocation {
2104 kind: "stack".to_owned(),
2105 offset: variable.slot.unwrap_or_default() as u64,
2106 },
2107 value,
2108 }
2109 })
2110 .collect()
2111 }
2112
2113 #[must_use]
2121 pub fn frame_arguments(&self, frame: &Frame, state: FrameState<'_>) -> Vec<FrameArgument> {
2122 let Some(info) = self.steps.get(frame.entry_step) else {
2123 return Vec::new();
2124 };
2125 let Some(location) = info.location else {
2126 return Vec::new();
2127 };
2128 let Some(function_index) = location.function else {
2129 return Vec::new();
2130 };
2131 let Some(contract) = self.contracts.get(location.key.contract) else {
2132 return Vec::new();
2133 };
2134 if contract.function_entry_at_pc(self.pcs[frame.entry_step]) != Some(function_index) {
2135 return Vec::new();
2136 }
2137 let Some(function) = contract.functions.get(function_index) else {
2138 return Vec::new();
2139 };
2140 let Some(layout) = self.argument_layout(location.key.contract) else {
2141 return Vec::new();
2142 };
2143 decode_arguments(
2144 &function.params,
2145 state,
2146 layout,
2147 &contract.types,
2148 contract
2149 .types
2150 .scope_at(function.source_id, function.declaration_start),
2151 )
2152 }
2153
2154 #[must_use]
2155 pub fn location(&self, step: usize) -> Option<StepLocation> {
2156 let info = self.steps.get(step)?;
2157 self.describe(&info.location?, info.generated)
2158 }
2159
2160 fn describe(&self, location: &LocationRef, generated: bool) -> Option<StepLocation> {
2161 let contract = self.contracts.get(location.key.contract)?;
2162 let function = location
2163 .function
2164 .and_then(|index| contract.functions.get(index));
2165 Some(StepLocation {
2166 key: location.key,
2167 generated,
2168 contract_name: contract.name.clone(),
2169 path: contract
2170 .source_path(location.key.source_id)
2171 .map(str::to_owned)
2172 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2173 offset: location.offset,
2174 length: location.length,
2175 line: location.key.line,
2176 column: contract
2177 .column_of(location.key.source_id, location.offset)
2178 .unwrap_or(0),
2179 function: location.function.map(|function| FunctionId {
2180 contract: location.key.contract,
2181 function,
2182 }),
2183 function_name: function.map(|function| function.name.clone()),
2184 })
2185 }
2186
2187 #[must_use]
2197 pub fn next_source(&self, step: usize) -> Option<usize> {
2198 let current = *self.steps.get(step)?;
2199 if self.outside_functions(¤t) {
2200 return self.step_into(step);
2201 }
2202 let key = current.key;
2203 ((step + 1)..self.steps.len()).find(|&index| {
2204 let candidate = &self.steps[index];
2205 if candidate.frame_depth > current.frame_depth {
2206 return false;
2207 }
2208 candidate.frame_depth < current.frame_depth
2209 || candidate
2210 .key
2211 .is_some_and(|candidate| Some(candidate) != key)
2212 })
2213 }
2214
2215 #[must_use]
2218 pub fn step_into(&self, step: usize) -> Option<usize> {
2219 let current = *self.steps.get(step)?;
2220 let key = current.key;
2221 ((step + 1)..self.steps.len()).find(|&index| {
2222 let candidate = &self.steps[index];
2223 candidate.frame_depth != current.frame_depth
2224 || candidate
2225 .key
2226 .is_some_and(|candidate| Some(candidate) != key)
2227 })
2228 }
2229
2230 #[must_use]
2233 pub fn finish(&self, step: usize) -> Option<usize> {
2234 let current = *self.steps.get(step)?;
2235 ((step + 1)..self.steps.len()).find(|&index| {
2236 let candidate = &self.steps[index];
2237 candidate.frame_depth < current.frame_depth || candidate.starts_root
2238 })
2239 }
2240
2241 #[must_use]
2246 pub fn previous_source(&self, step: usize) -> Option<usize> {
2247 let current = *self.steps.get(step)?;
2248 if self.outside_functions(¤t) {
2249 return self.reverse_step_into(step);
2250 }
2251 self.previous_line_start(step, true)
2252 }
2253
2254 #[must_use]
2256 pub fn reverse_step_into(&self, step: usize) -> Option<usize> {
2257 self.steps.get(step)?;
2258 self.previous_line_start(step, false)
2259 }
2260
2261 fn previous_line_start(&self, step: usize, skip_deeper: bool) -> Option<usize> {
2262 let current = self.steps[step];
2263 let anchor = if current.key.is_some() {
2265 step
2266 } else {
2267 self.previous_mapped(step, current.frame_depth, skip_deeper)?
2268 };
2269 let start = self.run_start(anchor, skip_deeper);
2270 if start < step {
2271 return Some(start);
2272 }
2273 let previous = self.previous_mapped(start, current.frame_depth, skip_deeper)?;
2274 Some(self.run_start(previous, skip_deeper))
2275 }
2276
2277 #[must_use]
2280 pub fn reverse_finish(&self, step: usize) -> Option<usize> {
2281 let current = *self.steps.get(step)?;
2282 (0..step).rev().find(|&index| {
2283 let candidate = &self.steps[index];
2284 candidate.frame_depth < current.frame_depth || candidate.starts_root
2285 })
2286 }
2287
2288 fn run_start(&self, step: usize, skip_deeper: bool) -> usize {
2292 let info = self.steps[step];
2293 let key = info.key;
2294 let root = root_index(&self.roots, step);
2295 let mut current = step;
2296 while let Some(previous) = self.previous_mapped(current, info.frame_depth, skip_deeper) {
2297 let candidate = self.steps[previous];
2298 if candidate.frame_depth != info.frame_depth || candidate.key != key {
2299 break;
2300 }
2301 if root_index(&self.roots, previous) != root {
2302 break;
2303 }
2304 current = previous;
2305 }
2306 current
2307 }
2308
2309 fn outside_functions(&self, info: &StepInfo) -> bool {
2312 info.location.is_some() && info.key.is_none()
2313 }
2314
2315 fn previous_mapped(&self, step: usize, depth: u32, skip_deeper: bool) -> Option<usize> {
2318 (0..step).rev().find(|&index| {
2319 let candidate = &self.steps[index];
2320 candidate.key.is_some() && (!skip_deeper || candidate.frame_depth <= depth)
2321 })
2322 }
2323
2324 pub fn resolve_line(&self, file: Option<&str>, line: u64) -> Result<Vec<ResolvedLine>, String> {
2327 let mut sources = Vec::<(usize, u64, String)>::new();
2328 for (contract_index, contract) in self.contracts.iter().enumerate() {
2329 for (source_id, path) in &contract.info.sources {
2330 if file.is_none_or(|file| source_path_matches(path, file)) {
2331 sources.push((contract_index, *source_id, path.clone()));
2332 }
2333 }
2334 }
2335 if sources.is_empty() {
2336 return Err(match file {
2337 Some(file) => format!("source file not found: {file}"),
2338 None => "no source files are available".to_owned(),
2339 });
2340 }
2341 if file.is_none() {
2342 let mut paths = sources.iter().map(|(_, _, path)| path).collect::<Vec<_>>();
2343 paths.sort();
2344 paths.dedup();
2345 if paths.len() > 1 {
2346 return Err("line breakpoint is ambiguous; use break <file>:<line>".to_owned());
2347 }
2348 }
2349
2350 let mut resolved = sources
2351 .into_iter()
2352 .filter_map(|(contract_index, source_id, path)| {
2353 let effective = self.contracts[contract_index].effective_line(source_id, line)?;
2354 Some(ResolvedLine {
2355 key: LineKey {
2356 contract: contract_index,
2357 source_id,
2358 line: effective,
2359 },
2360 path,
2361 requested_line: line,
2362 })
2363 })
2364 .collect::<Vec<_>>();
2365 if resolved.is_empty() {
2366 return Err(match file {
2367 Some(file) => format!("no instruction maps to {file}:{line}"),
2368 None => format!("no instruction maps to line {line}"),
2369 });
2370 }
2371 if resolved.iter().any(|candidate| candidate.key.line == line) {
2376 resolved.retain(|candidate| candidate.key.line == line);
2377 }
2378 Ok(resolved)
2379 }
2380
2381 pub fn resolve_function(&self, name: &str) -> Result<Vec<ResolvedFunction>, String> {
2384 let (contract_name, function_name) = match name.rsplit_once('.') {
2385 Some((contract, function)) => (Some(contract), function),
2386 None => (None, name),
2387 };
2388 let resolved = self
2389 .contracts
2390 .iter()
2391 .enumerate()
2392 .filter(|(_, contract)| contract_name.is_none_or(|name| contract.name == name))
2393 .flat_map(|(contract_index, contract)| {
2394 contract
2395 .functions
2396 .iter()
2397 .enumerate()
2398 .filter(move |(_, function)| function.name == function_name)
2399 .map(move |(function_index, function)| ResolvedFunction {
2400 id: FunctionId {
2401 contract: contract_index,
2402 function: function_index,
2403 },
2404 name: function.name.clone(),
2405 contract_name: contract.name.clone(),
2406 path: contract
2407 .source_path(function.source_id)
2408 .map(str::to_owned)
2409 .unwrap_or_else(|| format!("source:{}", function.source_id)),
2410 line: function.declaration_line,
2411 })
2412 })
2413 .collect::<Vec<_>>();
2414 if resolved.is_empty() {
2415 return Err(format!("no function named `{name}` in the loaded sources"));
2416 }
2417 Ok(resolved)
2418 }
2419
2420 #[must_use]
2422 pub fn frames(&self, step: usize) -> Vec<Frame> {
2423 struct Record {
2424 entry_step: usize,
2425 last_step: usize,
2426 external: bool,
2427 }
2428 if step >= self.steps.len() {
2429 return Vec::new();
2430 }
2431 let mut stack = Vec::<Record>::new();
2432 for (index, info) in self.steps.iter().enumerate().take(step + 1) {
2433 let depth = info.frame_depth as usize;
2434 stack.truncate(if info.starts_root { 0 } else { depth + 1 });
2435 let entered_call =
2438 index == 0 || info.starts_root || info.evm_depth > self.steps[index - 1].evm_depth;
2439 let mut first_new = true;
2440 while stack.len() <= depth {
2441 stack.push(Record {
2442 entry_step: index,
2443 last_step: index,
2444 external: first_new && entered_call,
2445 });
2446 first_new = false;
2447 }
2448 if let Some(top) = stack.last_mut() {
2449 top.last_step = index;
2450 }
2451 }
2452
2453 stack
2454 .iter()
2455 .enumerate()
2456 .rev()
2457 .map(|(depth, record)| {
2458 let info = self.steps[record.last_step];
2459 let location = info
2460 .location
2461 .and_then(|location| self.describe(&location, info.generated));
2462 Frame {
2463 depth: depth as u32,
2464 external: record.external,
2465 address: info
2466 .address
2467 .and_then(|index| self.addresses.get(index))
2468 .cloned(),
2469 contract_name: info
2470 .contract
2471 .and_then(|index| self.contracts.get(index))
2472 .map(|contract| contract.name.clone()),
2473 function_name: location
2474 .as_ref()
2475 .and_then(|location| location.function_name.clone()),
2476 entry_step: record.entry_step,
2477 step: record.last_step,
2478 pc: self.pcs[record.last_step],
2479 location,
2480 arguments: Vec::new(),
2481 kind: self.root_kind(depth, record.entry_step),
2482 }
2483 })
2484 .collect()
2485 }
2486
2487 fn root_kind(&self, depth: usize, entry_step: usize) -> Option<String> {
2490 if depth != 0 {
2491 return None;
2492 }
2493 let root = self.roots.get(root_index(&self.roots, entry_step))?;
2494 if root.entry_step != entry_step {
2495 return None;
2496 }
2497 root.kind.clone()
2498 }
2499
2500 #[must_use]
2502 pub fn source_listing(&self, step: usize, radius: u64) -> Option<SourceListing> {
2503 let location = self.steps.get(step)?.location?;
2504 let contract = self.contracts.get(location.key.contract)?;
2505 let count = contract.line_count(location.key.source_id)?;
2506 let first = location.key.line.saturating_sub(radius).max(1);
2507 let last = location.key.line.saturating_add(radius).min(count);
2508 let lines = (first..=last)
2509 .filter_map(|line| {
2510 contract
2511 .line_text(location.key.source_id, line)
2512 .map(|text| (line, text.to_owned()))
2513 })
2514 .collect();
2515 Some(SourceListing {
2516 path: contract
2517 .source_path(location.key.source_id)
2518 .map(str::to_owned)
2519 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2520 current_line: location.key.line,
2521 lines,
2522 })
2523 }
2524}
2525
2526fn prove_argument_layouts(
2541 trace: &TransactionTrace,
2542 contracts: &[ContractDebugInfo],
2543 framed: &[FramedStep],
2544) -> Vec<Evidence> {
2545 let mut evidence = vec![Evidence::Unknown; contracts.len()];
2546 if contracts.is_empty() || framed.is_empty() {
2547 return evidence;
2548 }
2549 let mut calldata_by_entry = HashMap::<usize, &str>::new();
2552 calldata_by_entry.insert(0, trace.input_data.as_str());
2553 for call in &trace.artifacts.calls {
2554 if let Some(entry_step) = call.entry_step {
2555 calldata_by_entry.insert(entry_step, call.input.as_str());
2556 }
2557 }
2558
2559 let mut examined = std::collections::HashSet::<u32>::new();
2560 let mut frame_calldata = HashMap::<u32, &str>::new();
2561 for (index, step) in framed.iter().enumerate() {
2562 let entered = index == 0 || framed[index - 1].frame_id != step.frame_id;
2563 if entered {
2564 if let Some(calldata) = calldata_by_entry.get(&index) {
2565 frame_calldata.insert(step.frame_id, calldata);
2566 }
2567 }
2568 let (Some(contract_index), Some(function_index)) = (step.contract, step.entry) else {
2569 continue;
2570 };
2571 if !examined.insert(step.frame_id) {
2574 continue;
2575 }
2576 let Some(calldata) = frame_calldata.get(&step.frame_id) else {
2577 continue;
2578 };
2579 let Some(function) = contracts[contract_index].functions.get(function_index) else {
2580 continue;
2581 };
2582 let stack = trace.steps[index].snapshot_ref().stack;
2583 let observed = argument_evidence(function, calldata, stack);
2584 evidence[contract_index] = evidence[contract_index].merge(observed);
2585 }
2586 evidence
2587}
2588
2589fn argument_evidence(function: &SourceFunction, calldata: &str, stack: &[StackWord]) -> Evidence {
2597 let count = function.params.len();
2598 if count == 0 || stack.len() < count {
2599 return Evidence::Unknown;
2600 }
2601 if !function.params.iter().all(readable_parameter) {
2603 return Evidence::Unknown;
2604 }
2605 let signature = format!(
2606 "{}({})",
2607 function.name,
2608 function
2609 .params
2610 .iter()
2611 .map(|param| canonical_value_type(¶m.ty))
2612 .collect::<Vec<_>>()
2613 .join(",")
2614 );
2615 let Ok(selector) = function_selector(&signature) else {
2616 return Evidence::Unknown;
2617 };
2618 let data = calldata.trim_start_matches("0x");
2619 let Some((encoded_selector, arguments)) = data.split_at_checked(8) else {
2620 return Evidence::Unknown;
2621 };
2622 if encoded_selector.to_ascii_lowercase() != hex_bytes(&selector) {
2624 return Evidence::Unknown;
2625 }
2626 let expected = (0..count)
2627 .map(|index| {
2628 arguments
2629 .get(index * 64..(index + 1) * 64)
2630 .map(str::to_ascii_lowercase)
2631 })
2632 .collect::<Option<Vec<_>>>();
2633 let Some(expected) = expected else {
2634 return Evidence::Unknown;
2635 };
2636 let checkable = (0..count)
2639 .filter(|index| is_value_type(&function.params[*index].ty))
2640 .collect::<Vec<_>>();
2641 if checkable.is_empty() {
2642 return Evidence::Unknown;
2643 }
2644 let matches = |order: ArgumentOrder| {
2645 checkable.iter().all(|index| {
2646 let candidate = &stack[order.word_index(*index, count, stack.len())];
2647 normalize_word(candidate) == expected[*index]
2648 })
2649 };
2650 let distinguishing = checkable.iter().any(|index| {
2652 ArgumentOrder::FirstOnTop.word_index(*index, count, stack.len())
2653 != ArgumentOrder::LastOnTop.word_index(*index, count, stack.len())
2654 });
2655 let first = matches(ArgumentOrder::FirstOnTop);
2656 let last = matches(ArgumentOrder::LastOnTop);
2657 match (first, last) {
2658 (true, true) => Evidence::TopWords,
2661 (true, false) if distinguishing => Evidence::Ordered(ArgumentOrder::FirstOnTop),
2662 (false, true) if distinguishing => Evidence::Ordered(ArgumentOrder::LastOnTop),
2663 (true, false) | (false, true) => Evidence::TopWords,
2665 (false, false) => Evidence::Contradicted,
2667 }
2668}
2669
2670fn canonical_value_type(ty: &str) -> String {
2673 match ty {
2674 "uint" => "uint256".to_owned(),
2675 "int" => "int256".to_owned(),
2676 "address payable" => "address".to_owned(),
2677 other => other.to_owned(),
2678 }
2679}
2680
2681fn normalize_word(word: &str) -> String {
2683 let digits = word.trim_start_matches("0x").trim_start_matches('0');
2684 format!("{:0>64}", digits.to_ascii_lowercase())
2685}
2686
2687fn hex_bytes(bytes: &[u8]) -> String {
2688 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
2689}
2690
2691#[must_use]
2693pub fn call_target(step: &soldb_core::TraceStep) -> Option<String> {
2694 if !CALL_OPCODES.contains(&&*step.op) {
2695 return None;
2696 }
2697 let stack = step.snapshot_ref().stack;
2698 let word = stack.get(stack.len().checked_sub(2)?)?;
2699 address_from_word(word)
2700}
2701
2702#[must_use]
2705pub fn address_from_word(word: &str) -> Option<String> {
2706 let hex = word.trim_start_matches("0x");
2707 if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
2708 return None;
2709 }
2710 let padded = format!("{hex:0>40}");
2711 let address = padded.get(padded.len() - 40..)?;
2712 if address.bytes().all(|byte| byte == b'0') {
2713 return None;
2714 }
2715 Some(format!("0x{}", address.to_ascii_lowercase()))
2716}
2717
2718#[cfg(test)]
2719mod tests {
2720 use std::collections::BTreeMap;
2721
2722 use serde_json::json;
2723 use soldb_core::{ContractCreation, ExecutionCall, StepSnapshot, TraceStep, TransactionTrace};
2724 use soldb_ethdebug::{function_selector, CodeGenerator, EthdebugInfo, Instruction};
2725
2726 use soldb_core::Word as StackWord;
2727
2728 use crate::FrameState;
2729
2730 use super::{
2731 address_from_word, normalize_address, ArgumentLayout, ArgumentOrder, ContractDebugInfo,
2732 InferredVariable, JumpMarker, LineKey, LocalsStatus, StepMap, VariableKind,
2733 };
2734
2735 const SOURCE: &str = "\
2737contract C {
2738 function outer(uint256 a) public {
2739 uint256 b = a + 1;
2740 inner(b);
2741 b = 0;
2742 }
2743 function inner(uint256 x) internal {
2744 x += 1;
2745 }
2746}
2747";
2748
2749 fn offset_of(needle: &str) -> u64 {
2750 SOURCE.find(needle).expect(needle) as u64
2751 }
2752
2753 fn instruction(pc: u64, offset: u64, length: u64) -> Instruction {
2754 instruction_with(pc, offset, length, json!({}))
2755 }
2756
2757 fn instruction_with(
2759 pc: u64,
2760 offset: u64,
2761 length: u64,
2762 extra: serde_json::Value,
2763 ) -> Instruction {
2764 let mut context =
2765 json!({"code": {"source": {"id": 0}, "range": {"offset": offset, "length": length}}});
2766 for (key, value) in extra.as_object().expect("object") {
2767 context[key] = value.clone();
2768 }
2769 serde_json::from_value(json!({
2770 "offset": pc,
2771 "operation": {"mnemonic": "JUMPDEST"},
2772 "context": context
2773 }))
2774 .expect("instruction")
2775 }
2776
2777 fn contract(address: Option<&str>) -> ContractDebugInfo {
2778 let whole = SOURCE.len() as u64;
2779 let info = EthdebugInfo {
2780 compilation: serde_json::Value::Null,
2781 contract_name: "C".to_owned(),
2782 environment: "runtime".to_owned(),
2783 instructions: vec![
2784 instruction(0, 0, whole),
2786 instruction(1, 0, whole),
2787 instruction(10, offset_of("function outer"), 90),
2789 instruction(11, offset_of("uint256 b = a + 1;"), 18),
2790 instruction(12, offset_of("inner(b);"), 9),
2791 instruction(13, offset_of("inner(b);"), 9),
2792 instruction(14, offset_of("b = 0;"), 6),
2793 instruction(20, offset_of("function inner"), 50),
2795 instruction(21, offset_of("x += 1;"), 7),
2796 instruction(30, 0, whole),
2798 instruction(40, 0, whole),
2800 ],
2801 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
2802 variable_locations: BTreeMap::new(),
2803 };
2804 ContractDebugInfo::new(address, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]))
2805 }
2806
2807 fn step(pc: u64, depth: u64, op: &str, stack: &[&str]) -> TraceStep {
2808 TraceStep {
2809 pc,
2810 op: op.into(),
2811 gas: 0,
2812 gas_cost: 0,
2813 depth,
2814 stack: stack.iter().map(|word| StackWord::from(*word)).collect(),
2815 memory: None,
2816 storage: None,
2817 error: None,
2818 snapshot: StepSnapshot::default(),
2819 }
2820 }
2821
2822 fn trace(steps: Vec<TraceStep>) -> TransactionTrace {
2823 TransactionTrace {
2824 tx_hash: None,
2825 from_addr: "0x1".to_owned(),
2826 to_addr: Some("0xAAaa000000000000000000000000000000000001".to_owned()),
2827 value: "0x0".to_owned(),
2828 input_data: "0x".to_owned(),
2829 gas_used: 0,
2830 output: "0x".to_owned(),
2831 success: true,
2832 error: None,
2833 debug_trace_available: true,
2834 contract_address: None,
2835 backend: None,
2836 capabilities: Default::default(),
2837 artifacts: Default::default(),
2838 steps,
2839 }
2840 }
2841
2842 fn outer_calls_inner() -> TransactionTrace {
2845 trace(vec![
2846 step(0, 1, "PUSH1", &[]),
2847 step(1, 1, "JUMPI", &[]),
2848 step(10, 1, "JUMPDEST", &[]),
2849 step(11, 1, "ADD", &[]),
2850 step(12, 1, "PUSH1", &[]),
2851 step(20, 1, "JUMPDEST", &[]),
2852 step(21, 1, "ADD", &[]),
2853 step(21, 1, "SWAP1", &[]),
2854 step(13, 1, "JUMPDEST", &[]),
2855 step(14, 1, "POP", &[]),
2856 step(30, 1, "STOP", &[]),
2857 ])
2858 }
2859
2860 const PAY_SOURCE: &str = "\
2863contract P {
2864 function pay(address to, uint256 amount) public {
2865 total(to, amount);
2866 }
2867 function total(address to, uint256 amount) internal {
2868 amount += 1;
2869 }
2870}
2871";
2872
2873 fn pay_contract() -> ContractDebugInfo {
2874 let whole = PAY_SOURCE.len() as u64;
2875 let at = |needle: &str| PAY_SOURCE.find(needle).expect(needle) as u64;
2876 let span = |pc: u64, offset: u64, length: u64| {
2877 serde_json::from_value::<Instruction>(json!({
2878 "offset": pc,
2879 "operation": {"mnemonic": "JUMPDEST"},
2880 "context": {"code": {
2881 "source": {"id": 0},
2882 "range": {"offset": offset, "length": length}
2883 }}
2884 }))
2885 .expect("instruction")
2886 };
2887 let info = EthdebugInfo {
2888 compilation: serde_json::Value::Null,
2889 contract_name: "P".to_owned(),
2890 environment: "runtime".to_owned(),
2891 instructions: vec![
2892 span(0, 0, whole),
2893 span(10, at("function pay"), 80),
2894 span(11, at("total(to, amount);"), 18),
2895 span(20, at("function total"), 70),
2896 span(21, at("amount += 1;"), 12),
2897 ],
2898 sources: BTreeMap::from([(0, "P.sol".to_owned())]),
2899 variable_locations: BTreeMap::new(),
2900 };
2901 ContractDebugInfo::new(
2902 None,
2903 "P",
2904 info,
2905 BTreeMap::from([(0, PAY_SOURCE.to_owned())]),
2906 )
2907 }
2908
2909 fn word_of(value: &str) -> StackWord {
2910 StackWord::from(format!("0x{:0>64}", value.trim_start_matches("0x")).as_str())
2911 }
2912
2913 fn pay_trace(entry_stack: &[StackWord], inner_stack: &[StackWord]) -> TransactionTrace {
2916 let selector = soldb_ethdebug::function_selector("pay(address,uint256)").expect("selector");
2917 let calldata = format!(
2918 "0x{}{}{}",
2919 selector
2920 .iter()
2921 .map(|byte| format!("{byte:02x}"))
2922 .collect::<String>(),
2923 &word_of(PAY_TO)[2..],
2924 &word_of(PAY_AMOUNT)[2..]
2925 );
2926 fn borrow(stack: &[StackWord]) -> Vec<&str> {
2927 stack.iter().map(|word| &**word).collect()
2928 }
2929 let mut trace = trace(vec![
2930 step(0, 1, "PUSH1", &[]),
2931 step(10, 1, "JUMPDEST", &borrow(entry_stack)),
2932 step(11, 1, "JUMP", &borrow(inner_stack)),
2933 step(20, 1, "JUMPDEST", &borrow(inner_stack)),
2934 step(21, 1, "ADD", &[]),
2935 ]);
2936 trace.input_data = calldata;
2937 trace
2938 }
2939
2940 const PAY_TO: &str = "00000000000000000000000000000000000000aa";
2941 const PAY_AMOUNT: &str = "7";
2942
2943 #[test]
2944 fn the_trace_proves_which_end_the_first_argument_is_at() {
2945 let to = word_of(PAY_TO);
2946 let amount = word_of(PAY_AMOUNT);
2947 let tag = word_of("2a");
2948
2949 let first_on_top = [tag.clone(), amount.clone(), to.clone()];
2951 let trace = pay_trace(&first_on_top, &first_on_top);
2952 let map = StepMap::new(&trace, vec![pay_contract()]);
2953 assert_eq!(
2954 map.argument_layout(0),
2955 Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))
2956 );
2957 let frames = map.frames(4);
2958 let inner = frames.first().expect("innermost frame");
2959 assert_eq!(inner.function_name.as_deref(), Some("total"));
2960 let arguments = map.frame_arguments(
2961 inner,
2962 FrameState {
2963 stack: &first_on_top,
2964 memory: None,
2965 },
2966 );
2967 assert_eq!(
2968 arguments
2969 .iter()
2970 .map(|argument| format!("{} = {}", argument.name, argument.value.display))
2971 .collect::<Vec<_>>(),
2972 [
2973 "to = 0x00000000000000000000000000000000000000aa".to_owned(),
2974 "amount = 7".to_owned()
2975 ]
2976 );
2977
2978 let last_on_top = [tag.clone(), to.clone(), amount.clone()];
2980 let trace = pay_trace(&last_on_top, &last_on_top);
2981 let map = StepMap::new(&trace, vec![pay_contract()]);
2982 assert_eq!(
2983 map.argument_layout(0),
2984 Some(ArgumentLayout::Ordered(ArgumentOrder::LastOnTop))
2985 );
2986 let frames = map.frames(4);
2987 let arguments = map.frame_arguments(
2988 frames.first().expect("frame"),
2989 FrameState {
2990 stack: &last_on_top,
2991 memory: None,
2992 },
2993 );
2994 assert_eq!(
2995 arguments[0].value.display,
2996 "0x00000000000000000000000000000000000000aa"
2997 );
2998 assert_eq!(arguments[1].value.display, "7");
2999
3000 let wrapper = [tag.clone(), word_of("dead"), word_of("beef")];
3003 let trace = pay_trace(&wrapper, &first_on_top);
3004 let map = StepMap::new(&trace, vec![pay_contract()]);
3005 assert_eq!(map.argument_layout(0), None);
3006 let frames = map.frames(4);
3007 assert!(map
3008 .frame_arguments(
3009 frames.first().expect("frame"),
3010 FrameState {
3011 stack: &first_on_top,
3012 memory: None
3013 }
3014 )
3015 .is_empty());
3016
3017 let mut unknown = pay_trace(&first_on_top, &first_on_top);
3019 unknown.input_data = "0x".to_owned();
3020 let map = StepMap::new(&unknown, vec![pay_contract()]);
3021 assert_eq!(map.argument_layout(0), None);
3022 let frames = map.frames(4);
3023 assert!(map
3024 .frame_arguments(
3025 frames.first().expect("frame"),
3026 FrameState {
3027 stack: &first_on_top,
3028 memory: None
3029 }
3030 )
3031 .is_empty());
3032 }
3033
3034 #[test]
3035 fn one_parameter_is_on_top_under_either_order() {
3036 let mut trace = outer_calls_inner();
3039 let selector = soldb_ethdebug::function_selector("outer(uint256)").expect("selector");
3040 let amount = word_of("5");
3041 trace.input_data = format!(
3042 "0x{}{}",
3043 selector
3044 .iter()
3045 .map(|byte| format!("{byte:02x}"))
3046 .collect::<String>(),
3047 &amount[2..]
3048 );
3049 let stack = [word_of("2a"), amount.clone()];
3050 trace.steps[2].snapshot =
3051 StepSnapshot::new(stack.to_vec(), None, BTreeMap::new(), BTreeMap::new());
3052 let map = StepMap::new(&trace, vec![contract(None)]);
3053 assert_eq!(map.argument_layout(0), Some(ArgumentLayout::TopWords));
3054 let frames = map.frames(3);
3055 let outer = frames
3056 .iter()
3057 .find(|frame| frame.function_name.as_deref() == Some("outer"))
3058 .expect("outer frame");
3059 let arguments = map.frame_arguments(
3060 outer,
3061 FrameState {
3062 stack: &stack,
3063 memory: None,
3064 },
3065 );
3066 assert_eq!(arguments.len(), 1);
3067 assert_eq!(arguments[0].name, "a");
3068 assert_eq!(arguments[0].value.display, "5");
3069
3070 let map = StepMap::new(&trace, vec![pay_contract()]);
3072 assert_eq!(map.argument_layout(0), None);
3073 }
3074
3075 fn key(line: u64) -> Option<LineKey> {
3076 Some(LineKey {
3077 contract: 0,
3078 source_id: 0,
3079 line,
3080 })
3081 }
3082
3083 #[test]
3084 fn maps_steps_to_lines_and_infers_internal_frames() {
3085 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3086 assert!(map.has_source());
3087 assert_eq!(map.line_key(0), None);
3089 assert_eq!(map.location(0).expect("location").line, 1);
3090 assert!(!map.location(0).expect("location").generated);
3091 assert_eq!(map.line_key(2), key(2));
3092 assert_eq!(map.line_key(3), key(3));
3093 assert_eq!(map.line_key(4), key(4));
3094 assert_eq!(map.line_key(5), key(7));
3095 assert_eq!(map.line_key(6), key(8));
3096 assert_eq!(map.line_key(8), key(4));
3097 assert_eq!(map.line_key(10), None);
3098
3099 let depths = (0..11)
3101 .map(|step| map.frame_depth(step).expect("depth"))
3102 .collect::<Vec<_>>();
3103 assert_eq!(depths, vec![0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 0]);
3104 assert!(map.is_frame_entry(2));
3105 assert!(map.is_frame_entry(5));
3106 assert!(!map.is_frame_entry(8));
3107 assert_eq!(
3108 map.location(6).expect("location").function_name.as_deref(),
3109 Some("inner")
3110 );
3111 assert_eq!(map.location(6).expect("location").path, "C.sol");
3112 assert_eq!(map.location(6).expect("location").column, 9);
3113 }
3114
3115 #[test]
3116 fn next_steps_over_calls_and_step_enters_them() {
3117 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3118 assert_eq!(map.next_source(0), Some(2));
3120 assert_eq!(map.next_source(2), Some(3));
3121 assert_eq!(map.next_source(4), Some(9));
3123 assert_eq!(map.step_into(4), Some(5));
3124 assert_eq!(map.step_into(5), Some(6));
3125 assert_eq!(map.next_source(6), Some(8));
3127 assert_eq!(map.finish(6), Some(8));
3128 assert_eq!(map.next_source(9), Some(10));
3130 assert_eq!(map.finish(9), Some(10));
3131 assert_eq!(map.next_source(10), None);
3132 assert_eq!(map.finish(10), None);
3133 }
3134
3135 #[test]
3136 fn reverse_stepping_lands_on_line_starts() {
3137 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3138 assert_eq!(map.previous_source(9), Some(4));
3140 assert_eq!(map.previous_source(7), Some(6));
3142 assert_eq!(map.previous_source(6), Some(5));
3143 assert_eq!(map.reverse_step_into(9), Some(8));
3145 assert_eq!(map.reverse_step_into(8), Some(6));
3146 assert_eq!(map.reverse_finish(6), Some(4));
3148 assert_eq!(map.reverse_finish(0), None);
3149 assert_eq!(map.previous_source(0), None);
3150 assert_eq!(map.previous_source(10), Some(9));
3153 }
3154
3155 #[test]
3156 fn line_starts_skip_returns_into_the_middle_of_a_line() {
3157 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3158 assert!(map.is_line_start(3));
3159 assert!(map.is_line_start(4));
3160 assert!(map.is_line_start(6));
3161 assert!(!map.is_line_start(7));
3162 assert!(!map.is_line_start(8));
3164 assert!(map.is_line_start(9));
3165 }
3166
3167 #[test]
3168 fn resolves_line_and_function_breakpoints() {
3169 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3170 let resolved = map.resolve_line(Some("C.sol"), 4).expect("line 4");
3171 assert_eq!(resolved.len(), 1);
3172 assert_eq!(resolved[0].key, key(4).unwrap());
3173 assert_eq!(resolved[0].path, "C.sol");
3174 assert_eq!(
3176 map.resolve_line(None, 8).expect("line 8")[0].key,
3177 key(8).unwrap()
3178 );
3179 assert_eq!(
3182 map.resolve_line(None, 10).expect("line 10")[0].key,
3183 key(1).unwrap()
3184 );
3185 assert_eq!(
3186 map.resolve_line(None, 99).expect_err("past the end"),
3187 "no instruction maps to line 99"
3188 );
3189 assert_eq!(
3190 map.resolve_line(Some("D.sol"), 1)
3191 .expect_err("unknown file"),
3192 "source file not found: D.sol"
3193 );
3194
3195 let inner = map.resolve_function("inner").expect("inner");
3196 assert_eq!(inner.len(), 1);
3197 assert_eq!(inner[0].line, 7);
3198 assert_eq!(inner[0].contract_name, "C");
3199 assert_eq!(
3200 map.resolve_function("C.outer").expect("qualified")[0].line,
3201 2
3202 );
3203 assert!(map.resolve_function("D.outer").is_err());
3204 assert!(map.resolve_function("missing").is_err());
3205 }
3206
3207 #[test]
3208 fn frames_list_the_call_structure_innermost_first() {
3209 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3210 let frames = map.frames(6);
3211 assert_eq!(frames.len(), 3);
3212 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3213 assert_eq!(frames[0].depth, 2);
3214 assert!(!frames[0].external);
3215 assert_eq!(frames[0].step, 6);
3216 assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
3217 assert_eq!(
3218 frames[1].location.as_ref().map(|location| location.line),
3219 Some(4)
3220 );
3221 assert_eq!(frames[1].step, 4);
3222 assert!(frames[2].external);
3223 assert_eq!(frames[2].depth, 0);
3224 assert_eq!(
3225 frames[2].address.as_deref(),
3226 Some("0xaaaa000000000000000000000000000000000001")
3227 );
3228 assert!(map.frames(99).is_empty());
3229
3230 let listing = map.source_listing(6, 1).expect("listing");
3231 assert_eq!(listing.current_line, 8);
3232 assert_eq!(listing.lines.len(), 3);
3233 assert_eq!(listing.lines[1].0, 8);
3234 assert_eq!(listing.lines[1].1.trim(), "x += 1;");
3235 }
3236
3237 #[test]
3238 fn external_calls_map_through_the_callee_contract() {
3239 let callee = "0xbbbb000000000000000000000000000000000002";
3241 let word = format!("0x{:0>64}", callee.trim_start_matches("0x"));
3242 let steps = vec![
3243 step(0, 1, "PUSH1", &[]),
3244 step(
3245 12,
3246 1,
3247 "CALL",
3248 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3249 ),
3250 step(21, 2, "JUMPDEST", &[]),
3251 step(21, 2, "ADD", &[]),
3252 step(13, 1, "JUMPDEST", &[]),
3253 step(30, 1, "STOP", &[]),
3254 ];
3255 let root = contract(Some("0xAAAA000000000000000000000000000000000001"));
3256 let map = StepMap::new(&trace(steps), vec![root, contract(Some(callee))]);
3257 assert_eq!(
3258 map.executing_address(1),
3259 Some("0xaaaa000000000000000000000000000000000001")
3260 );
3261 assert_eq!(map.executing_address(2), Some(callee));
3262 assert_eq!(
3263 map.line_key(2),
3264 Some(LineKey {
3265 contract: 1,
3266 source_id: 0,
3267 line: 8
3268 })
3269 );
3270 assert_eq!(map.frame_depth(1), Some(1));
3271 assert_eq!(map.frame_depth(2), Some(3));
3273 assert!(map.is_frame_entry(2));
3274 assert_eq!(map.frame_depth(4), Some(1));
3275 assert_eq!(map.next_source(1), Some(5));
3277 assert_eq!(map.step_into(1), Some(2));
3278 let frames = map.frames(2);
3279 assert_eq!(frames.len(), 4);
3280 assert!(frames[1].external);
3281 assert_eq!(frames[1].address.as_deref(), Some(callee));
3282 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3283 }
3284
3285 fn program(address: &str, environment: &str, spans: &[(u64, &str)]) -> ContractDebugInfo {
3288 let info = EthdebugInfo {
3289 compilation: serde_json::Value::Null,
3290 contract_name: "C".to_owned(),
3291 environment: environment.to_owned(),
3292 instructions: spans
3293 .iter()
3294 .map(|(pc, needle)| instruction(*pc, offset_of(needle), needle.len() as u64))
3295 .collect(),
3296 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
3297 variable_locations: BTreeMap::new(),
3298 };
3299 ContractDebugInfo::new(
3300 Some(address),
3301 "C",
3302 info,
3303 BTreeMap::from([(0, SOURCE.to_owned())]),
3304 )
3305 }
3306
3307 const ROOT: &str = "0xaaaa000000000000000000000000000000000001";
3308 const CREATED: &str = "0xcccc000000000000000000000000000000000003";
3309
3310 fn creation(entry_step: usize, exit_step: usize, address: Option<&str>) -> ContractCreation {
3311 ContractCreation {
3312 id: 0,
3313 parent_id: None,
3314 depth: 2,
3315 entry_step: Some(entry_step),
3316 exit_step: Some(exit_step),
3317 create_type: "CREATE".to_owned(),
3318 caller: ROOT.to_owned(),
3319 address: address.map(str::to_owned),
3320 value: "0x0".to_owned(),
3321 init_code: "0x".to_owned(),
3322 gas_limit: 0,
3323 gas_used: None,
3324 output: None,
3325 success: Some(address.is_some()),
3326 error: None,
3327 }
3328 }
3329
3330 fn call(
3331 id: usize,
3332 entry_step: usize,
3333 exit_step: usize,
3334 call_type: &str,
3335 bytecode_address: &str,
3336 ) -> ExecutionCall {
3337 ExecutionCall {
3338 id,
3339 parent_id: None,
3340 depth: 2,
3341 entry_step: Some(entry_step),
3342 exit_step: Some(exit_step),
3343 call_type: call_type.to_owned(),
3344 from: ROOT.to_owned(),
3345 to: ROOT.to_owned(),
3346 bytecode_address: bytecode_address.to_owned(),
3347 value: "0x0".to_owned(),
3348 input: "0x".to_owned(),
3349 gas_limit: 0,
3350 gas_used: None,
3351 output: None,
3352 success: Some(true),
3353 error: None,
3354 }
3355 }
3356
3357 fn root_creates_then_calls() -> TransactionTrace {
3360 let word = format!("0x{:0>64}", CREATED.trim_start_matches("0x"));
3361 let mut trace = trace(vec![
3362 step(0, 1, "PUSH1", &[]),
3363 step(12, 1, "CREATE", &["0x0", "0x0", "0x0"]),
3365 step(21, 2, "JUMPDEST", &[]),
3366 step(21, 2, "RETURN", &[]),
3367 step(13, 1, "JUMPDEST", &[]),
3368 step(
3369 14,
3370 1,
3371 "CALL",
3372 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3373 ),
3374 step(21, 2, "JUMPDEST", &[]),
3375 step(30, 1, "STOP", &[]),
3376 ]);
3377 trace
3378 .artifacts
3379 .creations
3380 .push(creation(2, 4, Some(CREATED)));
3381 trace.artifacts.calls.push(call(0, 6, 7, "CALL", CREATED));
3382 trace
3383 }
3384
3385 #[test]
3386 fn creations_map_through_the_created_contracts_creation_program() {
3387 let trace = root_creates_then_calls();
3388 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3392 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3393 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed, creation]);
3394
3395 assert_eq!(map.executing_address(2), Some(CREATED));
3396 assert_eq!(map.storage_address(2), Some(CREATED));
3397 assert_eq!(
3398 map.line_key(2).map(|key| (key.contract, key.line)),
3399 Some((2, 8))
3400 );
3401 assert_eq!(
3402 map.line_key(6).map(|key| (key.contract, key.line)),
3403 Some((1, 5))
3404 );
3405 let frames = map.frames(2);
3407 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3408 assert!(frames[1].external);
3409 assert_eq!(frames[1].address.as_deref(), Some(CREATED));
3410 assert_eq!(map.frame_depth(4), Some(1));
3411 }
3412
3413 #[test]
3414 fn a_line_resolves_only_in_the_programs_that_map_it() {
3415 let trace = root_creates_then_calls();
3419 let whole = SOURCE.len() as u64;
3420 let mut deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3421 deployed.info.instructions.push(instruction(20, 0, whole));
3422 let mut creation = program(CREATED, "create", &[(21, "x += 1;")]);
3423 creation.info.instructions.push(instruction(20, 0, whole));
3424 let map = StepMap::new(&trace, vec![deployed, creation]);
3425 let resolved = map.resolve_line(Some("C.sol"), 8).expect("line 8");
3426 assert_eq!(resolved.len(), 1);
3427 assert_eq!(resolved[0].key.contract, 1);
3428 assert_eq!(resolved[0].key.line, 8);
3429 let resolved = map.resolve_line(Some("C.sol"), 10).expect("line 10");
3431 assert_eq!(resolved.len(), 2);
3432 assert!(resolved.iter().all(|line| line.key.line == 1));
3433 }
3434
3435 #[test]
3436 fn a_creation_uses_the_only_program_at_its_address() {
3437 let trace = root_creates_then_calls();
3440 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3441 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed]);
3442 assert_eq!(
3443 map.line_key(2).map(|key| (key.contract, key.line)),
3444 Some((1, 5))
3445 );
3446 }
3447
3448 #[test]
3449 fn a_creation_the_backend_did_not_record_has_no_contract() {
3450 let mut trace = root_creates_then_calls();
3451 trace.artifacts.creations.clear();
3452 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3453 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), creation]);
3454 assert_eq!(map.executing_address(2), None);
3455 assert_eq!(map.line_key(2), None);
3456 assert_eq!(map.executing_address(6), Some(CREATED));
3458 }
3459
3460 #[test]
3461 fn a_deployment_root_prefers_the_creation_program() {
3462 let mut trace = trace(vec![step(21, 1, "JUMPDEST", &[]), step(30, 1, "STOP", &[])]);
3463 trace.to_addr = None;
3464 trace.contract_address = Some(CREATED.to_owned());
3465 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3466 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3467 let map = StepMap::new(&trace, vec![deployed, creation]);
3468 assert_eq!(
3469 map.line_key(0).map(|key| (key.contract, key.line)),
3470 Some((1, 8))
3471 );
3472 }
3473
3474 #[test]
3475 fn frames_are_resolved_from_recorded_calls_before_the_stack() {
3476 let callee = "0xbbbb000000000000000000000000000000000002";
3477 let precompile = "0x0000000000000000000000000000000000000001";
3478 let mut trace = trace(vec![
3480 step(0, 1, "PUSH1", &[]),
3481 step(11, 1, "STATICCALL", &[]),
3482 step(12, 1, "DELEGATECALL", &[]),
3483 step(21, 2, "JUMPDEST", &[]),
3484 step(21, 2, "ADD", &[]),
3485 step(13, 1, "JUMPDEST", &[]),
3486 ]);
3487 trace
3490 .artifacts
3491 .calls
3492 .push(call(0, 2, 2, "STATICCALL", precompile));
3493 trace
3494 .artifacts
3495 .calls
3496 .push(call(1, 3, 5, "DELEGATECALL", callee));
3497 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), contract(Some(callee))]);
3498
3499 assert_eq!(map.executing_address(2), Some(ROOT));
3500 assert_eq!(map.frame_depth(2), Some(1));
3501 assert_eq!(map.executing_address(3), Some(callee));
3502 assert_eq!(map.line_key(3).map(|key| key.contract), Some(1));
3503 assert_eq!(map.storage_address(3), Some(ROOT));
3505 }
3506
3507 const LEGACY_SOURCE: &str = "contract L {
3510 uint256 total;
3511 modifier tracked(uint256 tag) {
3512 uint256 before = total;
3513 _;
3514 total = before + tag;
3515 }
3516 function outer(uint256 a) public returns (uint256 sum) {
3517 uint256 twice = a * 2;
3518 if (twice > 2) {
3519 uint256 inner = twice - 2;
3520 sum = inner;
3521 }
3522 sum = twice + helper(twice);
3523 }
3524 function helper(uint256 x) internal pure returns (uint256 r) {
3525 uint256 y = x + 1;
3526 r = y;
3527 }
3528 function guarded(uint256 v) public tracked(9) {
3529 uint256 kept = v;
3530 total = kept;
3531 }
3532 function set(uint256 v) public {
3533 total = v;
3534 }
3535 function take(bytes calldata data, uint256[] calldata items, uint256 n) public {
3536 total = n;
3537 }
3538}
3539";
3540
3541 fn legacy_offset(needle: &str) -> u64 {
3542 LEGACY_SOURCE.find(needle).expect(needle) as u64
3543 }
3544
3545 fn legacy_contract(code_generator: Option<CodeGenerator>) -> ContractDebugInfo {
3549 let spans: &[(u64, &str)] = &[
3550 (0, "contract L"),
3551 (10, "function outer"),
3554 (11, "uint256 sum"),
3555 (12, "uint256 twice"),
3556 (13, "a * 2"),
3557 (14, "twice > 2"),
3558 (15, "uint256 inner"),
3559 (16, "sum = inner"),
3560 (17, "helper(twice)"),
3561 (18, "sum = twice + helper(twice)"),
3562 (19, "function outer"),
3563 (20, "function helper"),
3565 (21, "uint256 y"),
3566 (22, "r = y"),
3567 (23, "function helper"),
3568 (24, "uint256 r"),
3569 (30, "function guarded"),
3573 (31, "uint256 before"),
3574 (32, "uint256 kept"),
3575 (33, "total = kept"),
3576 (34, "total = before + tag"),
3577 (35, "function guarded"),
3578 (36, "function guarded"),
3579 (40, "function set"),
3582 (41, "total = v"),
3583 (42, "function set"),
3584 (43, "function set"),
3585 (50, "function take"),
3587 (51, "total = n"),
3588 ];
3589 let instructions = spans
3590 .iter()
3591 .map(|(pc, needle)| {
3592 if matches!(pc, 35 | 42) {
3594 instruction_with(
3595 *pc,
3596 legacy_offset(needle),
3597 needle.len() as u64,
3598 json!({"invoke": {}}),
3599 )
3600 } else {
3601 instruction(*pc, legacy_offset(needle), needle.len() as u64)
3602 }
3603 })
3604 .collect();
3605 let info = EthdebugInfo {
3606 compilation: serde_json::Value::Null,
3607 contract_name: "L".to_owned(),
3608 environment: "call".to_owned(),
3609 instructions,
3610 sources: BTreeMap::from([(0, "L.sol".to_owned())]),
3611 variable_locations: BTreeMap::new(),
3612 };
3613 ContractDebugInfo::new(
3614 Some(ROOT),
3615 "L",
3616 info,
3617 BTreeMap::from([(0, LEGACY_SOURCE.to_owned())]),
3618 )
3619 .with_code_generator(code_generator)
3620 }
3621
3622 fn outer_trace() -> TransactionTrace {
3625 let ret = "0x9";
3626 trace(vec![
3627 step(0, 1, "PUSH1", &[]),
3628 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
3629 step(11, 1, "PUSH0", &[ret, "0x5"]),
3630 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
3631 step(13, 1, "MUL", &[ret, "0x5", "0x0", "0x0"]),
3632 step(14, 1, "GT", &[ret, "0x5", "0x0", "0xa"]),
3633 step(15, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
3634 step(16, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0x8"]),
3635 step(18, 1, "DUP2", &[ret, "0x5", "0x8", "0xa"]),
3636 step(17, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
3637 step(
3638 20,
3639 1,
3640 "JUMPDEST",
3641 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"],
3642 ),
3643 step(24, 1, "PUSH0", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
3644 step(
3645 21,
3646 1,
3647 "PUSH0",
3648 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0"],
3649 ),
3650 step(
3651 22,
3652 1,
3653 "DUP1",
3654 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0", "0xb"],
3655 ),
3656 step(23, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0xb", "0x12"]),
3657 step(18, 1, "ADD", &[ret, "0x5", "0x8", "0xa", "0xb"]),
3658 step(19, 1, "JUMP", &[ret, "0x5", "0x15", "0xa"]),
3659 step(0, 1, "STOP", &[]),
3660 ])
3661 }
3662
3663 fn named(variables: &[InferredVariable]) -> Vec<(&str, VariableKind, Option<usize>)> {
3664 variables
3665 .iter()
3666 .map(|variable| (variable.name.as_str(), variable.kind, variable.slot))
3667 .collect()
3668 }
3669
3670 #[test]
3671 fn locals_are_inferred_from_the_legacy_stack_layout() {
3672 let map = StepMap::new(
3673 &outer_trace(),
3674 vec![legacy_contract(Some(CodeGenerator::Legacy))],
3675 );
3676 let trace = outer_trace();
3677
3678 assert_eq!(
3680 map.locals_at(0),
3681 LocalsStatus::Unavailable("no function is executing at this step")
3682 );
3683 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
3687 panic!("{:?}", map.locals_at(1));
3688 };
3689 assert_eq!(named(at_entry), [("sum", VariableKind::Return, None)]);
3690 let LocalsStatus::Inferred(at_return) = map.locals_at(2) else {
3691 panic!("{:?}", map.locals_at(2));
3692 };
3693 assert_eq!(
3694 named(at_return),
3695 [
3696 ("a", VariableKind::Parameter, Some(1)),
3697 ("sum", VariableKind::Return, Some(2))
3698 ]
3699 );
3700 let LocalsStatus::Inferred(at_twice) = map.locals_at(5) else {
3702 panic!("{:?}", map.locals_at(5));
3703 };
3704 assert_eq!(
3705 named(at_twice),
3706 [
3707 ("a", VariableKind::Parameter, Some(1)),
3708 ("sum", VariableKind::Return, Some(2)),
3709 ("twice", VariableKind::Local, Some(3))
3710 ]
3711 );
3712 let values = map.inferred_variables(&trace, 5, None);
3713 assert_eq!(values[0].value.display, "5");
3714 assert_eq!(values[2].value.display, "10");
3715 assert_eq!(values[2].location.offset, 3);
3716 let LocalsStatus::Inferred(in_block) = map.locals_at(7) else {
3718 panic!("{:?}", map.locals_at(7));
3719 };
3720 assert_eq!(
3721 named(in_block).last(),
3722 Some(&("inner", VariableKind::Local, Some(4)))
3723 );
3724 assert_eq!(
3725 map.inferred_variables(&trace, 7, None)[3].value.display,
3726 "8"
3727 );
3728 let LocalsStatus::Inferred(after_block) = map.locals_at(8) else {
3729 panic!("{:?}", map.locals_at(8));
3730 };
3731 assert_eq!(after_block.len(), 3);
3732 assert_eq!(
3733 map.inferred_variables(&trace, 8, None)[1].value.display,
3734 "8"
3735 );
3736
3737 let LocalsStatus::Inferred(helper_entry) = map.locals_at(10) else {
3742 panic!("{:?}", map.locals_at(10));
3743 };
3744 assert_eq!(
3745 named(helper_entry),
3746 [
3747 ("x", VariableKind::Parameter, Some(5)),
3748 ("r", VariableKind::Return, Some(6))
3749 ]
3750 );
3751 let at_entry = map.inferred_variables(&trace, 10, None);
3752 assert_eq!(
3753 at_entry[1].value.status,
3754 crate::DebugValueStatus::Unavailable
3755 );
3756 let helper_values = map.inferred_variables(&trace, 13, None);
3757 assert_eq!(
3758 helper_values
3759 .iter()
3760 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
3761 .collect::<Vec<_>>(),
3762 [("x", "10"), ("r", "0"), ("y", "11")]
3763 );
3764 assert_eq!(helper_values[2].location.offset, 7);
3765 let LocalsStatus::Inferred(back) = map.locals_at(15) else {
3766 panic!("{:?}", map.locals_at(15));
3767 };
3768 assert_eq!(named(back).len(), 3);
3769 assert_eq!(
3770 map.inferred_variables(&trace, 15, None)[2].value.display,
3771 "10"
3772 );
3773 }
3774
3775 #[test]
3776 fn a_modifier_places_itself_and_the_function_it_runs_for() {
3777 let ret = "0x9";
3778 let trace = trace(vec![
3779 step(30, 1, "JUMPDEST", &[ret, "0x7"]),
3780 step(31, 1, "PUSH0", &[ret, "0x7", "0x9"]),
3783 step(32, 1, "PUSH0", &[ret, "0x7", "0x9", "0x3"]),
3785 step(33, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3", "0x7"]),
3786 step(34, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3"]),
3788 ]);
3789 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3790 let LocalsStatus::Inferred(modifier) = map.locals_at(1) else {
3791 panic!("{:?}", map.locals_at(1));
3792 };
3793 assert_eq!(
3794 named(modifier),
3795 [
3796 ("tag", VariableKind::Parameter, Some(2)),
3797 ("before", VariableKind::Local, Some(3))
3798 ]
3799 );
3800 let LocalsStatus::Inferred(body) = map.locals_at(3) else {
3803 panic!("{:?}", map.locals_at(3));
3804 };
3805 assert_eq!(
3806 named(body),
3807 [
3808 ("v", VariableKind::Parameter, Some(1)),
3809 ("kept", VariableKind::Local, Some(4))
3810 ]
3811 );
3812 assert_eq!(
3813 map.inferred_variables(&trace, 3, None)[1].value.display,
3814 "7"
3815 );
3816 let LocalsStatus::Inferred(resumed) = map.locals_at(4) else {
3817 panic!("{:?}", map.locals_at(4));
3818 };
3819 assert_eq!(
3820 named(resumed),
3821 [
3822 ("tag", VariableKind::Parameter, Some(2)),
3823 ("before", VariableKind::Local, Some(3))
3824 ]
3825 );
3826 assert_eq!(
3827 map.inferred_variables(&trace, 4, None)[1].value.display,
3828 "3"
3829 );
3830 }
3831
3832 #[test]
3833 fn calldata_slices_read_the_frames_calldata() {
3834 let ret = "0x9";
3839 let mut trace = trace(vec![
3840 step(
3841 50,
3842 1,
3843 "JUMPDEST",
3844 &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"],
3845 ),
3846 step(51, 1, "PUSH0", &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"]),
3847 ]);
3848 trace.input_data = format!(
3849 "0xaabbccdd{}{}6162",
3850 "0000000000000000000000000000000000000000000000000000000000000003",
3851 "0000000000000000000000000000000000000000000000000000000000000004"
3852 );
3853 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3854 let LocalsStatus::Inferred(layout) = map.locals_at(1) else {
3855 panic!("{:?}", map.locals_at(1));
3856 };
3857 assert_eq!(
3858 layout
3859 .iter()
3860 .map(|variable| (variable.name.as_str(), variable.slot, variable.words))
3861 .collect::<Vec<_>>(),
3862 [
3863 ("data", Some(1), 2),
3864 ("items", Some(3), 2),
3865 ("n", Some(5), 1)
3866 ]
3867 );
3868 let values = map.inferred_variables(&trace, 1, None);
3869 assert_eq!(
3870 values
3871 .iter()
3872 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
3873 .collect::<Vec<_>>(),
3874 [("data", "0x6162"), ("items", "[3, 4]"), ("n", "5")]
3875 );
3876 assert_eq!(values[0].value.raw.as_deref(), Some("0x6162"));
3877 }
3878
3879 #[test]
3880 fn parameters_are_placed_by_the_first_statement_of_a_body_without_locals() {
3881 let trace = trace(vec![
3885 step(40, 1, "JUMPDEST", &["0x9", "0x7"]),
3886 step(41, 1, "DUP1", &["0x9", "0x7"]),
3887 step(41, 1, "SSTORE", &["0x9", "0x7", "0x7", "0x0"]),
3888 ]);
3889 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3890 let LocalsStatus::Inferred(at_entry) = map.locals_at(0) else {
3891 panic!("{:?}", map.locals_at(0));
3892 };
3893 assert!(at_entry.is_empty());
3894 let LocalsStatus::Inferred(in_body) = map.locals_at(2) else {
3895 panic!("{:?}", map.locals_at(2));
3896 };
3897 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(1))]);
3898 assert_eq!(
3899 map.inferred_variables(&trace, 2, None)[0].value.display,
3900 "7"
3901 );
3902 }
3903
3904 #[test]
3905 fn the_dispatchers_jump_to_the_body_places_a_public_function() {
3906 let selector = "0x9c";
3911 let mut trace = trace(vec![
3912 step(0, 1, "PUSH1", &[]),
3913 step(40, 1, "JUMPDEST", &[selector]),
3914 step(42, 1, "JUMP", &[selector, "0x9", "0x7", "0x2b"]),
3915 step(43, 1, "JUMPDEST", &[selector, "0x9", "0x7"]),
3916 step(41, 1, "SSTORE", &[selector, "0x9", "0x7", "0x0"]),
3917 ]);
3918 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3919 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
3920 panic!("{:?}", map.locals_at(1));
3921 };
3922 assert!(at_entry.is_empty());
3923 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
3924 panic!("{:?}", map.locals_at(4));
3925 };
3926 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
3927 assert_eq!(
3928 map.inferred_variables(&trace, 4, None)[0].value.display,
3929 "7"
3930 );
3931
3932 let selector = super::hex_bytes(&function_selector("set(uint256)").expect("selector"));
3936 trace.input_data = format!("0x{selector}{:064x}", 8);
3937 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3938 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
3939 panic!("{:?}", map.locals_at(4));
3940 };
3941 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(3))]);
3942 trace.input_data = format!("0x{selector}{:064x}", 7);
3944 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3945 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
3946 panic!("{:?}", map.locals_at(4));
3947 };
3948 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
3949 }
3950
3951 #[test]
3952 fn a_placed_function_places_its_modifiers() {
3953 let selector = "0x9c";
3959 let ret = "0x9";
3960 let trace = trace(vec![
3961 step(0, 1, "PUSH1", &[]),
3962 step(30, 1, "JUMPDEST", &[selector]),
3963 step(35, 1, "JUMP", &[selector, ret, "0x5", "0x24"]),
3964 step(36, 1, "JUMPDEST", &[selector, ret, "0x5"]),
3965 step(31, 1, "DUP1", &[selector, ret, "0x5", "0x9", "0x9"]),
3966 step(32, 1, "PUSH0", &[selector, ret, "0x5", "0x9", "0x9"]),
3967 step(
3968 33,
3969 1,
3970 "SSTORE",
3971 &[selector, ret, "0x5", "0x9", "0x9", "0x5"],
3972 ),
3973 step(34, 1, "SSTORE", &[selector, ret, "0x5", "0x9", "0x9"]),
3974 ]);
3975 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3976 let LocalsStatus::Inferred(modifier) = map.locals_at(4) else {
3977 panic!("{:?}", map.locals_at(4));
3978 };
3979 assert_eq!(
3980 named(modifier),
3981 [
3982 ("tag", VariableKind::Parameter, Some(3)),
3983 ("before", VariableKind::Local, Some(4))
3984 ]
3985 );
3986 let LocalsStatus::Inferred(body) = map.locals_at(6) else {
3987 panic!("{:?}", map.locals_at(6));
3988 };
3989 assert_eq!(
3990 named(body),
3991 [
3992 ("v", VariableKind::Parameter, Some(2)),
3993 ("kept", VariableKind::Local, Some(5))
3994 ]
3995 );
3996 assert_eq!(
3997 map.inferred_variables(&trace, 6, None)[1].value.display,
3998 "5"
3999 );
4000 let LocalsStatus::Inferred(resumed) = map.locals_at(7) else {
4001 panic!("{:?}", map.locals_at(7));
4002 };
4003 assert_eq!(
4004 named(resumed),
4005 [
4006 ("tag", VariableKind::Parameter, Some(3)),
4007 ("before", VariableKind::Local, Some(4))
4008 ]
4009 );
4010 let values = map.inferred_variables(&trace, 7, None);
4011 assert_eq!(values[0].value.display, "9");
4012 assert_eq!(values[1].value.display, "9");
4013 }
4014
4015 #[test]
4016 fn a_function_reached_by_fallthrough_was_inlined_and_has_no_slots() {
4017 let ret = "0x9";
4021 let trace = trace(vec![
4022 step(0, 1, "PUSH1", &[]),
4023 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
4024 step(11, 1, "PUSH0", &[ret, "0x5"]),
4025 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
4026 step(21, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
4027 step(22, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0xb"]),
4028 step(18, 1, "ADD", &[ret, "0x5", "0x0", "0xa", "0xb"]),
4029 ]);
4030 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
4031 assert_eq!(
4032 map.location(4).and_then(|location| location.function_name),
4033 Some("helper".to_owned())
4034 );
4035 assert_eq!(
4036 map.locals_at(4),
4037 LocalsStatus::Unavailable(
4038 "this function was inlined by the optimizer, so its variables have no stack \
4039 slots of their own"
4040 )
4041 );
4042 assert_eq!(
4043 map.location(6).and_then(|location| location.function_name),
4044 Some("outer".to_owned())
4045 );
4046 let LocalsStatus::Inferred(back) = map.locals_at(6) else {
4047 panic!("{:?}", map.locals_at(6));
4048 };
4049 assert_eq!(
4050 named(back),
4051 [
4052 ("a", VariableKind::Parameter, Some(1)),
4053 ("sum", VariableKind::Return, Some(2)),
4054 ("twice", VariableKind::Local, Some(3))
4055 ]
4056 );
4057 }
4058
4059 #[test]
4060 fn locals_are_not_inferred_for_the_via_ir_pipeline_or_an_unknown_one() {
4061 let via_ir = StepMap::new(
4062 &outer_trace(),
4063 vec![legacy_contract(Some(CodeGenerator::ViaIr))],
4064 );
4065 assert!(matches!(
4066 via_ir.locals_at(5),
4067 LocalsStatus::Unavailable(reason) if reason.contains("via-IR pipeline")
4068 ));
4069 assert!(via_ir
4070 .inferred_variables(&outer_trace(), 5, None)
4071 .is_empty());
4072 let unknown = StepMap::new(&outer_trace(), vec![legacy_contract(None)]);
4073 assert!(matches!(
4074 unknown.locals_at(5),
4075 LocalsStatus::Unavailable(reason) if reason.contains("not known")
4076 ));
4077 }
4078
4079 #[test]
4080 fn steps_without_debug_info_have_no_location() {
4081 let map = StepMap::new(&outer_calls_inner(), Vec::new());
4082 assert!(!map.has_source());
4083 assert_eq!(map.line_key(3), None);
4084 assert_eq!(map.frame_depth(3), Some(0));
4085 assert_eq!(map.next_source(3), None);
4087 assert_eq!(map.previous_source(3), None);
4088 assert!(map.resolve_line(None, 3).is_err());
4089 assert_eq!(map.frames(3).len(), 1);
4090 }
4091
4092 #[test]
4093 fn steps_without_source_are_transparent_to_line_stepping() {
4094 let map = StepMap::new(
4096 &trace(vec![
4097 step(0, 1, "PUSH1", &[]),
4098 step(10, 1, "JUMPDEST", &[]),
4099 step(11, 1, "ADD", &[]),
4100 step(99, 1, "MUL", &[]),
4101 step(11, 1, "SWAP1", &[]),
4102 step(14, 1, "POP", &[]),
4103 step(30, 1, "STOP", &[]),
4104 ]),
4105 vec![contract(None)],
4106 );
4107 assert_eq!(map.line_key(3), None);
4108 assert_eq!(map.frame_depth(3), Some(1));
4109 assert_eq!(map.next_source(2), Some(5));
4111 assert_eq!(map.step_into(2), Some(5));
4112 assert!(!map.is_line_start(3));
4113 assert!(!map.is_line_start(4));
4114 assert_eq!(map.previous_source(5), Some(2));
4116 assert_eq!(map.previous_source(4), Some(2));
4117 assert_eq!(map.previous_source(3), Some(2));
4118 assert_eq!(map.previous_source(2), Some(1));
4119 }
4120
4121 #[test]
4122 fn generated_code_belongs_to_the_statement_and_the_epilogue_pops_the_function() {
4123 let map = StepMap::new(
4126 &trace(vec![
4127 step(0, 1, "PUSH1", &[]),
4128 step(10, 1, "JUMPDEST", &[]),
4129 step(11, 1, "ADD", &[]),
4130 step(40, 1, "JUMPDEST", &[]),
4131 step(40, 1, "SSTORE", &[]),
4132 step(11, 1, "SWAP1", &[]),
4133 step(14, 1, "POP", &[]),
4134 step(30, 1, "STOP", &[]),
4135 ]),
4136 vec![contract(None)],
4137 );
4138 let helper = map.location(4).expect("location");
4139 assert!(helper.generated);
4140 assert_eq!(helper.line, 3);
4141 assert_eq!(helper.function_name.as_deref(), Some("outer"));
4142 assert_eq!(map.line_key(4), key(3));
4143 assert_eq!(map.function_id(4).map(|id| id.function), Some(0));
4144 assert!(!map.is_line_start(4));
4145 assert!(!map.is_line_start(5));
4146 assert_eq!(map.frame_depth(4), Some(1));
4148 assert_eq!(map.frame_depth(7), Some(0));
4149 assert_eq!(map.line_key(7), None);
4150 assert!(!map.location(7).expect("location").generated);
4151 assert_eq!(map.next_source(2), Some(6));
4152 assert_eq!(map.finish(2), Some(7));
4153 assert_eq!(map.reverse_finish(4), Some(0));
4154 assert_eq!(map.previous_source(6), Some(2));
4155 }
4156
4157 #[test]
4158 fn a_lone_instruction_on_another_line_belongs_to_the_line_around_it() {
4159 let map = StepMap::new(
4162 &trace(vec![
4163 step(0, 1, "PUSH1", &[]),
4164 step(10, 1, "JUMPDEST", &[]),
4165 step(12, 1, "PUSH2", &[]),
4166 step(10, 1, "SWAP1", &[]),
4167 step(11, 1, "ADD", &[]),
4168 step(14, 1, "POP", &[]),
4169 step(30, 1, "STOP", &[]),
4170 ]),
4171 vec![contract(None)],
4172 );
4173 assert_eq!(map.line_key(2), key(2));
4174 assert_eq!(map.location(2).expect("location").line, 4);
4175 assert!(!map.is_line_start(2));
4176 assert!(!map.is_line_start(3));
4177 assert_eq!(map.next_source(1), Some(4));
4178 assert_eq!(map.previous_source(4), Some(1));
4179 assert_eq!(map.line_key(5), key(5));
4181 assert!(map.is_line_start(5));
4182 }
4183
4184 #[test]
4185 fn a_jump_onto_a_function_entry_is_a_call_even_when_it_is_active() {
4186 let map = StepMap::new(
4190 &trace(vec![
4191 step(0, 1, "PUSH1", &[]),
4192 step(1, 1, "JUMP", &[]),
4193 step(10, 1, "JUMPDEST", &[]),
4194 step(12, 1, "JUMP", &[]),
4195 step(20, 1, "JUMPDEST", &[]),
4196 step(21, 1, "JUMP", &[]),
4197 step(10, 1, "JUMPDEST", &[]),
4198 step(14, 1, "JUMP", &[]),
4199 step(21, 1, "SWAP1", &[]),
4200 step(21, 1, "JUMP", &[]),
4201 step(13, 1, "JUMPDEST", &[]),
4202 step(14, 1, "POP", &[]),
4203 step(30, 1, "STOP", &[]),
4204 ]),
4205 vec![contract(None)],
4206 );
4207 let contract = &map.contracts()[0];
4208 assert_eq!(contract.function_entry_at_pc(10), Some(0));
4209 assert_eq!(contract.function_entry_at_pc(20), Some(1));
4210 assert_eq!(contract.function_entry_at_pc(11), None);
4211 let depths = (0..13)
4212 .map(|step| map.frame_depth(step).expect("depth"))
4213 .collect::<Vec<_>>();
4214 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 1, 1, 0]);
4215 assert!(map.is_frame_entry(6));
4216 let frames = map.frames(6);
4217 assert_eq!(
4218 frames
4219 .iter()
4220 .map(|frame| frame.function_name.as_deref().unwrap_or("-"))
4221 .collect::<Vec<_>>(),
4222 vec!["outer", "inner", "outer", "-"]
4223 );
4224 assert_eq!(map.finish(6), Some(8));
4225 assert_eq!(map.reverse_finish(6), Some(5));
4226 assert_eq!(map.finish(4), Some(10));
4227 }
4228
4229 #[test]
4230 fn a_recursive_call_returns_at_the_address_read_off_the_stack() {
4231 let map = StepMap::new(
4236 &trace(vec![
4237 step(0, 1, "PUSH1", &[]),
4238 step(1, 1, "JUMP", &[]),
4239 step(10, 1, "JUMPDEST", &[]),
4240 step(12, 1, "JUMP", &["0xd", "0x1", "0xa"]),
4241 step(10, 1, "JUMPDEST", &["0x1"]),
4242 step(11, 1, "ADD", &["0x2"]),
4243 step(14, 1, "JUMP", &["0xd"]),
4244 step(13, 1, "JUMPDEST", &[]),
4245 step(14, 1, "POP", &[]),
4246 step(30, 1, "STOP", &[]),
4247 ]),
4248 vec![contract(None)],
4249 );
4250 let depths = (0..10)
4251 .map(|step| map.frame_depth(step).expect("depth"))
4252 .collect::<Vec<_>>();
4253 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 2, 1, 1, 0]);
4254 assert!(map.is_frame_entry(4));
4255 assert_eq!(map.finish(4), Some(7));
4256 assert_eq!(map.frames(5).len(), 3);
4257 assert_eq!(map.frames(7).len(), 2);
4258 assert!(map.contracts()[0].is_jumpdest(13));
4259 assert!(!map.contracts()[0].is_jumpdest(99));
4260 }
4261
4262 #[test]
4263 fn jump_markers_decide_calls_and_returns_without_entry_points() {
4264 let whole = SOURCE.len() as u64;
4269 let info = EthdebugInfo {
4270 compilation: serde_json::Value::Null,
4271 contract_name: "C".to_owned(),
4272 environment: "runtime".to_owned(),
4273 instructions: vec![
4274 instruction(0, 0, whole),
4275 instruction(1, 0, whole),
4276 instruction(10, offset_of("function outer"), 90),
4277 instruction(11, offset_of("uint256 b = a + 1;"), 18),
4278 instruction_with(
4279 12,
4280 offset_of("inner(b);"),
4281 9,
4282 json!({"invoke": {"identifier": "outer"}}),
4283 ),
4284 instruction(13, offset_of("inner(b);"), 9),
4285 instruction_with(14, offset_of("b = 0;"), 6, json!({"return": {}})),
4286 instruction(30, 0, whole),
4287 ],
4288 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4289 variable_locations: BTreeMap::new(),
4290 };
4291 let marked =
4292 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4293 assert_eq!(marked.jump_marker_at_pc(12), JumpMarker::Call);
4294 assert_eq!(marked.jump_marker_at_pc(14), JumpMarker::Return);
4295 assert_eq!(marked.jump_marker_at_pc(13), JumpMarker::None);
4296 let map = StepMap::new(
4297 &trace(vec![
4298 step(0, 1, "PUSH1", &[]),
4299 step(1, 1, "JUMP", &[]),
4300 step(10, 1, "JUMPDEST", &[]),
4301 step(12, 1, "JUMP", &[]),
4302 step(11, 1, "ADD", &[]),
4303 step(14, 1, "JUMP", &[]),
4304 step(13, 1, "JUMPDEST", &[]),
4305 step(14, 1, "POP", &[]),
4306 step(30, 1, "STOP", &[]),
4307 ]),
4308 vec![marked],
4309 );
4310 let depths = (0..9)
4311 .map(|step| map.frame_depth(step).expect("depth"))
4312 .collect::<Vec<_>>();
4313 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 1, 0]);
4314 assert!(!map.is_frame_entry(4));
4315 assert_eq!(map.frames(4).len(), 2);
4316 assert_eq!(map.finish(4), Some(8));
4317 let map = StepMap::new(
4319 &trace(vec![
4320 step(0, 1, "PUSH1", &[]),
4321 step(1, 1, "JUMP", &[]),
4322 step(10, 1, "JUMPDEST", &[]),
4323 step(12, 1, "JUMP", &[]),
4324 step(20, 1, "JUMPDEST", &[]),
4325 step(21, 1, "JUMP", &[]),
4326 step(13, 1, "JUMPDEST", &[]),
4327 step(30, 1, "STOP", &[]),
4328 ]),
4329 vec![contract(None)],
4330 );
4331 assert_eq!(map.frame_depth(4), Some(2));
4332 assert_eq!(map.frame_depth(6), Some(1));
4333 }
4334
4335 #[test]
4336 fn marked_jumps_into_generated_helpers_do_not_pop_the_function() {
4337 let whole = SOURCE.len() as u64;
4341 let info = EthdebugInfo {
4342 compilation: serde_json::Value::Null,
4343 contract_name: "C".to_owned(),
4344 environment: "runtime".to_owned(),
4345 instructions: vec![
4346 instruction(0, 0, whole),
4347 instruction(1, 0, whole),
4348 instruction(10, offset_of("function outer"), 90),
4349 instruction_with(
4350 11,
4351 offset_of("uint256 b = a + 1;"),
4352 18,
4353 json!({"invoke": {}}),
4354 ),
4355 instruction_with(40, 0, whole, json!({"return": {}})),
4356 instruction(14, offset_of("b = 0;"), 6),
4357 instruction(30, 0, whole),
4358 ],
4359 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4360 variable_locations: BTreeMap::new(),
4361 };
4362 let contract =
4363 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4364 let map = StepMap::new(
4365 &trace(vec![
4366 step(0, 1, "PUSH1", &[]),
4367 step(1, 1, "JUMP", &[]),
4368 step(10, 1, "JUMPDEST", &[]),
4369 step(11, 1, "JUMP", &[]),
4370 step(40, 1, "JUMPDEST", &[]),
4371 step(40, 1, "JUMP", &[]),
4372 step(14, 1, "JUMPDEST", &[]),
4373 step(30, 1, "STOP", &[]),
4374 ]),
4375 vec![contract],
4376 );
4377 let depths = (0..8)
4378 .map(|step| map.frame_depth(step).expect("depth"))
4379 .collect::<Vec<_>>();
4380 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 0]);
4381 assert!(map.location(4).expect("location").generated);
4382 assert_eq!(
4383 map.location(6).expect("location").function_name.as_deref(),
4384 Some("outer")
4385 );
4386 assert_eq!(map.next_source(3), Some(6));
4387 assert_eq!(map.finish(3), Some(7));
4388 }
4389
4390 #[test]
4391 fn contract_info_indexes_lines_and_functions() {
4392 let contract = contract(Some("0xABCD"));
4393 assert_eq!(contract.address.as_deref(), Some("0xabcd"));
4394 assert_eq!(contract.line_of(0, 0), Some(1));
4395 assert_eq!(
4396 contract.line_of(0, SOURCE.find("inner(b)").unwrap() as u64),
4397 Some(4)
4398 );
4399 assert_eq!(contract.line_count(0), Some(11));
4400 assert_eq!(contract.line_text(0, 8).map(str::trim), Some("x += 1;"));
4401 assert_eq!(contract.line_text(0, 99), None);
4402 assert_eq!(
4403 contract.function_at_pc(21).map(|f| f.name.as_str()),
4404 Some("inner")
4405 );
4406 assert_eq!(contract.function_at_pc(0), None);
4407 assert_eq!(contract.function_at_pc(999), None);
4408 assert_eq!(contract.functions[0].declaration_line, 2);
4409 assert_eq!(contract.effective_line(0, 3), Some(3));
4410 assert_eq!(contract.effective_line(0, 6), Some(1));
4411 assert_eq!(contract.effective_line(0, 0), None);
4412 assert_eq!(contract.effective_line(0, 12), None);
4413 assert_eq!(contract.effective_line(7, 1), None);
4414
4415 assert_eq!(normalize_address("0XAbC"), "0xabc");
4416 assert_eq!(
4417 address_from_word("0x000000000000000000000000AAAA000000000000000000000000000000000001"),
4418 Some("0xaaaa000000000000000000000000000000000001".to_owned())
4419 );
4420 assert_eq!(address_from_word("0x0"), None);
4421 assert_eq!(address_from_word("zz"), None);
4422 }
4423
4424 const SECOND_ROOT: &str = "0xbbbb000000000000000000000000000000000002";
4425
4426 fn two_roots() -> TransactionTrace {
4430 let mut trace = outer_calls_inner();
4431 let again = trace.steps.clone();
4432 trace.steps.extend(again);
4433 let mut first = call(0, 0, 11, "CALL", ROOT);
4434 first.input = "0x01".to_owned();
4435 let mut second = call(1, 11, 22, "CALL", SECOND_ROOT);
4436 second.input = "0x02".to_owned();
4437 trace.artifacts.calls = vec![first, second];
4438 trace
4439 }
4440
4441 #[test]
4442 fn each_root_call_maps_through_its_own_contract() {
4443 let trace = two_roots();
4444 let map = StepMap::new(
4445 &trace,
4446 vec![contract(Some(ROOT)), contract(Some(SECOND_ROOT))],
4447 );
4448 let address_at = |step: usize| {
4449 map.contract_at_step(step)
4450 .and_then(|contract| contract.address.clone())
4451 };
4452 assert_eq!(address_at(3).as_deref(), Some(ROOT));
4453 assert_eq!(address_at(14).as_deref(), Some(SECOND_ROOT));
4454
4455 let frames = map.frames(17);
4457 assert_eq!(frames.len(), 3);
4458 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
4459 assert!(frames[2].external);
4460 assert_eq!(frames[2].entry_step, 11);
4461 assert_eq!(frames[2].address.as_deref(), Some(SECOND_ROOT));
4462
4463 let next = map.next_source(10).expect("a line in the second call");
4465 assert!(next > 10);
4466 assert_eq!(address_at(next).as_deref(), Some(SECOND_ROOT));
4467
4468 assert_eq!(map.calldata_at(&trace, 3), "0x01");
4469 assert_eq!(map.calldata_at(&trace, 14), "0x02");
4470 }
4471
4472 #[test]
4473 fn finish_from_a_root_frame_stops_where_the_next_root_starts() {
4474 let map = StepMap::new(
4475 &two_roots(),
4476 vec![contract(Some(ROOT)), contract(Some(SECOND_ROOT))],
4477 );
4478 assert_eq!(map.finish(1), Some(11));
4480 assert_eq!(map.finish(10), Some(11));
4481 assert_eq!(map.reverse_finish(12), Some(11));
4483 assert_eq!(map.finish(12), None);
4485 assert_eq!(map.reverse_finish(1), None);
4486 }
4487
4488 #[test]
4489 fn reverse_next_stays_within_a_root() {
4490 let mut trace = outer_calls_inner();
4493 trace.steps.truncate(10);
4494 trace.steps.push(step(10, 1, "JUMP", &[]));
4495 trace.steps.extend(outer_calls_inner().steps);
4496 trace.artifacts.calls = vec![call(0, 0, 11, "CALL", ROOT), call(1, 11, 22, "CALL", ROOT)];
4497 let map = StepMap::new(&trace, vec![contract(Some(ROOT))]);
4498 assert_eq!(map.line_key(10), map.line_key(13));
4499 assert_eq!(map.previous_source(14), Some(13));
4500 }
4501
4502 #[test]
4503 fn a_single_contract_describes_only_a_single_root() {
4504 let map = StepMap::new(&two_roots(), vec![contract(None)]);
4505 assert!(map.contract_at_step(3).is_none());
4506 assert!(map.contract_at_step(14).is_none());
4507 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
4508 assert!(map.contract_at_step(3).is_some());
4509 }
4510
4511 #[test]
4512 fn smoothing_stays_within_a_root() {
4513 let mut trace = outer_calls_inner();
4516 trace.steps.truncate(10);
4517 trace.steps.push(step(10, 1, "JUMPDEST", &[]));
4518 trace.steps.push(step(14, 1, "POP", &[]));
4519 trace.steps.push(step(30, 1, "STOP", &[]));
4520 trace.artifacts.calls = vec![call(0, 0, 10, "CALL", ROOT), call(1, 10, 13, "CALL", ROOT)];
4521 let map = StepMap::new(&trace, vec![contract(Some(ROOT))]);
4522 assert_eq!(map.frames(10)[0].function_name.as_deref(), Some("outer"));
4523 assert_ne!(map.line_key(10), map.line_key(9));
4524 assert_eq!(map.line_key(9), map.line_key(11));
4525 }
4526}