1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19
20use soldb_core::{StepSnapshot, TraceStep, TransactionTrace, Word as StackWord};
21use soldb_ethdebug::{decode_value, parse_word, EthdebugInfo, VariableLocation, Word};
22
23pub mod condition;
24pub mod debug_diff;
25pub mod state;
26pub mod stepping;
27
28pub use condition::{Condition, ConditionContext, Evaluation};
29pub use debug_diff::{
30 capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
31 DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
32 DebugTraceSummary,
33};
34pub use soldb_ethdebug::StorageLayout;
35pub use state::{
36 short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
37 StateVariable, StorageTape, StorageWords,
38};
39pub use stepping::{
40 address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
41 Frame, FunctionId, JumpMarker, LineKey, ResolvedFunction, ResolvedLine, SourceListing,
42 StepLocation, StepMap,
43};
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct DebugSession {
47 pub trace: TransactionTrace,
48 pub ethdebug: Option<EthdebugInfo>,
49 #[serde(default)]
50 pub source_contents: BTreeMap<u64, String>,
51 #[serde(default)]
52 pub functions: Vec<SourceFunction>,
53}
54
55impl DebugSession {
56 #[must_use]
57 pub fn new(trace: TransactionTrace) -> Self {
58 Self {
59 trace,
60 ethdebug: None,
61 source_contents: BTreeMap::new(),
62 functions: Vec::new(),
63 }
64 }
65
66 #[must_use]
67 pub fn with_ethdebug(
68 trace: TransactionTrace,
69 ethdebug: EthdebugInfo,
70 source_contents: BTreeMap<u64, String>,
71 ) -> Self {
72 let mut session = Self::new(trace);
73 session.attach_ethdebug(ethdebug, source_contents);
74 session
75 }
76
77 pub fn attach_ethdebug(
84 &mut self,
85 ethdebug: EthdebugInfo,
86 source_contents: BTreeMap<u64, String>,
87 ) {
88 self.functions = source_contents
89 .iter()
90 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
91 .collect();
92 self.ethdebug = Some(ethdebug);
93 self.source_contents = source_contents;
94 }
95
96 #[must_use]
97 pub fn step(&self, step_index: usize) -> Option<DebugStep> {
98 let step = self.trace.steps.get(step_index)?;
99 Some(DebugStep {
100 index: step_index,
101 pc: step.pc,
102 op: step.op.to_string(),
103 gas: step.gas,
104 gas_cost: step.gas_cost,
105 depth: step.depth,
106 source: self.source_span(step.pc),
107 function: self.function_at_pc(step.pc).cloned(),
108 snapshot: step.normalized_snapshot(),
109 variables: self.variables_at_step(step),
110 })
111 }
112
113 #[must_use]
114 pub fn steps(&self) -> Vec<DebugStep> {
115 (0..self.trace.steps.len())
116 .filter_map(|step_index| self.step(step_index))
117 .collect()
118 }
119
120 #[must_use]
121 pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
122 let ethdebug = self.ethdebug.as_ref()?;
123 let instruction = ethdebug.instruction_at_pc(pc)?;
124 let location = instruction.source_location()?;
125 let path = ethdebug.sources.get(&location.source_id)?.clone();
126 let position = self
127 .source_contents
128 .get(&location.source_id)
129 .map(|source| line_column_for_offset(source, location.offset as usize));
130 Some(SourceSpan {
131 source_id: location.source_id,
132 path,
133 offset: location.offset,
134 length: location.length,
135 line: position.map_or(0, |position| position.line),
136 column: position.map_or(0, |position| position.column),
137 })
138 }
139
140 #[must_use]
141 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
142 let location = self
143 .ethdebug
144 .as_ref()?
145 .instruction_at_pc(pc)?
146 .source_location()?;
147 self.functions
148 .iter()
149 .filter(|function| {
150 function.source_id == location.source_id
151 && function.declaration_start <= location.offset
152 && location.offset <= function.body_end
153 })
154 .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
155 }
156
157 #[must_use]
158 pub fn variables_at_step(&self, step: &TraceStep) -> Vec<DebugVariable> {
159 let Some(ethdebug) = &self.ethdebug else {
160 return Vec::new();
161 };
162 variables_for_step(&self.trace, ethdebug, step)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct DebugStep {
168 pub index: usize,
169 pub pc: u64,
170 pub op: String,
171 pub gas: u64,
172 pub gas_cost: u64,
173 pub depth: u64,
174 pub source: Option<SourceSpan>,
175 pub function: Option<SourceFunction>,
176 pub snapshot: StepSnapshot,
177 pub variables: Vec<DebugVariable>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct SourceSpan {
182 pub source_id: u64,
183 pub path: String,
184 pub offset: u64,
185 pub length: u64,
186 pub line: u64,
187 pub column: u64,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct SourceFunction {
192 pub source_id: u64,
193 pub name: String,
196 pub params: Vec<SourceParam>,
197 pub declaration_start: u64,
198 #[serde(default)]
200 pub declaration_line: u64,
201 pub body_end: u64,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct SourceParam {
206 pub name: String,
207 pub ty: String,
209 #[serde(default)]
212 pub location: Option<String>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct FrameArgument {
218 pub name: String,
219 pub ty: String,
220 pub value: DebugValue,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct DebugVariable {
225 pub name: String,
226 pub ty: String,
227 pub location: DebugLocation,
228 pub value: DebugValue,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct DebugLocation {
233 pub kind: String,
234 pub offset: u64,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct DebugValue {
239 pub display: String,
240 pub raw: Option<String>,
241 pub status: DebugValueStatus,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum DebugValueStatus {
247 Decoded,
248 Raw,
249 Unavailable,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253struct SourcePosition {
254 line: u64,
255 column: u64,
256}
257
258#[must_use]
259pub fn variables_for_step(
260 trace: &TransactionTrace,
261 ethdebug: &EthdebugInfo,
262 step: &TraceStep,
263) -> Vec<DebugVariable> {
264 ethdebug
265 .variables_at_pc(step.pc)
266 .into_iter()
267 .map(|variable| decode_variable(trace, step, variable))
268 .collect()
269}
270
271fn decode_variable(
272 trace: &TransactionTrace,
273 step: &TraceStep,
274 variable: &VariableLocation,
275) -> DebugVariable {
276 let raw = raw_value_for_location(trace, step, variable);
277 let value = raw.map_or_else(
278 || DebugValue {
279 display: "<unavailable>".to_owned(),
280 raw: None,
281 status: DebugValueStatus::Unavailable,
282 },
283 |raw| decode_debug_value(&raw, &variable.ty),
284 );
285 DebugVariable {
286 name: variable.name.clone(),
287 ty: variable.ty.clone(),
288 location: DebugLocation {
289 kind: variable.location_type.clone(),
290 offset: variable.offset,
291 },
292 value,
293 }
294}
295
296fn raw_value_for_location(
297 trace: &TransactionTrace,
298 step: &TraceStep,
299 variable: &VariableLocation,
300) -> Option<String> {
301 let snapshot = step.snapshot_ref();
302 match variable.location_type.as_str() {
303 "stack" => snapshot
304 .stack
305 .get(variable.offset as usize)
306 .map(|value| normalize_hex(value)),
307 "memory" => word_from_hex_bytes(snapshot.memory?, variable.offset as usize),
308 "calldata" => word_from_hex_bytes(&trace.input_data, variable.offset as usize),
309 "storage" => storage_value(snapshot.storage, variable.offset),
310 _ => None,
311 }
312}
313
314fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
315 let ty = ty.trim();
316 let normalized = normalize_hex(raw);
317 let word = normalized.trim_start_matches("0x");
318 let decoded = decode_static_word(word, ty);
319 match decoded {
320 Some(display) => DebugValue {
321 display,
322 raw: Some(normalized),
323 status: DebugValueStatus::Decoded,
324 },
325 None => DebugValue {
326 display: normalized.clone(),
327 raw: Some(normalized),
328 status: DebugValueStatus::Raw,
329 },
330 }
331}
332
333fn decode_static_word(word: &str, ty: &str) -> Option<String> {
334 if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
335 return None;
336 }
337 if ty.starts_with("uint") {
338 return Some(format_uint_word(word));
339 }
340 if ty == "address" {
341 let padded = left_pad_word(word);
342 return Some(
343 format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
344 );
345 }
346 if ty == "bool" {
347 let trimmed = word.trim_start_matches('0');
348 return match trimmed {
349 "" | "0" => Some("false".to_owned()),
350 "1" => Some("true".to_owned()),
351 _ => None,
352 };
353 }
354 if ty == "bytes32" {
355 return Some(normalize_hex(word));
356 }
357 if let Some(size) = fixed_bytes_size(ty) {
358 let padded = right_pad_word(word);
359 return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
360 }
361 None
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum ArgumentOrder {
369 FirstOnTop,
372 LastOnTop,
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub enum ArgumentLayout {
380 Ordered(ArgumentOrder),
382 TopWords,
385}
386
387impl ArgumentOrder {
388 #[must_use]
391 fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
392 match self {
393 Self::FirstOnTop => stack_len - 1 - index,
394 Self::LastOnTop => stack_len - count + index,
395 }
396 }
397}
398
399#[derive(Debug, Clone, Copy, Default)]
402pub struct FrameState<'a> {
403 pub stack: &'a [StackWord],
404 pub memory: Option<&'a str>,
407}
408
409#[must_use]
418pub fn decode_arguments(
419 params: &[SourceParam],
420 state: FrameState<'_>,
421 layout: ArgumentLayout,
422) -> Vec<FrameArgument> {
423 let stack = state.stack;
424 if params.is_empty() || stack.len() < params.len() {
425 return Vec::new();
426 }
427 if !params.iter().all(readable_parameter) {
428 return Vec::new();
429 }
430 let order = match layout {
432 ArgumentLayout::Ordered(order) => order,
433 ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
434 ArgumentLayout::TopWords => return Vec::new(),
435 };
436 params
437 .iter()
438 .enumerate()
439 .map(|(index, param)| {
440 let word = &stack[order.word_index(index, params.len(), stack.len())];
441 FrameArgument {
442 name: param.name.clone(),
443 ty: param.ty.clone(),
444 value: argument_value(param, word, state.memory),
445 }
446 })
447 .collect()
448}
449
450fn argument_value(param: &SourceParam, word: &str, memory: Option<&str>) -> DebugValue {
451 let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
452 return DebugValue {
453 display: "<unreadable stack word>".to_owned(),
454 raw: Some(word.to_owned()),
455 status: DebugValueStatus::Unavailable,
456 };
457 };
458 let raw = Some(short_hex(&parsed));
459 match param.location.as_deref() {
460 Some("memory") => {
461 let pointer = word_as_usize(&parsed);
462 let display = match (pointer, memory) {
463 (Some(pointer), Some(memory)) => read_memory_value(memory, pointer, ¶m.ty)
464 .unwrap_or_else(|| {
465 format!(
466 "<{} in memory at {}, beyond what this step captured>",
467 param.ty,
468 short_hex(&parsed)
469 )
470 }),
471 (_, None) => format!(
472 "<{} in memory at {}; this backend captured no memory>",
473 param.ty,
474 short_hex(&parsed)
475 ),
476 (None, _) => format!("<{} at {}>", param.ty, short_hex(&parsed)),
477 };
478 let decoded =
479 display.starts_with('"') || display.starts_with('[') || display.starts_with("0x");
480 DebugValue {
481 display,
482 raw,
483 status: if decoded {
484 DebugValueStatus::Decoded
485 } else {
486 DebugValueStatus::Raw
487 },
488 }
489 }
490 Some(location) => DebugValue {
491 display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
492 raw,
493 status: DebugValueStatus::Raw,
494 },
495 None => DebugValue {
496 display: decode_value(value_bytes(&parsed, ¶m.ty), ¶m.ty),
497 raw,
498 status: DebugValueStatus::Decoded,
499 },
500 }
501}
502
503pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
505 match param.location.as_deref() {
506 Some("calldata") => !is_dynamic_type(¶m.ty),
508 Some(_) => true,
509 None => is_value_type(¶m.ty),
510 }
511}
512
513fn is_dynamic_type(ty: &str) -> bool {
515 ty == "string" || ty == "bytes" || ty.ends_with("[]")
516}
517
518fn read_memory_value(memory: &str, pointer: usize, ty: &str) -> Option<String> {
526 if ty == "string" || ty == "bytes" {
527 let length = word_as_usize(&memory_word(memory, pointer)?)?;
528 let bytes = memory_bytes(memory, pointer.checked_add(32)?, length)?;
529 if ty == "string" {
530 if let Ok(text) = std::str::from_utf8(&bytes) {
531 return Some(format!("{text:?}"));
532 }
533 }
534 return Some(format!("0x{}", hex_of(&bytes)));
535 }
536 let (element, count) = array_shape(ty)?;
537 if !is_value_type(element) {
538 return None;
539 }
540 let (first, count) = match count {
541 None => (
543 pointer.checked_add(32)?,
544 word_as_usize(&memory_word(memory, pointer)?)?,
545 ),
546 Some(count) => (pointer, count),
547 };
548 let shown = count.min(8);
549 let mut parts = Vec::with_capacity(shown);
550 for index in 0..shown {
551 let word = memory_word(memory, first.checked_add(index.checked_mul(32)?)?)?;
552 parts.push(decode_value(value_bytes(&word, element), element));
553 }
554 if count > shown {
555 parts.push(format!("... {} more", count - shown));
556 }
557 Some(format!("[{}]", parts.join(", ")))
558}
559
560fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
563 let inner = ty.strip_suffix(']')?;
564 let open = inner.rfind('[')?;
565 let element = inner[..open].trim();
566 let length = &inner[open + 1..];
567 if length.is_empty() {
568 return Some((element, None));
569 }
570 Some((element, Some(length.parse::<usize>().ok()?)))
571}
572
573fn memory_word(memory: &str, offset: usize) -> Option<Word> {
575 let bytes = memory_bytes(memory, offset, 32)?;
576 let mut word = [0_u8; 32];
577 word.copy_from_slice(&bytes);
578 Some(word)
579}
580
581fn memory_bytes(memory: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
583 let start = offset.checked_mul(2)?;
584 let end = start.checked_add(length.checked_mul(2)?)?;
585 let digits = memory.get(start..end)?;
586 if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
587 return None;
588 }
589 Some(
590 digits
591 .as_bytes()
592 .chunks(2)
593 .map(|pair| {
594 u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
595 })
596 .collect(),
597 )
598}
599
600fn word_as_usize(word: &Word) -> Option<usize> {
602 if word[..24].iter().any(|byte| *byte != 0) {
603 return None;
604 }
605 usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
606}
607
608fn hex_of(bytes: &[u8]) -> String {
609 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
610}
611
612fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
616 if let Some(bits) = integer_bits(ty, "int") {
617 return &word[32 - bits / 8..];
618 }
619 if let Some(size) = fixed_bytes_size(ty) {
620 return &word[..size];
621 }
622 word
623}
624
625#[must_use]
627pub fn is_value_type(ty: &str) -> bool {
628 matches!(ty, "bool" | "address" | "address payable")
629 || fixed_bytes_size(ty).is_some()
630 || integer_bits(ty, "uint").is_some()
631 || integer_bits(ty, "int").is_some()
632}
633
634fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
636 let rest = ty.strip_prefix(prefix)?;
637 if rest.is_empty() {
639 return Some(256);
640 }
641 let bits = rest.parse::<usize>().ok()?;
642 (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
643}
644
645fn fixed_bytes_size(ty: &str) -> Option<usize> {
646 let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
647 (1..=32).contains(&size).then_some(size)
648}
649
650fn format_uint_word(word: &str) -> String {
651 let trimmed = word.trim_start_matches('0');
652 if trimmed.is_empty() {
653 return "0".to_owned();
654 }
655 if trimmed.len() <= 32 {
656 return u128::from_str_radix(trimmed, 16)
657 .map(|value| value.to_string())
658 .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
659 }
660 format!("0x{}", trimmed.to_ascii_lowercase())
661}
662
663fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
664 storage
665 .get(&format!("0x{slot:x}"))
666 .or_else(|| storage.get(&format!("{slot:x}")))
667 .or_else(|| storage.get(&format!("0x{slot:064x}")))
668 .or_else(|| storage.get(&format!("{slot:064x}")))
669 .map(|value| normalize_hex(value))
670}
671
672fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
673 let hex = input.trim_start_matches("0x");
674 let start = byte_offset.checked_mul(2)?;
675 let end = start.checked_add(64)?;
676 let word = hex.get(start..end)?;
677 word.bytes()
678 .all(|byte| byte.is_ascii_hexdigit())
679 .then(|| format!("0x{}", word.to_ascii_lowercase()))
680}
681
682fn normalize_hex(value: &str) -> String {
683 let value = value.trim();
684 let hex = value.strip_prefix("0x").unwrap_or(value);
685 format!("0x{}", hex.to_ascii_lowercase())
686}
687
688fn left_pad_word(word: &str) -> String {
689 let word = word.trim_start_matches("0x").to_ascii_lowercase();
690 if word.len() >= 64 {
691 return word;
692 }
693 format!("{:0>64}", word)
694}
695
696fn right_pad_word(word: &str) -> String {
697 let word = word.trim_start_matches("0x").to_ascii_lowercase();
698 if word.len() >= 64 {
699 return word;
700 }
701 format!("{:0<64}", word)
702}
703
704fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
705 let mut line = 1;
706 let mut column = 1;
707 for byte in source.bytes().take(offset.min(source.len())) {
708 if byte == b'\n' {
709 line += 1;
710 column = 1;
711 } else {
712 column += 1;
713 }
714 }
715 SourcePosition { line, column }
716}
717
718const DECLARATION_KEYWORDS: [&str; 5] =
720 ["function", "modifier", "constructor", "fallback", "receive"];
721
722#[must_use]
726pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
727 let mut functions = Vec::new();
728 for keyword in DECLARATION_KEYWORDS {
729 collect_declarations(source_id, source, keyword, &mut functions);
730 }
731 functions.sort_by_key(|function| function.declaration_start);
732 functions
733}
734
735fn collect_declarations(
736 source_id: u64,
737 source: &str,
738 keyword: &str,
739 functions: &mut Vec<SourceFunction>,
740) {
741 let named = matches!(keyword, "function" | "modifier");
742 let mut cursor = 0;
743 while let Some(keyword_start) = find_solidity_keyword(source, keyword, cursor) {
744 let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
745 let name = if named {
746 let Some((name, name_end)) = parse_identifier(source, index) else {
747 cursor = index;
748 continue;
749 };
750 index = skip_ascii_whitespace(source, name_end);
751 name
752 } else {
753 keyword
754 };
755 if source.as_bytes().get(index) != Some(&b'(') {
756 cursor = index;
757 continue;
758 }
759 let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
760 cursor = index + 1;
761 continue;
762 };
763 let params = parse_source_params(&source[index + 1..params_end]);
764 let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
765 cursor = params_end + 1;
766 continue;
767 };
768 if source[params_end + 1..body_start].contains(';') {
771 cursor = params_end + 1;
772 continue;
773 }
774 let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
775 cursor = body_start + 1;
776 continue;
777 };
778
779 functions.push(SourceFunction {
780 source_id,
781 name: name.to_owned(),
782 params,
783 declaration_start: keyword_start as u64,
784 declaration_line: source[..keyword_start]
785 .bytes()
786 .filter(|byte| *byte == b'\n')
787 .count() as u64
788 + 1,
789 body_end: body_end as u64,
790 });
791 cursor = body_end + 1;
792 }
793}
794
795fn parse_source_params(params: &str) -> Vec<SourceParam> {
796 split_top_level_commas(params)
797 .into_iter()
798 .enumerate()
799 .filter_map(|(index, param)| {
800 let param = param.trim();
801 if param.is_empty() {
802 return None;
803 }
804 let mut tokens = param.split_whitespace().collect::<Vec<_>>();
805 let name = tokens
806 .last()
807 .copied()
808 .filter(|token| is_identifier(token))
809 .map_or_else(|| format!("arg{index}"), str::to_owned);
810 if tokens.last().copied() == Some(name.as_str()) && tokens.len() > 1 {
811 tokens.pop();
812 }
813 let location = tokens
814 .iter()
815 .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
816 .map(|token| (*token).to_owned());
817 let ty = tokens
818 .into_iter()
819 .filter(|token| !matches!(*token, "memory" | "calldata" | "storage" | "payable"))
820 .collect::<Vec<_>>()
821 .join(" ");
822 (!ty.is_empty()).then_some(SourceParam { name, ty, location })
823 })
824 .collect()
825}
826
827fn split_top_level_commas(input: &str) -> Vec<&str> {
828 let mut parts = Vec::new();
829 let mut start = 0;
830 let mut depth = 0_i32;
831 for (index, byte) in input.bytes().enumerate() {
832 match byte {
833 b'(' | b'[' => depth += 1,
834 b')' | b']' => depth -= 1,
835 b',' if depth == 0 => {
836 parts.push(&input[start..index]);
837 start = index + 1;
838 }
839 _ => {}
840 }
841 }
842 parts.push(&input[start..]);
843 parts
844}
845
846fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
847 let mut cursor = start;
848 while let Some(relative) = source[cursor..].find(keyword) {
849 let absolute = cursor + relative;
850 let before = absolute
851 .checked_sub(1)
852 .and_then(|index| source.as_bytes().get(index))
853 .copied();
854 let after = source.as_bytes().get(absolute + keyword.len()).copied();
855 if before.is_none_or(|byte| !is_identifier_byte(byte))
856 && after.is_none_or(|byte| !is_identifier_byte(byte))
857 {
858 return Some(absolute);
859 }
860 cursor = absolute + keyword.len();
861 }
862 None
863}
864
865fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
866 let bytes = source.as_bytes();
867 let first = *bytes.get(start)?;
868 if !is_identifier_start_byte(first) {
869 return None;
870 }
871 let mut end = start + 1;
872 while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
873 end += 1;
874 }
875 Some((&source[start..end], end))
876}
877
878fn is_identifier(input: &str) -> bool {
879 let mut bytes = input.bytes();
880 let Some(first) = bytes.next() else {
881 return false;
882 };
883 is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
884}
885
886fn is_identifier_start_byte(byte: u8) -> bool {
887 byte == b'_' || byte.is_ascii_alphabetic()
888}
889
890fn is_identifier_byte(byte: u8) -> bool {
891 is_identifier_start_byte(byte) || byte.is_ascii_digit()
892}
893
894fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
895 while source
896 .as_bytes()
897 .get(index)
898 .is_some_and(u8::is_ascii_whitespace)
899 {
900 index += 1;
901 }
902 index
903}
904
905fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
906 source
907 .as_bytes()
908 .iter()
909 .enumerate()
910 .skip(start)
911 .find_map(|(index, byte)| (*byte == needle).then_some(index))
912}
913
914fn find_matching_delimiter(source: &str, open_index: usize, open: u8, close: u8) -> Option<usize> {
915 let mut depth = 0_i32;
916 for (index, byte) in source.bytes().enumerate().skip(open_index) {
917 if byte == open {
918 depth += 1;
919 } else if byte == close {
920 depth -= 1;
921 if depth == 0 {
922 return Some(index);
923 }
924 }
925 }
926 None
927}
928
929#[cfg(test)]
930mod tests {
931 use std::collections::BTreeMap;
932
933 use serde_json::json;
934 use soldb_core::{TraceStep, TransactionTrace};
935 use soldb_ethdebug::{EthdebugInfo, Instruction, VariableLocation};
936
937 use super::{parse_source_functions, DebugSession, DebugValueStatus};
938
939 #[test]
940 fn builds_source_steps_and_decodes_active_variables() {
941 let source =
942 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
943 let offset = source.find("value = x").expect("source offset") as u64;
944 let trace = sample_trace();
945 let mut sources = BTreeMap::new();
946 sources.insert(0, "Counter.sol".to_owned());
947 let mut source_contents = BTreeMap::new();
948 source_contents.insert(0, source.to_owned());
949 let mut variable_locations = BTreeMap::new();
950 variable_locations.insert(
951 0,
952 vec![
953 VariableLocation {
954 name: "x".to_owned(),
955 ty: "uint256".to_owned(),
956 location_type: "stack".to_owned(),
957 offset: 0,
958 pc_range: (0, 1),
959 },
960 VariableLocation {
961 name: "sender".to_owned(),
962 ty: "address".to_owned(),
963 location_type: "calldata".to_owned(),
964 offset: 4,
965 pc_range: (0, 1),
966 },
967 ],
968 );
969 let info = EthdebugInfo {
970 compilation: json!({}),
971 contract_name: "Counter".to_owned(),
972 environment: "runtime".to_owned(),
973 instructions: vec![Instruction {
974 offset: 0,
975 operation: json!({"mnemonic": "PUSH1"}),
976 context: Some(json!({
977 "code": {
978 "source": {"id": 0},
979 "range": {"offset": offset, "length": 9}
980 }
981 })),
982 }],
983 sources,
984 variable_locations,
985 };
986
987 let session = DebugSession::with_ethdebug(trace, info, source_contents);
988 let step = session.step(0).expect("debug step");
989 assert_eq!(step.source.as_ref().expect("source").line, 3);
990 assert_eq!(step.function.as_ref().expect("function").name, "set");
991 assert_eq!(
992 step.snapshot
993 .stack
994 .iter()
995 .map(|word| &**word)
996 .collect::<Vec<_>>(),
997 ["0x2a"]
998 );
999 assert_eq!(step.variables[0].name, "x");
1000 assert_eq!(step.variables[0].value.display, "42");
1001 assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1002 assert_eq!(
1003 step.variables[1].value.display,
1004 "0x1111111111111111111111111111111111111111"
1005 );
1006 }
1007
1008 #[test]
1009 fn keeps_raw_values_when_type_is_dynamic_or_location_is_unknown() {
1010 let trace = sample_trace();
1011 let mut variable_locations = BTreeMap::new();
1012 variable_locations.insert(
1013 0,
1014 vec![
1015 VariableLocation {
1016 name: "name".to_owned(),
1017 ty: "string".to_owned(),
1018 location_type: "stack".to_owned(),
1019 offset: 0,
1020 pc_range: (0, 0),
1021 },
1022 VariableLocation {
1023 name: "missing".to_owned(),
1024 ty: "uint256".to_owned(),
1025 location_type: "stack".to_owned(),
1026 offset: 99,
1027 pc_range: (0, 0),
1028 },
1029 ],
1030 );
1031 let info = EthdebugInfo {
1032 compilation: json!({}),
1033 contract_name: "Counter".to_owned(),
1034 environment: "runtime".to_owned(),
1035 instructions: Vec::new(),
1036 sources: BTreeMap::new(),
1037 variable_locations,
1038 };
1039
1040 let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1041 let step = session.step(0).expect("debug step");
1042 assert_eq!(step.variables[0].value.status, DebugValueStatus::Raw);
1043 assert_eq!(step.variables[0].value.display, "0x2a");
1044 assert_eq!(
1045 step.variables[1].value.status,
1046 DebugValueStatus::Unavailable
1047 );
1048 }
1049
1050 #[test]
1051 fn attaching_ethdebug_matches_constructing_with_it_and_replaces_prior_info() {
1052 let source =
1053 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1054 let offset = source.find("value = x").expect("source offset") as u64;
1055 let info = EthdebugInfo {
1056 compilation: json!({}),
1057 contract_name: "Counter".to_owned(),
1058 environment: "runtime".to_owned(),
1059 instructions: vec![Instruction {
1060 offset: 0,
1061 operation: json!({"mnemonic": "PUSH1"}),
1062 context: Some(json!({
1063 "code": {"source": {"id": 0}, "range": {"offset": offset, "length": 9}}
1064 })),
1065 }],
1066 sources: BTreeMap::from([(0, "Counter.sol".to_owned())]),
1067 variable_locations: BTreeMap::new(),
1068 };
1069 let source_contents = BTreeMap::from([(0, source.to_owned())]);
1070
1071 let constructed =
1072 DebugSession::with_ethdebug(sample_trace(), info.clone(), source_contents.clone());
1073 let mut attached = DebugSession::new(sample_trace());
1074 assert!(attached.ethdebug.is_none());
1075 assert!(attached.functions.is_empty());
1076 attached.attach_ethdebug(info, source_contents);
1077
1078 assert_eq!(attached, constructed);
1079 assert_eq!(attached.functions.len(), 1);
1080 assert_eq!(attached.functions[0].name, "set");
1081 assert_eq!(
1082 attached
1083 .step(0)
1084 .and_then(|step| step.source)
1085 .map(|span| span.line),
1086 Some(3)
1087 );
1088
1089 let other = EthdebugInfo {
1091 compilation: json!({}),
1092 contract_name: "Other".to_owned(),
1093 environment: "runtime".to_owned(),
1094 instructions: Vec::new(),
1095 sources: BTreeMap::new(),
1096 variable_locations: BTreeMap::new(),
1097 };
1098 attached.attach_ethdebug(other, BTreeMap::new());
1099 assert_eq!(
1100 attached
1101 .ethdebug
1102 .as_ref()
1103 .map(|info| info.contract_name.as_str()),
1104 Some("Other")
1105 );
1106 assert!(attached.functions.is_empty());
1107 assert!(attached.source_contents.is_empty());
1108 assert_eq!(attached.step(0).and_then(|step| step.source), None);
1109 assert_eq!(attached.trace, sample_trace());
1110 }
1111
1112 #[test]
1113 fn parses_source_functions_with_tuple_and_array_params() {
1114 let functions = parse_source_functions(
1115 7,
1116 "contract C { function submit(Person memory p, uint256[2] memory xs) public {} }",
1117 );
1118 assert_eq!(functions[0].source_id, 7);
1119 assert_eq!(functions[0].name, "submit");
1120 assert_eq!(functions[0].params[0].name, "p");
1121 assert_eq!(functions[0].params[0].ty, "Person");
1122 assert_eq!(functions[0].params[1].name, "xs");
1123 assert_eq!(functions[0].params[1].ty, "uint256[2]");
1124 }
1125
1126 fn sample_trace() -> TransactionTrace {
1127 TransactionTrace {
1128 tx_hash: Some("0xabc".to_owned()),
1129 from_addr: "0x1".to_owned(),
1130 to_addr: Some("0x2".to_owned()),
1131 value: "0".to_owned(),
1132 input_data: format!(
1133 "0x12345678{}",
1134 "0000000000000000000000001111111111111111111111111111111111111111"
1135 ),
1136 gas_used: 1,
1137 output: "0x".to_owned(),
1138 success: true,
1139 error: None,
1140 debug_trace_available: true,
1141 contract_address: None,
1142 backend: Some("debug-rpc".to_owned()),
1143 capabilities: Default::default(),
1144 artifacts: Default::default(),
1145 steps: vec![TraceStep {
1146 pc: 0,
1147 op: "PUSH1".into(),
1148 gas: 1,
1149 gas_cost: 0,
1150 depth: 0,
1151 stack: vec!["0x2a".into()],
1152 memory: Some(format!("{}{}", "00".repeat(32), "2a".repeat(32))),
1153 storage: Some(BTreeMap::from([("0x0".to_owned(), "0x2a".to_owned())])),
1154 error: None,
1155 snapshot: Default::default(),
1156 }],
1157 }
1158 }
1159 fn memory_image(words: &[&str]) -> String {
1162 words
1163 .iter()
1164 .map(|word| format!("{:0>64}", word.trim_start_matches("0x")))
1165 .collect()
1166 }
1167
1168 #[test]
1169 fn reads_arguments_that_live_in_memory() {
1170 let memory = memory_image(&[
1172 "5",
1173 "68656c6c6f000000000000000000000000000000000000000000000000000000",
1174 "3",
1175 "7",
1176 "8",
1177 "9",
1178 ]);
1179 let string_at = |offset: usize| super::read_memory_value(&memory, offset, "string");
1180 assert_eq!(string_at(0).as_deref(), Some("\"hello\""));
1181 assert_eq!(
1182 super::read_memory_value(&memory, 0, "bytes").as_deref(),
1183 Some("0x68656c6c6f")
1184 );
1185 assert_eq!(
1186 super::read_memory_value(&memory, 64, "uint256[]").as_deref(),
1187 Some("[7, 8, 9]")
1188 );
1189 assert_eq!(
1191 super::read_memory_value(&memory, 96, "uint256[2]").as_deref(),
1192 Some("[7, 8]")
1193 );
1194 assert_eq!(super::read_memory_value(&memory, 4096, "string"), None);
1197 assert_eq!(super::read_memory_value(&memory, 64, "string[]"), None);
1198 }
1199
1200 #[test]
1201 fn a_parameter_is_readable_only_when_its_width_is_known() {
1202 let param = |ty: &str, location: Option<&str>| super::SourceParam {
1203 name: "x".to_owned(),
1204 ty: ty.to_owned(),
1205 location: location.map(str::to_owned),
1206 };
1207 assert!(super::readable_parameter(¶m("uint256", None)));
1208 assert!(super::readable_parameter(¶m("string", Some("memory"))));
1209 assert!(super::readable_parameter(¶m("Config", Some("storage"))));
1210 assert!(super::readable_parameter(¶m(
1212 "uint256[2]",
1213 Some("calldata")
1214 )));
1215 assert!(!super::readable_parameter(¶m(
1217 "bytes",
1218 Some("calldata")
1219 )));
1220 assert!(!super::readable_parameter(¶m(
1221 "uint256[]",
1222 Some("calldata")
1223 )));
1224 assert!(!super::readable_parameter(¶m("Config", None)));
1226 }
1227}