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
23use crate::decode::ValueReader;
24
25pub mod condition;
26pub mod debug_diff;
27mod decode;
28mod locals;
29pub mod state;
30pub mod stepping;
31mod types;
32
33pub use condition::{Condition, ConditionContext, Evaluation};
34pub use debug_diff::{
35 capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
36 DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
37 DebugTraceSummary,
38};
39pub use soldb_ethdebug::{CodeGenerator, StorageLayout};
40pub use state::{
41 short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
42 StateVariable, StorageTape, StorageWords,
43};
44pub use stepping::{
45 address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
46 Frame, FunctionId, InferredVariable, JumpMarker, LineKey, LocalsStatus, ResolvedFunction,
47 ResolvedLine, SourceListing, StepLocation, StepMap, VariableKind, INFERRED_LOCALS_NOTE,
48 INFERRED_LOCALS_WARNING,
49};
50pub use types::{SourceTypes, StructMember};
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct DebugSession {
54 pub trace: TransactionTrace,
55 pub ethdebug: Option<EthdebugInfo>,
56 #[serde(default)]
57 pub source_contents: BTreeMap<u64, String>,
58 #[serde(default)]
59 pub functions: Vec<SourceFunction>,
60}
61
62impl DebugSession {
63 #[must_use]
64 pub fn new(trace: TransactionTrace) -> Self {
65 Self {
66 trace,
67 ethdebug: None,
68 source_contents: BTreeMap::new(),
69 functions: Vec::new(),
70 }
71 }
72
73 #[must_use]
74 pub fn with_ethdebug(
75 trace: TransactionTrace,
76 ethdebug: EthdebugInfo,
77 source_contents: BTreeMap<u64, String>,
78 ) -> Self {
79 let mut session = Self::new(trace);
80 session.attach_ethdebug(ethdebug, source_contents);
81 session
82 }
83
84 pub fn attach_ethdebug(
91 &mut self,
92 ethdebug: EthdebugInfo,
93 source_contents: BTreeMap<u64, String>,
94 ) {
95 self.functions = source_contents
96 .iter()
97 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
98 .collect();
99 self.ethdebug = Some(ethdebug);
100 self.source_contents = source_contents;
101 }
102
103 #[must_use]
104 pub fn step(&self, step_index: usize) -> Option<DebugStep> {
105 let step = self.trace.steps.get(step_index)?;
106 Some(DebugStep {
107 index: step_index,
108 pc: step.pc,
109 op: step.op.to_string(),
110 gas: step.gas,
111 gas_cost: step.gas_cost,
112 depth: step.depth,
113 source: self.source_span(step.pc),
114 function: self.function_at_pc(step.pc).cloned(),
115 snapshot: step.normalized_snapshot(),
116 variables: self.variables_at_step(step),
117 })
118 }
119
120 #[must_use]
121 pub fn steps(&self) -> Vec<DebugStep> {
122 (0..self.trace.steps.len())
123 .filter_map(|step_index| self.step(step_index))
124 .collect()
125 }
126
127 #[must_use]
128 pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
129 let ethdebug = self.ethdebug.as_ref()?;
130 let instruction = ethdebug.instruction_at_pc(pc)?;
131 let location = instruction.source_location()?;
132 let path = ethdebug.sources.get(&location.source_id)?.clone();
133 let position = self
134 .source_contents
135 .get(&location.source_id)
136 .map(|source| line_column_for_offset(source, location.offset as usize));
137 Some(SourceSpan {
138 source_id: location.source_id,
139 path,
140 offset: location.offset,
141 length: location.length,
142 line: position.map_or(0, |position| position.line),
143 column: position.map_or(0, |position| position.column),
144 })
145 }
146
147 #[must_use]
148 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
149 let location = self
150 .ethdebug
151 .as_ref()?
152 .instruction_at_pc(pc)?
153 .source_location()?;
154 self.functions
155 .iter()
156 .filter(|function| {
157 function.source_id == location.source_id
158 && function.declaration_start <= location.offset
159 && location.offset <= function.body_end
160 })
161 .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
162 }
163
164 #[must_use]
165 pub fn variables_at_step(&self, step: &TraceStep) -> Vec<DebugVariable> {
166 let Some(ethdebug) = &self.ethdebug else {
167 return Vec::new();
168 };
169 variables_for_step(&self.trace, ethdebug, step)
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct DebugStep {
175 pub index: usize,
176 pub pc: u64,
177 pub op: String,
178 pub gas: u64,
179 pub gas_cost: u64,
180 pub depth: u64,
181 pub source: Option<SourceSpan>,
182 pub function: Option<SourceFunction>,
183 pub snapshot: StepSnapshot,
184 pub variables: Vec<DebugVariable>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct SourceSpan {
189 pub source_id: u64,
190 pub path: String,
191 pub offset: u64,
192 pub length: u64,
193 pub line: u64,
194 pub column: u64,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub struct SourceFunction {
199 pub source_id: u64,
200 pub name: String,
203 pub params: Vec<SourceParam>,
204 pub declaration_start: u64,
205 #[serde(default)]
207 pub declaration_line: u64,
208 #[serde(default)]
210 pub body_start: u64,
211 pub body_end: u64,
212 #[serde(default)]
214 pub returns: Vec<SourceLocal>,
215 #[serde(default)]
217 pub locals: Vec<SourceLocal>,
218 #[serde(default)]
221 pub has_modifiers: bool,
222 #[serde(default)]
224 pub modifiers: Vec<String>,
225 #[serde(default)]
227 pub placeholder: Option<u64>,
228}
229
230#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
232pub struct ByteRange {
233 pub start: u64,
234 pub end: u64,
235}
236
237impl ByteRange {
238 #[must_use]
239 pub const fn contains(self, offset: u64) -> bool {
240 self.start <= offset && offset < self.end
241 }
242
243 #[must_use]
245 pub const fn covers(self, offset: u64, length: u64) -> bool {
246 self.start <= offset && offset.saturating_add(length) <= self.end
247 }
248
249 #[must_use]
251 pub const fn encloses(self, other: Self) -> bool {
252 self.start <= other.start && other.end <= self.end
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct SourceLocal {
263 pub name: String,
264 pub ty: String,
266 #[serde(default)]
268 pub location: Option<String>,
269 pub declaration: ByteRange,
270 pub statement: ByteRange,
272 pub position: usize,
274 pub scope: ByteRange,
275}
276
277impl SourceLocal {
278 #[must_use]
280 pub fn declared_type(&self) -> String {
281 match &self.location {
282 Some(location) => format!("{} {location}", self.ty),
283 None => self.ty.clone(),
284 }
285 }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct SourceParam {
290 pub name: String,
291 pub ty: String,
293 #[serde(default)]
296 pub location: Option<String>,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub struct FrameArgument {
302 pub name: String,
303 pub ty: String,
304 pub value: DebugValue,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct DebugVariable {
309 pub name: String,
310 pub ty: String,
311 pub location: DebugLocation,
312 pub value: DebugValue,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316pub struct DebugLocation {
317 pub kind: String,
318 pub offset: u64,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322pub struct DebugValue {
323 pub display: String,
324 pub raw: Option<String>,
325 pub status: DebugValueStatus,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum DebugValueStatus {
331 Decoded,
332 Raw,
333 Unavailable,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337struct SourcePosition {
338 line: u64,
339 column: u64,
340}
341
342#[must_use]
343pub fn variables_for_step(
344 trace: &TransactionTrace,
345 ethdebug: &EthdebugInfo,
346 step: &TraceStep,
347) -> Vec<DebugVariable> {
348 ethdebug
349 .variables_at_pc(step.pc)
350 .into_iter()
351 .map(|variable| decode_variable(trace, step, variable))
352 .collect()
353}
354
355fn decode_variable(
356 trace: &TransactionTrace,
357 step: &TraceStep,
358 variable: &VariableLocation,
359) -> DebugVariable {
360 let raw = raw_value_for_location(trace, step, variable);
361 let value = raw.map_or_else(
362 || DebugValue {
363 display: "<unavailable>".to_owned(),
364 raw: None,
365 status: DebugValueStatus::Unavailable,
366 },
367 |raw| decode_debug_value(&raw, &variable.ty),
368 );
369 DebugVariable {
370 name: variable.name.clone(),
371 ty: variable.ty.clone(),
372 location: DebugLocation {
373 kind: variable.location_type.clone(),
374 offset: variable.offset,
375 },
376 value,
377 }
378}
379
380fn raw_value_for_location(
381 trace: &TransactionTrace,
382 step: &TraceStep,
383 variable: &VariableLocation,
384) -> Option<String> {
385 let snapshot = step.snapshot_ref();
386 match variable.location_type.as_str() {
387 "stack" => snapshot
388 .stack
389 .get(variable.offset as usize)
390 .map(|value| normalize_hex(value)),
391 "memory" => word_from_hex_bytes(snapshot.memory?, variable.offset as usize),
392 "calldata" => word_from_hex_bytes(&trace.input_data, variable.offset as usize),
393 "storage" => storage_value(snapshot.storage, variable.offset),
394 _ => None,
395 }
396}
397
398pub(crate) fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
399 let ty = ty.trim();
400 let normalized = normalize_hex(raw);
401 let word = normalized.trim_start_matches("0x");
402 let decoded = decode_static_word(word, ty);
403 match decoded {
404 Some(display) => DebugValue {
405 display,
406 raw: Some(normalized),
407 status: DebugValueStatus::Decoded,
408 },
409 None => DebugValue {
410 display: normalized.clone(),
411 raw: Some(normalized),
412 status: DebugValueStatus::Raw,
413 },
414 }
415}
416
417pub(crate) fn decode_static_word(word: &str, ty: &str) -> Option<String> {
418 if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
419 return None;
420 }
421 if ty.starts_with("uint") {
422 return Some(format_uint_word(word));
423 }
424 if ty.starts_with("int") {
425 return Some(format_int_word(word));
426 }
427 if ty == "address" {
428 let padded = left_pad_word(word);
429 return Some(
430 format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
431 );
432 }
433 if ty == "bool" {
434 let trimmed = word.trim_start_matches('0');
435 return match trimmed {
436 "" | "0" => Some("false".to_owned()),
437 "1" => Some("true".to_owned()),
438 _ => None,
439 };
440 }
441 if ty == "bytes32" {
442 return Some(normalize_hex(word));
443 }
444 if let Some(size) = fixed_bytes_size(ty) {
445 let padded = right_pad_word(word);
446 return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
447 }
448 None
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum ArgumentOrder {
456 FirstOnTop,
459 LastOnTop,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum ArgumentLayout {
467 Ordered(ArgumentOrder),
469 TopWords,
472}
473
474impl ArgumentOrder {
475 #[must_use]
478 fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
479 match self {
480 Self::FirstOnTop => stack_len - 1 - index,
481 Self::LastOnTop => stack_len - count + index,
482 }
483 }
484}
485
486#[derive(Debug, Clone, Copy, Default)]
489pub struct FrameState<'a> {
490 pub stack: &'a [StackWord],
491 pub memory: Option<&'a str>,
494}
495
496#[must_use]
505pub fn decode_arguments(
506 params: &[SourceParam],
507 state: FrameState<'_>,
508 layout: ArgumentLayout,
509 types: &SourceTypes,
510 scope: Option<&str>,
511) -> Vec<FrameArgument> {
512 let stack = state.stack;
513 if params.is_empty() || stack.len() < params.len() {
514 return Vec::new();
515 }
516 if !params.iter().all(readable_parameter) {
517 return Vec::new();
518 }
519 let order = match layout {
521 ArgumentLayout::Ordered(order) => order,
522 ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
523 ArgumentLayout::TopWords => return Vec::new(),
524 };
525 params
526 .iter()
527 .enumerate()
528 .map(|(index, param)| {
529 let word = &stack[order.word_index(index, params.len(), stack.len())];
530 FrameArgument {
531 name: param.name.clone(),
532 ty: param.ty.clone(),
533 value: argument_value(param, word, state.memory, types, scope),
534 }
535 })
536 .collect()
537}
538
539fn argument_value(
540 param: &SourceParam,
541 word: &str,
542 memory: Option<&str>,
543 types: &SourceTypes,
544 scope: Option<&str>,
545) -> DebugValue {
546 let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
547 return DebugValue {
548 display: "<unreadable stack word>".to_owned(),
549 raw: Some(word.to_owned()),
550 status: DebugValueStatus::Unavailable,
551 };
552 };
553 let reader = ValueReader {
554 memory,
555 calldata: "",
556 storage: None,
557 layout: None,
558 types,
559 scope,
560 };
561 let raw = Some(short_hex(&parsed));
562 match param.location.as_deref() {
563 Some("memory") => reader.variable(&format!("{} memory", param.ty), &[word]),
564 Some(location) => DebugValue {
565 display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
566 raw,
567 status: DebugValueStatus::Raw,
568 },
569 None => DebugValue {
570 display: decode_value(value_bytes(&parsed, ¶m.ty), ¶m.ty),
571 raw,
572 status: DebugValueStatus::Decoded,
573 },
574 }
575}
576
577pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
579 match param.location.as_deref() {
580 Some("calldata") => !is_dynamic_type(¶m.ty),
582 Some(_) => true,
583 None => is_value_type(¶m.ty),
584 }
585}
586
587fn is_dynamic_type(ty: &str) -> bool {
589 ty == "string" || ty == "bytes" || ty.ends_with("[]")
590}
591
592pub(crate) fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
595 let inner = ty.strip_suffix(']')?;
596 let open = inner.rfind('[')?;
597 let element = inner[..open].trim();
598 let length = &inner[open + 1..];
599 if length.is_empty() {
600 return Some((element, None));
601 }
602 Some((element, Some(length.parse::<usize>().ok()?)))
603}
604
605pub(crate) fn memory_word(memory: &str, offset: usize) -> Option<Word> {
607 let bytes = memory_bytes(memory, offset, 32)?;
608 let mut word = [0_u8; 32];
609 word.copy_from_slice(&bytes);
610 Some(word)
611}
612
613pub(crate) fn memory_bytes(memory: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
615 let start = offset.checked_mul(2)?;
616 let end = start.checked_add(length.checked_mul(2)?)?;
617 let digits = memory.get(start..end)?;
618 if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
619 return None;
620 }
621 Some(
622 digits
623 .as_bytes()
624 .chunks(2)
625 .map(|pair| {
626 u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
627 })
628 .collect(),
629 )
630}
631
632pub(crate) fn word_as_usize(word: &Word) -> Option<usize> {
634 if word[..24].iter().any(|byte| *byte != 0) {
635 return None;
636 }
637 usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
638}
639
640pub(crate) fn hex_of(bytes: &[u8]) -> String {
641 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
642}
643
644pub(crate) fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
648 if let Some(bits) = integer_bits(ty, "int") {
649 return &word[32 - bits / 8..];
650 }
651 if let Some(size) = fixed_bytes_size(ty) {
652 return &word[..size];
653 }
654 word
655}
656
657#[must_use]
659pub fn is_value_type(ty: &str) -> bool {
660 matches!(ty, "bool" | "address" | "address payable")
661 || fixed_bytes_size(ty).is_some()
662 || integer_bits(ty, "uint").is_some()
663 || integer_bits(ty, "int").is_some()
664}
665
666fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
668 let rest = ty.strip_prefix(prefix)?;
669 if rest.is_empty() {
671 return Some(256);
672 }
673 let bits = rest.parse::<usize>().ok()?;
674 (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
675}
676
677fn fixed_bytes_size(ty: &str) -> Option<usize> {
678 let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
679 (1..=32).contains(&size).then_some(size)
680}
681
682fn format_uint_word(word: &str) -> String {
683 let trimmed = word.trim_start_matches('0');
684 if trimmed.is_empty() {
685 return "0".to_owned();
686 }
687 if trimmed.len() <= 32 {
688 return u128::from_str_radix(trimmed, 16)
689 .map(|value| value.to_string())
690 .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
691 }
692 format!("0x{}", trimmed.to_ascii_lowercase())
693}
694
695fn format_int_word(word: &str) -> String {
697 let padded = left_pad_word(word);
698 let Ok(bytes) = (0..32)
699 .map(|index| u8::from_str_radix(&padded[index * 2..index * 2 + 2], 16))
700 .collect::<Result<Vec<_>, _>>()
701 else {
702 return format!("0x{}", word.to_ascii_lowercase());
703 };
704 if bytes[0] & 0x80 == 0 {
705 return format_uint_word(word);
706 }
707 let mut magnitude = bytes.iter().map(|byte| !byte).collect::<Vec<_>>();
709 for byte in magnitude.iter_mut().rev() {
710 let (sum, carry) = byte.overflowing_add(1);
711 *byte = sum;
712 if !carry {
713 break;
714 }
715 }
716 let hex = magnitude
717 .iter()
718 .map(|byte| format!("{byte:02x}"))
719 .collect::<String>();
720 let trimmed = hex.trim_start_matches('0');
721 if trimmed.len() <= 32 {
722 if let Ok(value) = u128::from_str_radix(trimmed, 16) {
723 return format!("-{value}");
724 }
725 }
726 format!("-0x{trimmed}")
727}
728
729fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
730 storage
731 .get(&format!("0x{slot:x}"))
732 .or_else(|| storage.get(&format!("{slot:x}")))
733 .or_else(|| storage.get(&format!("0x{slot:064x}")))
734 .or_else(|| storage.get(&format!("{slot:064x}")))
735 .map(|value| normalize_hex(value))
736}
737
738fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
739 let hex = input.trim_start_matches("0x");
740 let start = byte_offset.checked_mul(2)?;
741 let end = start.checked_add(64)?;
742 let word = hex.get(start..end)?;
743 word.bytes()
744 .all(|byte| byte.is_ascii_hexdigit())
745 .then(|| format!("0x{}", word.to_ascii_lowercase()))
746}
747
748fn normalize_hex(value: &str) -> String {
749 let value = value.trim();
750 let hex = value.strip_prefix("0x").unwrap_or(value);
751 format!("0x{}", hex.to_ascii_lowercase())
752}
753
754fn left_pad_word(word: &str) -> String {
755 let word = word.trim_start_matches("0x").to_ascii_lowercase();
756 if word.len() >= 64 {
757 return word;
758 }
759 format!("{:0>64}", word)
760}
761
762fn right_pad_word(word: &str) -> String {
763 let word = word.trim_start_matches("0x").to_ascii_lowercase();
764 if word.len() >= 64 {
765 return word;
766 }
767 format!("{:0<64}", word)
768}
769
770fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
771 let mut line = 1;
772 let mut column = 1;
773 for byte in source.bytes().take(offset.min(source.len())) {
774 if byte == b'\n' {
775 line += 1;
776 column = 1;
777 } else {
778 column += 1;
779 }
780 }
781 SourcePosition { line, column }
782}
783
784const DECLARATION_KEYWORDS: [&str; 5] =
786 ["function", "modifier", "constructor", "fallback", "receive"];
787
788#[must_use]
792pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
793 let mut functions = Vec::new();
794 for keyword in DECLARATION_KEYWORDS {
795 collect_declarations(source_id, source, keyword, &mut functions);
796 }
797 functions.sort_by_key(|function| function.declaration_start);
798 functions
799}
800
801fn collect_declarations(
802 source_id: u64,
803 source: &str,
804 keyword: &str,
805 functions: &mut Vec<SourceFunction>,
806) {
807 let named = matches!(keyword, "function" | "modifier");
808 let mut cursor = 0;
809 while let Some(keyword_start) = find_solidity_keyword(source, keyword, cursor) {
810 let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
811 let name = if named {
812 let Some((name, name_end)) = parse_identifier(source, index) else {
813 cursor = index;
814 continue;
815 };
816 index = skip_ascii_whitespace(source, name_end);
817 name
818 } else {
819 keyword
820 };
821 if source.as_bytes().get(index) != Some(&b'(') {
822 cursor = index;
823 continue;
824 }
825 let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
826 cursor = index + 1;
827 continue;
828 };
829 let params = parse_source_params(&source[index + 1..params_end]);
830 let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
831 cursor = params_end + 1;
832 continue;
833 };
834 if source[params_end + 1..body_start].contains(';') {
837 cursor = params_end + 1;
838 continue;
839 }
840 let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
841 cursor = body_start + 1;
842 continue;
843 };
844 let (returns, modifiers) = parse_header(source, params_end + 1, body_start);
845 let modifiers = if keyword == "modifier" {
846 Vec::new()
847 } else {
848 modifiers
849 };
850 let returns = returns
851 .into_iter()
852 .map(|variable| SourceLocal {
853 name: variable.name,
854 ty: variable.ty,
855 location: variable.location,
856 declaration: variable.span,
857 statement: variable.span,
858 position: 0,
859 scope: ByteRange {
860 start: body_start as u64,
861 end: body_end as u64,
862 },
863 })
864 .collect();
865
866 functions.push(SourceFunction {
867 source_id,
868 name: name.to_owned(),
869 params,
870 declaration_start: keyword_start as u64,
871 declaration_line: source[..keyword_start]
872 .bytes()
873 .filter(|byte| *byte == b'\n')
874 .count() as u64
875 + 1,
876 body_start: body_start as u64,
877 body_end: body_end as u64,
878 returns,
879 locals: locals::scan_locals(source, body_start, body_end),
880 has_modifiers: !modifiers.is_empty(),
881 modifiers,
882 placeholder: (keyword == "modifier")
883 .then(|| locals::find_placeholder(source, body_start, body_end))
884 .flatten(),
885 });
886 cursor = body_end + 1;
887 }
888}
889
890const HEADER_KEYWORDS: [&str; 11] = [
893 "public", "private", "internal", "external", "pure", "view", "payable", "virtual", "override",
894 "returns", "constant",
895];
896
897fn parse_header(source: &str, start: usize, end: usize) -> (Vec<locals::Declared>, Vec<String>) {
904 let bytes = source.as_bytes();
905 let mut returns = Vec::new();
906 let mut modifiers = Vec::new();
907 let mut index = start;
908 while index < end {
909 index = locals::skip_trivia(source, index);
910 if index >= end {
911 break;
912 }
913 let Some((word, word_end)) = parse_identifier(source, index) else {
914 index += 1;
915 continue;
916 };
917 let after = locals::skip_trivia(source, word_end);
918 let arguments = (bytes.get(after) == Some(&b'('))
919 .then(|| find_matching_delimiter(source, after, b'(', b')'))
920 .flatten()
921 .filter(|close| *close < end);
922 if word == "returns" {
923 if let Some(close) = arguments {
924 returns = parse_return_list(source, after, close);
925 }
926 } else if !HEADER_KEYWORDS.contains(&word) {
927 modifiers.push(word.to_owned());
928 }
929 index = arguments.map_or(word_end, |close| close + 1);
930 }
931 (returns, modifiers)
932}
933
934fn parse_return_list(source: &str, open: usize, close: usize) -> Vec<locals::Declared> {
936 let mut returns = Vec::new();
937 let mut start = open + 1;
938 let mut depth = 0_i32;
939 for index in open + 1..=close {
940 match source.as_bytes()[index] {
941 b'(' | b'[' => depth += 1,
942 b')' | b']' if depth > 0 => depth -= 1,
943 b',' | b')' => {
944 let component = source[start..index].trim();
945 if !component.is_empty() {
946 let component_start = start
947 + (source[start..index].len() - source[start..index].trim_start().len());
948 let component_start = locals::skip_trivia(source, component_start);
949 let component_end = component_start + component.len();
950 if let Some(declared) =
951 locals::parse_return(source, component_start, component_end, returns.len())
952 {
953 returns.push(declared);
954 }
955 }
956 start = index + 1;
957 }
958 _ => {}
959 }
960 }
961 returns
962}
963
964const PARAM_KEYWORDS: [&str; 4] = ["memory", "calldata", "storage", "payable"];
966
967fn parse_source_params(params: &str) -> Vec<SourceParam> {
968 let params = strip_param_comments(params);
969 split_top_level_commas(¶ms)
970 .into_iter()
971 .enumerate()
972 .filter_map(|(index, param)| {
973 let param = param.trim();
974 if param.is_empty() {
975 return None;
976 }
977 let mut tokens = param.split_whitespace().collect::<Vec<_>>();
978 let trailing = tokens.last().copied();
984 let name = match trailing {
985 Some(token)
986 if tokens.len() > 1
987 && is_identifier(token)
988 && !PARAM_KEYWORDS.contains(&token) =>
989 {
990 tokens.pop();
991 token.to_owned()
992 }
993 _ => format!("arg{index}"),
994 };
995 let location = tokens
996 .iter()
997 .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
998 .map(|token| (*token).to_owned());
999 let ty = tokens
1000 .into_iter()
1001 .filter(|token| !PARAM_KEYWORDS.contains(token))
1002 .collect::<Vec<_>>()
1003 .join(" ");
1004 (!ty.is_empty()).then_some(SourceParam { name, ty, location })
1005 })
1006 .collect()
1007}
1008
1009fn strip_param_comments(params: &str) -> String {
1027 let mut output = String::with_capacity(params.len());
1028 let mut chars = params.chars().peekable();
1029 while let Some(character) = chars.next() {
1030 if character == '/' {
1031 match chars.peek() {
1032 Some('/') => {
1033 for next in chars.by_ref() {
1036 if next == '\n' {
1037 output.push('\n');
1038 break;
1039 }
1040 }
1041 continue;
1042 }
1043 Some('*') => {
1044 chars.next();
1045 let mut previous = None;
1046 for next in chars.by_ref() {
1047 if previous == Some('*') && next == '/' {
1048 break;
1049 }
1050 previous = Some(next);
1051 }
1052 output.push(' ');
1055 continue;
1056 }
1057 _ => {}
1058 }
1059 }
1060 output.push(character);
1061 }
1062 output
1063}
1064
1065fn split_top_level_commas(input: &str) -> Vec<&str> {
1066 let mut parts = Vec::new();
1067 let mut start = 0;
1068 let mut depth = 0_i32;
1069 for (index, byte) in input.bytes().enumerate() {
1070 match byte {
1071 b'(' | b'[' => depth += 1,
1072 b')' | b']' => depth -= 1,
1073 b',' if depth == 0 => {
1074 parts.push(&input[start..index]);
1075 start = index + 1;
1076 }
1077 _ => {}
1078 }
1079 }
1080 parts.push(&input[start..]);
1081 parts
1082}
1083
1084pub(crate) fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
1085 let mut cursor = start;
1086 while let Some(relative) = source[cursor..].find(keyword) {
1087 let absolute = cursor + relative;
1088 let before = absolute
1089 .checked_sub(1)
1090 .and_then(|index| source.as_bytes().get(index))
1091 .copied();
1092 let after = source.as_bytes().get(absolute + keyword.len()).copied();
1093 if before.is_none_or(|byte| !is_identifier_byte(byte))
1094 && after.is_none_or(|byte| !is_identifier_byte(byte))
1095 {
1096 return Some(absolute);
1097 }
1098 cursor = absolute + keyword.len();
1099 }
1100 None
1101}
1102
1103pub(crate) fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
1104 let bytes = source.as_bytes();
1105 let first = *bytes.get(start)?;
1106 if !is_identifier_start_byte(first) {
1107 return None;
1108 }
1109 let mut end = start + 1;
1110 while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
1111 end += 1;
1112 }
1113 Some((&source[start..end], end))
1114}
1115
1116pub(crate) fn is_identifier(input: &str) -> bool {
1117 let mut bytes = input.bytes();
1118 let Some(first) = bytes.next() else {
1119 return false;
1120 };
1121 is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
1122}
1123
1124fn is_identifier_start_byte(byte: u8) -> bool {
1125 byte == b'_' || byte.is_ascii_alphabetic()
1126}
1127
1128pub(crate) fn is_identifier_byte(byte: u8) -> bool {
1129 is_identifier_start_byte(byte) || byte.is_ascii_digit()
1130}
1131
1132pub(crate) fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
1133 while source
1134 .as_bytes()
1135 .get(index)
1136 .is_some_and(u8::is_ascii_whitespace)
1137 {
1138 index += 1;
1139 }
1140 index
1141}
1142
1143fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
1144 source
1145 .as_bytes()
1146 .iter()
1147 .enumerate()
1148 .skip(start)
1149 .find_map(|(index, byte)| (*byte == needle).then_some(index))
1150}
1151
1152pub(crate) fn find_matching_delimiter(
1153 source: &str,
1154 open_index: usize,
1155 open: u8,
1156 close: u8,
1157) -> Option<usize> {
1158 let mut depth = 0_i32;
1159 for (index, byte) in source.bytes().enumerate().skip(open_index) {
1160 if byte == open {
1161 depth += 1;
1162 } else if byte == close {
1163 depth -= 1;
1164 if depth == 0 {
1165 return Some(index);
1166 }
1167 }
1168 }
1169 None
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use std::collections::BTreeMap;
1175
1176 use serde_json::json;
1177 use soldb_core::{TraceStep, TransactionTrace};
1178 use soldb_ethdebug::{EthdebugInfo, Instruction, VariableLocation};
1179
1180 use super::{parse_source_functions, DebugSession, DebugValueStatus};
1181
1182 #[test]
1183 fn builds_source_steps_and_decodes_active_variables() {
1184 let source =
1185 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1186 let offset = source.find("value = x").expect("source offset") as u64;
1187 let trace = sample_trace();
1188 let mut sources = BTreeMap::new();
1189 sources.insert(0, "Counter.sol".to_owned());
1190 let mut source_contents = BTreeMap::new();
1191 source_contents.insert(0, source.to_owned());
1192 let mut variable_locations = BTreeMap::new();
1193 variable_locations.insert(
1194 0,
1195 vec![
1196 VariableLocation {
1197 name: "x".to_owned(),
1198 ty: "uint256".to_owned(),
1199 location_type: "stack".to_owned(),
1200 offset: 0,
1201 pc_range: (0, 1),
1202 },
1203 VariableLocation {
1204 name: "sender".to_owned(),
1205 ty: "address".to_owned(),
1206 location_type: "calldata".to_owned(),
1207 offset: 4,
1208 pc_range: (0, 1),
1209 },
1210 ],
1211 );
1212 let info = EthdebugInfo {
1213 compilation: json!({}),
1214 contract_name: "Counter".to_owned(),
1215 environment: "runtime".to_owned(),
1216 instructions: vec![Instruction {
1217 offset: 0,
1218 operation: json!({"mnemonic": "PUSH1"}),
1219 context: Some(json!({
1220 "code": {
1221 "source": {"id": 0},
1222 "range": {"offset": offset, "length": 9}
1223 }
1224 })),
1225 }],
1226 sources,
1227 variable_locations,
1228 };
1229
1230 let session = DebugSession::with_ethdebug(trace, info, source_contents);
1231 let step = session.step(0).expect("debug step");
1232 assert_eq!(step.source.as_ref().expect("source").line, 3);
1233 assert_eq!(step.function.as_ref().expect("function").name, "set");
1234 assert_eq!(
1235 step.snapshot
1236 .stack
1237 .iter()
1238 .map(|word| &**word)
1239 .collect::<Vec<_>>(),
1240 ["0x2a"]
1241 );
1242 assert_eq!(step.variables[0].name, "x");
1243 assert_eq!(step.variables[0].value.display, "42");
1244 assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1245 assert_eq!(
1246 step.variables[1].value.display,
1247 "0x1111111111111111111111111111111111111111"
1248 );
1249 }
1250
1251 #[test]
1252 fn keeps_raw_values_when_type_is_dynamic_or_location_is_unknown() {
1253 let trace = sample_trace();
1254 let mut variable_locations = BTreeMap::new();
1255 variable_locations.insert(
1256 0,
1257 vec![
1258 VariableLocation {
1259 name: "name".to_owned(),
1260 ty: "string".to_owned(),
1261 location_type: "stack".to_owned(),
1262 offset: 0,
1263 pc_range: (0, 0),
1264 },
1265 VariableLocation {
1266 name: "missing".to_owned(),
1267 ty: "uint256".to_owned(),
1268 location_type: "stack".to_owned(),
1269 offset: 99,
1270 pc_range: (0, 0),
1271 },
1272 ],
1273 );
1274 let info = EthdebugInfo {
1275 compilation: json!({}),
1276 contract_name: "Counter".to_owned(),
1277 environment: "runtime".to_owned(),
1278 instructions: Vec::new(),
1279 sources: BTreeMap::new(),
1280 variable_locations,
1281 };
1282
1283 let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1284 let step = session.step(0).expect("debug step");
1285 assert_eq!(step.variables[0].value.status, DebugValueStatus::Raw);
1286 assert_eq!(step.variables[0].value.display, "0x2a");
1287 assert_eq!(
1288 step.variables[1].value.status,
1289 DebugValueStatus::Unavailable
1290 );
1291 }
1292
1293 #[test]
1294 fn attaching_ethdebug_matches_constructing_with_it_and_replaces_prior_info() {
1295 let source =
1296 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1297 let offset = source.find("value = x").expect("source offset") as u64;
1298 let info = EthdebugInfo {
1299 compilation: json!({}),
1300 contract_name: "Counter".to_owned(),
1301 environment: "runtime".to_owned(),
1302 instructions: vec![Instruction {
1303 offset: 0,
1304 operation: json!({"mnemonic": "PUSH1"}),
1305 context: Some(json!({
1306 "code": {"source": {"id": 0}, "range": {"offset": offset, "length": 9}}
1307 })),
1308 }],
1309 sources: BTreeMap::from([(0, "Counter.sol".to_owned())]),
1310 variable_locations: BTreeMap::new(),
1311 };
1312 let source_contents = BTreeMap::from([(0, source.to_owned())]);
1313
1314 let constructed =
1315 DebugSession::with_ethdebug(sample_trace(), info.clone(), source_contents.clone());
1316 let mut attached = DebugSession::new(sample_trace());
1317 assert!(attached.ethdebug.is_none());
1318 assert!(attached.functions.is_empty());
1319 attached.attach_ethdebug(info, source_contents);
1320
1321 assert_eq!(attached, constructed);
1322 assert_eq!(attached.functions.len(), 1);
1323 assert_eq!(attached.functions[0].name, "set");
1324 assert_eq!(
1325 attached
1326 .step(0)
1327 .and_then(|step| step.source)
1328 .map(|span| span.line),
1329 Some(3)
1330 );
1331
1332 let other = EthdebugInfo {
1334 compilation: json!({}),
1335 contract_name: "Other".to_owned(),
1336 environment: "runtime".to_owned(),
1337 instructions: Vec::new(),
1338 sources: BTreeMap::new(),
1339 variable_locations: BTreeMap::new(),
1340 };
1341 attached.attach_ethdebug(other, BTreeMap::new());
1342 assert_eq!(
1343 attached
1344 .ethdebug
1345 .as_ref()
1346 .map(|info| info.contract_name.as_str()),
1347 Some("Other")
1348 );
1349 assert!(attached.functions.is_empty());
1350 assert!(attached.source_contents.is_empty());
1351 assert_eq!(attached.step(0).and_then(|step| step.source), None);
1352 assert_eq!(attached.trace, sample_trace());
1353 }
1354
1355 #[test]
1356 fn parses_source_functions_with_tuple_and_array_params() {
1357 let functions = parse_source_functions(
1358 7,
1359 "contract C { function submit(Person memory p, uint256[2] memory xs) public {} }",
1360 );
1361 assert_eq!(functions[0].source_id, 7);
1362 assert_eq!(functions[0].name, "submit");
1363 assert_eq!(functions[0].params[0].name, "p");
1364 assert_eq!(functions[0].params[0].ty, "Person");
1365 assert_eq!(functions[0].params[1].name, "xs");
1366 assert_eq!(functions[0].params[1].ty, "uint256[2]");
1367 }
1368
1369 #[test]
1370 fn unnamed_params_documented_by_comments_do_not_become_parameters() {
1371 let functions = parse_source_functions(
1375 0,
1376 "contract C {\n function burnFrom_withCaller(\n address caller,\n address, //from\n uint256, //amount\n bytes32 //h\n ) external view returns (bool) {}\n}",
1377 );
1378 let params = &functions[0].params;
1379 assert_eq!(functions[0].name, "burnFrom_withCaller");
1380 assert_eq!(params.len(), 4);
1381 assert_eq!(
1382 (params[0].ty.as_str(), params[0].name.as_str()),
1383 ("address", "caller")
1384 );
1385 assert_eq!(
1386 (params[1].ty.as_str(), params[1].name.as_str()),
1387 ("address", "arg1")
1388 );
1389 assert_eq!(
1390 (params[2].ty.as_str(), params[2].name.as_str()),
1391 ("uint256", "arg2")
1392 );
1393 assert_eq!(
1394 (params[3].ty.as_str(), params[3].name.as_str()),
1395 ("bytes32", "arg3")
1396 );
1397 }
1398
1399 #[test]
1400 fn unnamed_params_take_positional_names_not_their_type() {
1401 let functions = parse_source_functions(
1402 0,
1403 "contract C { function burn(address from, uint256 amount, bytes32, bytes memory signature) public {} }",
1404 );
1405 let params = &functions[0].params;
1406 assert_eq!(
1407 (params[1].ty.as_str(), params[1].name.as_str()),
1408 ("uint256", "amount")
1409 );
1410 assert_eq!(
1412 (params[2].ty.as_str(), params[2].name.as_str()),
1413 ("bytes32", "arg2")
1414 );
1415 assert_eq!(
1416 (params[3].ty.as_str(), params[3].name.as_str()),
1417 ("bytes", "signature")
1418 );
1419 assert_eq!(params[3].location.as_deref(), Some("memory"));
1420 }
1421
1422 #[test]
1423 fn a_trailing_declaration_keyword_does_not_name_a_param() {
1424 let functions = parse_source_functions(
1425 0,
1426 "contract C { function f(string memory, address payable, bytes calldata data) public {} }",
1427 );
1428 let params = &functions[0].params;
1429 assert_eq!(
1430 (params[0].ty.as_str(), params[0].name.as_str()),
1431 ("string", "arg0")
1432 );
1433 assert_eq!(params[0].location.as_deref(), Some("memory"));
1434 assert_eq!(
1435 (params[1].ty.as_str(), params[1].name.as_str()),
1436 ("address", "arg1")
1437 );
1438 assert_eq!(
1439 (params[2].ty.as_str(), params[2].name.as_str()),
1440 ("bytes", "data")
1441 );
1442 assert_eq!(params[2].location.as_deref(), Some("calldata"));
1443 }
1444
1445 #[test]
1446 fn block_comments_in_a_param_list_separate_rather_than_join() {
1447 let functions = parse_source_functions(
1448 0,
1449 "contract C { function f(uint256 /* wei */ amount, address /*to*/) public {} }",
1450 );
1451 let params = &functions[0].params;
1452 assert_eq!(
1453 (params[0].ty.as_str(), params[0].name.as_str()),
1454 ("uint256", "amount")
1455 );
1456 assert_eq!(
1457 (params[1].ty.as_str(), params[1].name.as_str()),
1458 ("address", "arg1")
1459 );
1460 }
1461
1462 #[test]
1463 fn stripping_comments_leaves_declaration_offsets_alone() {
1464 let source = "contract C {\n function f(address, //from\n uint256 amount\n ) public {}\n}";
1467 let functions = parse_source_functions(0, source);
1468 let start = functions[0].declaration_start as usize;
1469 assert!(source[start..].starts_with("function f("));
1470 assert_eq!(functions[0].declaration_line, 2);
1471 assert_eq!(source.as_bytes()[functions[0].body_end as usize], b'}');
1472 }
1473
1474 fn sample_trace() -> TransactionTrace {
1475 TransactionTrace {
1476 tx_hash: Some("0xabc".to_owned()),
1477 from_addr: "0x1".to_owned(),
1478 to_addr: Some("0x2".to_owned()),
1479 value: "0".to_owned(),
1480 input_data: format!(
1481 "0x12345678{}",
1482 "0000000000000000000000001111111111111111111111111111111111111111"
1483 ),
1484 gas_used: 1,
1485 output: "0x".to_owned(),
1486 success: true,
1487 error: None,
1488 debug_trace_available: true,
1489 contract_address: None,
1490 backend: Some("debug-rpc".to_owned()),
1491 capabilities: Default::default(),
1492 artifacts: Default::default(),
1493 steps: vec![TraceStep {
1494 pc: 0,
1495 op: "PUSH1".into(),
1496 gas: 1,
1497 gas_cost: 0,
1498 depth: 0,
1499 stack: vec!["0x2a".into()],
1500 memory: Some(format!("{}{}", "00".repeat(32), "2a".repeat(32))),
1501 storage: Some(BTreeMap::from([("0x0".to_owned(), "0x2a".to_owned())])),
1502 error: None,
1503 snapshot: Default::default(),
1504 }],
1505 }
1506 }
1507 fn memory_image(words: &[&str]) -> String {
1510 words
1511 .iter()
1512 .map(|word| format!("{:0>64}", word.trim_start_matches("0x")))
1513 .collect()
1514 }
1515
1516 #[test]
1517 fn reads_arguments_that_live_in_memory() {
1518 let memory = memory_image(&[
1520 "5",
1521 "68656c6c6f000000000000000000000000000000000000000000000000000000",
1522 "3",
1523 "7",
1524 "8",
1525 "9",
1526 ]);
1527 let types = super::SourceTypes::default();
1528 let reader = super::ValueReader {
1529 memory: Some(&memory),
1530 calldata: "",
1531 storage: None,
1532 layout: None,
1533 types: &types,
1534 scope: None,
1535 };
1536 let read = |offset: usize, ty: &str| reader.read_memory(offset, ty, 0);
1537 assert_eq!(read(0, "string").as_deref(), Some("\"hello\""));
1538 assert_eq!(read(0, "bytes").as_deref(), Some("0x68656c6c6f"));
1539 assert_eq!(read(64, "uint256[]").as_deref(), Some("[7, 8, 9]"));
1540 assert_eq!(read(96, "uint256[2]").as_deref(), Some("[7, 8]"));
1542 assert_eq!(read(4096, "string"), None);
1546 assert_eq!(
1547 read(64, "string[]").as_deref(),
1548 Some("[<string at 0x7>, <string at 0x8>, <string at 0x9>]")
1549 );
1550 }
1551
1552 #[test]
1553 fn a_parameter_is_readable_only_when_its_width_is_known() {
1554 let param = |ty: &str, location: Option<&str>| super::SourceParam {
1555 name: "x".to_owned(),
1556 ty: ty.to_owned(),
1557 location: location.map(str::to_owned),
1558 };
1559 assert!(super::readable_parameter(¶m("uint256", None)));
1560 assert!(super::readable_parameter(¶m("string", Some("memory"))));
1561 assert!(super::readable_parameter(¶m("Config", Some("storage"))));
1562 assert!(super::readable_parameter(¶m(
1564 "uint256[2]",
1565 Some("calldata")
1566 )));
1567 assert!(!super::readable_parameter(¶m(
1569 "bytes",
1570 Some("calldata")
1571 )));
1572 assert!(!super::readable_parameter(¶m(
1573 "uint256[]",
1574 Some("calldata")
1575 )));
1576 assert!(!super::readable_parameter(¶m("Config", None)));
1578 }
1579}