1use std::cell::RefCell;
21
22use soldb_core::{ExecutionCall, TraceStep, TransactionTrace};
23
24mod command;
25mod render;
26mod response;
27mod session;
28
29pub use command::{
30 command_spec, CommandGroup, CommandSpec, DebuggerCommand, DebuggerInfoCommand, COMMANDS,
31};
32pub use render::{shorten_hex, Renderer};
33pub use response::{
34 ArgumentInfo, BreakpointEvent, BreakpointInfo, FrameInfo, Level, ListedLine, LoadedContract,
35 MemoryInfo, MemoryWord, Output, ResourceInfo, SlotInfo, StateInfo, Stop, StopLocation,
36 StopReason, ValueStatus, VariableInfo,
37};
38pub use session::{
39 breakpoint_lines, Session, FRAME_ARGUMENTS_NOTE, FRAME_ARGUMENTS_WARNING, LISTING_RADIUS,
40};
41use soldb_debugger::{
42 call_target, normalize_address, variables_for_step, ChainStorage, Condition, ConditionContext,
43 ContractDebugInfo, DebugVariable, Evaluation, Frame, FrameState, LocalsStatus,
44 ResolvedFunction, ResolvedLine, SourceListing, StepLocation, StepMap, StorageLayout,
45 StorageTape, StorageWords,
46};
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum VariablesOrigin {
51 Ethdebug,
53 Inferred,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct StepVariables {
61 pub variables: Vec<DebugVariable>,
62 pub origin: VariablesOrigin,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum DisplayMode {
67 Source,
68 Assembly,
69}
70
71impl DisplayMode {
72 pub fn parse(input: &str) -> Option<Self> {
73 match input.trim().to_ascii_lowercase().as_str() {
74 "source" | "src" => Some(Self::Source),
75 "asm" | "assembly" => Some(Self::Assembly),
76 _ => None,
77 }
78 }
79
80 pub fn as_str(self) -> &'static str {
81 match self {
82 Self::Source => "source",
83 Self::Assembly => "asm",
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum BreakpointTarget {
91 Pc(u64),
92 SourceLine(SourceBreakpointTarget),
93 Function(String),
95 Storage(String),
97 State(String),
100 Revert,
102 Call(Option<String>),
104 Opcode(String),
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct SourceBreakpointTarget {
110 pub file: Option<String>,
111 pub line: u64,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct Breakpoint {
117 pub id: u32,
118 pub kind: BreakpointKind,
119 pub condition: Option<Condition>,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum BreakpointKind {
125 Pc(u64),
126 Line(Vec<ResolvedLine>),
128 Function(Vec<ResolvedFunction>),
130 Storage(String),
133 StateWrite {
136 path: String,
137 slot: String,
138 },
139 Revert,
140 Call(Option<String>),
142 Opcode(String),
144}
145
146impl Breakpoint {
147 #[must_use]
149 pub fn label(&self) -> String {
150 let target = match &self.kind {
151 BreakpointKind::Pc(pc) => format!("PC {pc}"),
152 BreakpointKind::Line(lines) => unique(lines.iter().map(|line| {
153 if line.requested_line == line.key.line {
154 format!("{}:{}", line.path, line.key.line)
155 } else {
156 format!(
157 "{}:{} (the statement containing line {})",
158 line.path, line.key.line, line.requested_line
159 )
160 }
161 }))
162 .join(", "),
163 BreakpointKind::Function(functions) => unique(functions.iter().map(|function| {
164 format!(
165 "function {}.{} at {}:{}",
166 function.contract_name, function.name, function.path, function.line
167 )
168 }))
169 .join(", "),
170 BreakpointKind::Storage(slot) => format!("storage slot 0x{slot}"),
171 BreakpointKind::StateWrite { path, slot } => {
172 format!("a write to `{path}` (storage slot 0x{slot})")
173 }
174 BreakpointKind::Revert => "revert".to_owned(),
175 BreakpointKind::Call(Some(address)) => format!("call to {address}"),
176 BreakpointKind::Call(None) => "any call".to_owned(),
177 BreakpointKind::Opcode(mnemonic) => format!("opcode {mnemonic}"),
178 };
179 match &self.condition {
180 Some(condition) => format!("{target} if {}", condition.text()),
181 None => target,
182 }
183 }
184}
185
186fn unique(names: impl Iterator<Item = String>) -> Vec<String> {
190 let mut seen = Vec::new();
191 for name in names {
192 if !seen.contains(&name) {
193 seen.push(name);
194 }
195 }
196 seen
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum StepOutcome {
201 NoTrace,
202 Moved {
203 step: usize,
204 pc: u64,
205 op: String,
206 },
207 BreakpointHit {
208 step: usize,
209 pc: u64,
210 breakpoint: Breakpoint,
211 },
212 AtEnd {
213 step: usize,
214 },
215 AtStart {
217 step: usize,
218 },
219 InvalidStep {
220 requested: usize,
221 max_step: Option<usize>,
222 },
223 ModeChanged(DisplayMode),
224 BreakpointSet(Breakpoint),
225 BreakpointCleared(Breakpoint),
226 BreakpointMissing(String),
228 BreakpointError(String),
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct DebuggerState {
234 pub current_step: usize,
235 pub display_mode: DisplayMode,
236 breakpoints: Vec<Breakpoint>,
237 next_breakpoint_id: u32,
238 trace: Option<TransactionTrace>,
239 step_map: Option<StepMap>,
240 storage_tape: Option<StorageTape>,
241 note: RefCell<Option<String>>,
245}
246
247impl Default for DebuggerState {
248 fn default() -> Self {
249 Self {
250 current_step: 0,
251 display_mode: DisplayMode::Source,
252 breakpoints: Vec::new(),
253 next_breakpoint_id: 1,
254 trace: None,
255 step_map: None,
256 storage_tape: None,
257 note: RefCell::new(None),
258 }
259 }
260}
261
262impl DebuggerState {
263 pub fn new() -> Self {
264 Self::default()
265 }
266
267 pub fn load_trace(&mut self, trace: TransactionTrace) {
270 let map = StepMap::new(&trace, Vec::new());
271 self.storage_tape = Some(StorageTape::new(&trace, &map));
272 self.step_map = Some(map);
273 self.trace = Some(trace);
274 self.current_step = 0;
275 }
276
277 pub fn attach_debug_info(&mut self, contracts: Vec<ContractDebugInfo>) {
280 if let Some(trace) = &self.trace {
281 let map = StepMap::new(trace, contracts);
282 self.storage_tape = Some(StorageTape::new(trace, &map));
283 self.step_map = Some(map);
284 }
285 }
286
287 #[must_use]
289 pub fn storage_words(&self) -> Option<StorageWords<'_>> {
290 let map = self.step_map.as_ref()?;
291 Some(self.storage_tape.as_ref()?.at_step(map, self.current_step))
292 }
293
294 #[must_use]
297 pub fn storage_words_with_chain<'a>(
298 &'a self,
299 chain: Option<&'a dyn ChainStorage>,
300 ) -> Option<StorageWords<'a>> {
301 Some(self.storage_words()?.with_chain(chain))
302 }
303
304 #[must_use]
306 pub fn storage_address(&self) -> Option<&str> {
307 self.step_map.as_ref()?.storage_address(self.current_step)
308 }
309
310 #[must_use]
313 pub fn storage_layout(&self) -> Option<&StorageLayout> {
314 self.step_map
315 .as_ref()?
316 .contract_at_step(self.current_step)?
317 .storage_layout
318 .as_ref()
319 }
320
321 pub fn trace(&self) -> Option<&TransactionTrace> {
322 self.trace.as_ref()
323 }
324
325 #[must_use]
326 pub fn step_map(&self) -> Option<&StepMap> {
327 self.step_map.as_ref()
328 }
329
330 #[must_use]
332 pub fn has_source(&self) -> bool {
333 self.step_map.as_ref().is_some_and(StepMap::has_source)
334 }
335
336 pub fn current_step_data(&self) -> Option<&TraceStep> {
337 self.trace
338 .as_ref()
339 .and_then(|trace| trace.steps.get(self.current_step))
340 }
341
342 #[must_use]
344 pub fn location(&self) -> Option<StepLocation> {
345 self.step_map.as_ref()?.location(self.current_step)
346 }
347
348 pub fn step_count(&self) -> usize {
349 self.trace
350 .as_ref()
351 .map(|trace| trace.steps.len())
352 .unwrap_or(0)
353 }
354
355 #[must_use]
356 pub fn breakpoints(&self) -> &[Breakpoint] {
357 &self.breakpoints
358 }
359
360 pub fn set_display_mode(&mut self, mode: DisplayMode) -> StepOutcome {
361 self.display_mode = mode;
362 StepOutcome::ModeChanged(mode)
363 }
364
365 pub fn next_instruction(&mut self) -> StepOutcome {
366 let Some(trace) = &self.trace else {
367 return StepOutcome::NoTrace;
368 };
369 if self.current_step >= trace.steps.len().saturating_sub(1) {
370 return StepOutcome::AtEnd {
371 step: self.current_step,
372 };
373 }
374
375 self.current_step += 1;
376 self.outcome_for_current_step()
377 }
378
379 pub fn next_source(&mut self) -> StepOutcome {
383 if !self.has_source() {
384 return self.next_instruction();
385 }
386 let target = self
387 .step_map
388 .as_ref()
389 .and_then(|map| map.next_source(self.current_step));
390 self.advance_to(target)
391 }
392
393 pub fn step_into(&mut self) -> StepOutcome {
395 if !self.has_source() {
396 return self.next_instruction();
397 }
398 let target = self
399 .step_map
400 .as_ref()
401 .and_then(|map| map.step_into(self.current_step));
402 self.advance_to(target)
403 }
404
405 pub fn finish(&mut self) -> StepOutcome {
408 if self.trace.is_none() {
409 return StepOutcome::NoTrace;
410 }
411 let target = self
412 .step_map
413 .as_ref()
414 .and_then(|map| map.finish(self.current_step));
415 self.advance_to(target)
416 }
417
418 pub fn continue_execution(&mut self) -> StepOutcome {
419 if self.trace.is_none() {
420 return StepOutcome::NoTrace;
421 }
422 self.advance_to(None)
423 }
424
425 pub fn previous_instruction(&mut self) -> StepOutcome {
428 if self.trace.is_none() {
429 return StepOutcome::NoTrace;
430 }
431 if self.current_step == 0 {
432 return StepOutcome::AtStart { step: 0 };
433 }
434 self.current_step -= 1;
435 self.outcome_for_current_step()
436 }
437
438 pub fn previous_source(&mut self) -> StepOutcome {
441 if !self.has_source() {
442 return self.previous_instruction();
443 }
444 let target = self
445 .step_map
446 .as_ref()
447 .and_then(|map| map.previous_source(self.current_step));
448 self.retreat_to(target)
449 }
450
451 pub fn reverse_step_into(&mut self) -> StepOutcome {
453 if !self.has_source() {
454 return self.previous_instruction();
455 }
456 let target = self
457 .step_map
458 .as_ref()
459 .and_then(|map| map.reverse_step_into(self.current_step));
460 self.retreat_to(target)
461 }
462
463 pub fn reverse_finish(&mut self) -> StepOutcome {
466 if self.trace.is_none() {
467 return StepOutcome::NoTrace;
468 }
469 let target = self
470 .step_map
471 .as_ref()
472 .and_then(|map| map.reverse_finish(self.current_step));
473 self.retreat_to(target)
474 }
475
476 pub fn reverse_continue(&mut self) -> StepOutcome {
478 if self.trace.is_none() {
479 return StepOutcome::NoTrace;
480 }
481 self.retreat_to(None)
482 }
483
484 fn advance_to(&mut self, target: Option<usize>) -> StepOutcome {
487 let Some(trace) = &self.trace else {
488 return StepOutcome::NoTrace;
489 };
490 let last = trace.steps.len().saturating_sub(1);
491 if self.current_step >= last {
492 return StepOutcome::AtEnd {
493 step: self.current_step,
494 };
495 }
496 let target = target.unwrap_or(last).min(last);
497 while self.current_step < target {
498 self.current_step += 1;
499 if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
500 return StepOutcome::BreakpointHit {
501 step: self.current_step,
502 pc: self.pc_at(self.current_step),
503 breakpoint,
504 };
505 }
506 }
507 if target == last {
508 return StepOutcome::AtEnd { step: last };
509 }
510 self.outcome_for_current_step()
511 }
512
513 fn retreat_to(&mut self, target: Option<usize>) -> StepOutcome {
516 if self.trace.is_none() {
517 return StepOutcome::NoTrace;
518 }
519 if self.current_step == 0 {
520 return StepOutcome::AtStart { step: 0 };
521 }
522 let target = target.unwrap_or(0);
523 while self.current_step > target {
524 self.current_step -= 1;
525 if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
526 return StepOutcome::BreakpointHit {
527 step: self.current_step,
528 pc: self.pc_at(self.current_step),
529 breakpoint,
530 };
531 }
532 }
533 if target == 0 {
534 return StepOutcome::AtStart { step: 0 };
535 }
536 self.outcome_for_current_step()
537 }
538
539 pub fn goto_step(&mut self, step: usize) -> StepOutcome {
540 let Some(trace) = &self.trace else {
541 return StepOutcome::NoTrace;
542 };
543 if step >= trace.steps.len() {
544 return StepOutcome::InvalidStep {
545 requested: step,
546 max_step: trace.steps.len().checked_sub(1),
547 };
548 }
549 self.current_step = step;
550 self.outcome_for_current_step()
551 }
552
553 pub fn set_breakpoint(&mut self, pc: u64) -> StepOutcome {
555 self.add_breakpoint(BreakpointKind::Pc(pc), None)
556 }
557
558 pub fn clear_breakpoint(&mut self, pc: u64) -> StepOutcome {
560 self.remove_breakpoint(&BreakpointKind::Pc(pc), &format!("PC {pc}"))
561 }
562
563 pub fn set_breakpoint_target(&mut self, target: &BreakpointTarget) -> StepOutcome {
566 self.set_conditional_breakpoint_target(target, None)
567 }
568
569 pub fn set_conditional_breakpoint_target(
571 &mut self,
572 target: &BreakpointTarget,
573 condition: Option<&str>,
574 ) -> StepOutcome {
575 let condition = match condition.map(Condition::parse).transpose() {
576 Ok(condition) => condition,
577 Err(message) => return StepOutcome::BreakpointError(message),
578 };
579 match self.resolve_target(target) {
580 Ok(kind) => {
581 if !self.matches_any_step(&kind) {
582 let label = Breakpoint {
583 id: 0,
584 kind: kind.clone(),
585 condition: None,
586 }
587 .label();
588 self.push_note(format!(
589 "no step of this trace reaches {label}, so this breakpoint cannot be hit"
590 ));
591 }
592 self.add_breakpoint(kind, condition)
593 }
594 Err(message) => StepOutcome::BreakpointError(message),
595 }
596 }
597
598 pub fn clear_breakpoint_target(&mut self, target: &BreakpointTarget) -> StepOutcome {
600 match self.resolve_target(target) {
601 Ok(kind) => {
602 let label = Breakpoint {
603 id: 0,
604 kind: kind.clone(),
605 condition: None,
606 }
607 .label();
608 self.remove_breakpoint(&kind, &label)
609 }
610 Err(message) => StepOutcome::BreakpointError(message),
611 }
612 }
613
614 pub fn delete_breakpoint(&mut self, id: u32) -> StepOutcome {
616 match self
617 .breakpoints
618 .iter()
619 .position(|breakpoint| breakpoint.id == id)
620 {
621 Some(index) => StepOutcome::BreakpointCleared(self.breakpoints.remove(index)),
622 None => StepOutcome::BreakpointMissing(format!("#{id}")),
623 }
624 }
625
626 fn add_breakpoint(
627 &mut self,
628 kind: BreakpointKind,
629 condition: Option<Condition>,
630 ) -> StepOutcome {
631 if let Some(existing) = self
632 .breakpoints
633 .iter()
634 .find(|breakpoint| breakpoint.kind == kind && breakpoint.condition == condition)
635 {
636 return StepOutcome::BreakpointSet(existing.clone());
637 }
638 let breakpoint = Breakpoint {
639 id: self.next_breakpoint_id,
640 kind,
641 condition,
642 };
643 self.next_breakpoint_id += 1;
644 self.breakpoints.push(breakpoint.clone());
645 StepOutcome::BreakpointSet(breakpoint)
646 }
647
648 fn remove_breakpoint(&mut self, kind: &BreakpointKind, label: &str) -> StepOutcome {
649 match self
650 .breakpoints
651 .iter()
652 .position(|breakpoint| breakpoint.kind == *kind)
653 {
654 Some(index) => StepOutcome::BreakpointCleared(self.breakpoints.remove(index)),
655 None => StepOutcome::BreakpointMissing(label.to_owned()),
656 }
657 }
658
659 fn resolve_state_target(&self, path: &str) -> Result<BreakpointKind, String> {
661 let map = self.source_map_for("state breakpoints")?;
662 let layouts = map
663 .contracts()
664 .iter()
665 .filter_map(|contract| contract.storage_layout.as_ref());
666 let mut failure = None;
667 for layout in layouts {
668 match layout.resolve(path) {
669 Ok(reference) => {
670 let slot = soldb_debugger::short_hex(&reference.slot);
671 return Ok(BreakpointKind::StateWrite {
672 path: reference.path,
673 slot: slot.trim_start_matches("0x").to_owned(),
674 });
675 }
676 Err(error) => failure = Some(error.to_string()),
677 }
678 }
679 Err(failure.unwrap_or_else(|| {
680 "no storage layout is loaded, so state variables cannot be named; compile with `--storage-layout`".to_owned()
681 }))
682 }
683
684 fn resolve_target(&self, target: &BreakpointTarget) -> Result<BreakpointKind, String> {
685 match target {
686 BreakpointTarget::Pc(pc) => Ok(BreakpointKind::Pc(*pc)),
687 BreakpointTarget::SourceLine(target) => {
688 let map = self.source_map_for("source breakpoints")?;
689 map.resolve_line(target.file.as_deref(), target.line)
690 .map(BreakpointKind::Line)
691 }
692 BreakpointTarget::Function(name) => {
693 let map = self.source_map_for("function breakpoints")?;
694 match map.resolve_function(name) {
695 Ok(functions) => Ok(BreakpointKind::Function(functions)),
696 Err(error) => self.resolve_state_target(name).map_err(|state_error| {
699 if state_error.contains("no storage layout") {
700 error
701 } else {
702 format!("{error}; and {state_error}")
703 }
704 }),
705 }
706 }
707 BreakpointTarget::State(path) => self.resolve_state_target(path),
708 BreakpointTarget::Storage(slot) => normalize_slot(slot)
709 .map(BreakpointKind::Storage)
710 .ok_or_else(|| format!("invalid storage slot `{slot}`; expected decimal or hex")),
711 BreakpointTarget::Revert => Ok(BreakpointKind::Revert),
712 BreakpointTarget::Call(None) => Ok(BreakpointKind::Call(None)),
713 BreakpointTarget::Call(Some(address)) => {
714 let normalized = normalize_address(address);
715 let hex = &normalized[2..];
716 if hex.len() != 40 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
717 return Err(format!(
718 "invalid address `{address}`; expected 20 bytes of hex"
719 ));
720 }
721 Ok(BreakpointKind::Call(Some(normalized)))
722 }
723 BreakpointTarget::Opcode(mnemonic) => {
724 Ok(BreakpointKind::Opcode(mnemonic.to_ascii_uppercase()))
725 }
726 }
727 }
728
729 fn source_map_for(&self, what: &str) -> Result<&StepMap, String> {
730 if self.trace.is_none() {
731 return Err("no trace is loaded".to_owned());
732 }
733 self.step_map
734 .as_ref()
735 .filter(|map| !map.contracts().is_empty())
736 .ok_or_else(|| {
737 format!("{what} require compiler debug metadata; start the session with `--ethdebug-dir <address>:<contract>:<dir>`")
738 })
739 }
740
741 #[must_use]
743 pub fn breakpoint_hit(&self, step: usize) -> Option<Breakpoint> {
744 let trace_step = self.trace.as_ref()?.steps.get(step)?;
745 self.breakpoints
746 .iter()
747 .filter(|breakpoint| self.kind_matches(&breakpoint.kind, step, trace_step))
748 .find(|breakpoint| match &breakpoint.condition {
751 Some(condition) => {
752 matches!(self.evaluate_condition(condition, step), Evaluation::True)
753 }
754 None => true,
755 })
756 .cloned()
757 }
758
759 fn kind_matches(&self, kind: &BreakpointKind, step: usize, trace_step: &TraceStep) -> bool {
761 let map = self.step_map.as_ref();
762 match kind {
763 BreakpointKind::Pc(pc) => trace_step.pc == *pc,
764 BreakpointKind::Line(lines) => map.is_some_and(|map| {
765 map.is_line_start(step)
766 && map
767 .line_key(step)
768 .is_some_and(|key| lines.iter().any(|line| line.key == key))
769 }),
770 BreakpointKind::Function(functions) => map.is_some_and(|map| {
771 map.is_frame_entry(step)
772 && map
773 .function_id(step)
774 .is_some_and(|id| functions.iter().any(|function| function.id == id))
775 }),
776 BreakpointKind::Storage(slot) | BreakpointKind::StateWrite { slot, .. } => {
777 &*trace_step.op == "SSTORE"
778 && trace_step
779 .snapshot_ref()
780 .stack
781 .last()
782 .and_then(|word| normalize_slot(word))
783 .is_some_and(|written| written == *slot)
784 }
785 BreakpointKind::Revert => &*trace_step.op == "REVERT" || trace_step.error.is_some(),
786 BreakpointKind::Call(address) => {
787 let target = call_target(trace_step);
788 match address {
789 Some(address) => target.as_deref() == Some(address.as_str()),
790 None => target.is_some() || is_call_opcode(&trace_step.op),
791 }
792 }
793 BreakpointKind::Opcode(mnemonic) => trace_step.op.eq_ignore_ascii_case(mnemonic),
794 }
795 }
796
797 fn matches_any_step(&self, kind: &BreakpointKind) -> bool {
805 let Some(trace) = &self.trace else {
806 return true;
807 };
808 trace
809 .steps
810 .iter()
811 .enumerate()
812 .any(|(step, trace_step)| self.kind_matches(kind, step, trace_step))
813 }
814
815 #[must_use]
820 pub fn evaluate_condition(&self, condition: &Condition, step: usize) -> Evaluation {
821 let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
822 return Evaluation::Unavailable("no trace is loaded".to_owned());
823 };
824 let Some(trace_step) = trace.steps.get(step) else {
825 return Evaluation::Unavailable(format!("step {step} is outside the trace"));
826 };
827 let words = self
828 .storage_tape
829 .as_ref()
830 .map(|tape| tape.at_step(map, step));
831 let frames = self.frames_at(step);
832 let frame = frames.iter().find(|frame| !frame.arguments.is_empty());
833 let context = ConditionContext::new(map, step, trace_step, words)
834 .with_frame(frame)
835 .with_trace(trace);
836 let outcome = condition.evaluate(&context);
837 if let Evaluation::Unavailable(reason) = &outcome {
838 self.push_note(format!(
839 "`{}` could not be evaluated: {reason}",
840 condition.text()
841 ));
842 }
843 outcome
844 }
845
846 fn push_note(&self, note: String) {
848 let mut slot = self.note.borrow_mut();
849 if slot.is_none() {
850 *slot = Some(note);
851 }
852 }
853
854 #[must_use]
857 pub fn take_note(&self) -> Option<String> {
858 self.note.borrow_mut().take()
859 }
860
861 #[must_use]
863 pub fn frames_at(&self, step: usize) -> Vec<Frame> {
864 let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
865 return Vec::new();
866 };
867 let mut frames = map.frames(step);
868 for frame in &mut frames {
869 let Some(entry) = trace.steps.get(frame.entry_step) else {
870 continue;
871 };
872 let snapshot = entry.snapshot_ref();
873 frame.arguments = map.frame_arguments(
874 frame,
875 FrameState {
876 stack: snapshot.stack,
877 memory: snapshot.memory,
878 },
879 );
880 }
881 frames
882 }
883
884 #[must_use]
886 pub fn frames(&self) -> Vec<Frame> {
887 self.frames_at(self.current_step)
888 }
889
890 #[must_use]
892 pub fn source_listing(&self, radius: u64) -> Option<SourceListing> {
893 self.step_map
894 .as_ref()?
895 .source_listing(self.current_step, radius)
896 }
897
898 pub fn variables(&self) -> Result<StepVariables, String> {
902 let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
903 return Err("no trace is loaded".to_owned());
904 };
905 let Some(step) = trace.steps.get(self.current_step) else {
906 return Err(format!("step {} is outside the trace", self.current_step));
907 };
908 let Some(contract) = map.contract_at_step(self.current_step) else {
909 return Err("no sources matched the contract executing at this step".to_owned());
910 };
911 if contract.info.has_variable_locations() {
912 return Ok(StepVariables {
913 variables: variables_for_step(trace, &contract.info, step),
914 origin: VariablesOrigin::Ethdebug,
915 });
916 }
917 match map.locals_at(self.current_step) {
918 LocalsStatus::Inferred(_) => {
919 let words = self
921 .storage_tape
922 .as_ref()
923 .map(|tape| tape.at_step(map, self.current_step));
924 Ok(StepVariables {
925 variables: map.inferred_variables(trace, self.current_step, words.as_ref()),
926 origin: VariablesOrigin::Inferred,
927 })
928 }
929 LocalsStatus::Unavailable(reason) => Err(reason.to_owned()),
930 }
931 }
932
933 #[must_use]
938 pub fn local_path(&self, path: &str) -> Option<Result<DebugVariable, String>> {
939 let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
940 return None;
941 };
942 let words = self
943 .storage_tape
944 .as_ref()
945 .map(|tape| tape.at_step(map, self.current_step));
946 map.local_path(trace, self.current_step, words.as_ref(), path)
947 }
948
949 #[must_use]
952 pub fn current_call(&self) -> Option<&ExecutionCall> {
953 let trace = self.trace.as_ref()?;
954 trace
955 .artifacts
956 .calls
957 .iter()
958 .filter(|call| {
959 call.entry_step
960 .is_some_and(|entry| entry <= self.current_step)
961 && call.exit_step.is_none_or(|exit| self.current_step < exit)
962 })
963 .max_by_key(|call| call.depth)
964 }
965
966 #[must_use]
970 pub fn calldata(&self) -> Option<String> {
971 let trace = self.trace.as_ref()?;
972 let step = trace.steps.get(self.current_step)?;
973 let root_depth = trace.steps.first().map_or(0, |first| first.depth);
974 if step.depth == root_depth {
975 return Some(trace.input_data.clone());
976 }
977 self.current_call().map(|call| call.input.clone())
978 }
979
980 pub fn apply_command(&mut self, command: DebuggerCommand) -> Option<StepOutcome> {
981 match command {
982 DebuggerCommand::Next => Some(self.next_source()),
983 DebuggerCommand::NextInstruction => Some(self.next_instruction()),
984 DebuggerCommand::Step => Some(self.step_into()),
985 DebuggerCommand::Continue => Some(self.continue_execution()),
986 DebuggerCommand::Finish => Some(self.finish()),
987 DebuggerCommand::ReverseNext => Some(self.previous_source()),
988 DebuggerCommand::ReverseNextInstruction => Some(self.previous_instruction()),
989 DebuggerCommand::ReverseStep => Some(self.reverse_step_into()),
990 DebuggerCommand::ReverseContinue => Some(self.reverse_continue()),
991 DebuggerCommand::ReverseFinish => Some(self.reverse_finish()),
992 DebuggerCommand::Goto(step) => Some(self.goto_step(step)),
993 DebuggerCommand::Mode(Some(mode)) => Some(self.set_display_mode(mode)),
994 DebuggerCommand::Break(target, condition) => {
995 Some(self.set_conditional_breakpoint_target(&target, condition.as_deref()))
996 }
997 DebuggerCommand::Clear(target) => Some(self.clear_breakpoint_target(&target)),
998 DebuggerCommand::Delete(id) => Some(self.delete_breakpoint(id)),
999 DebuggerCommand::Empty
1000 | DebuggerCommand::Help(_)
1001 | DebuggerCommand::Info(_)
1002 | DebuggerCommand::Backtrace
1003 | DebuggerCommand::List
1004 | DebuggerCommand::Memory { .. }
1005 | DebuggerCommand::Calldata
1006 | DebuggerCommand::Stack { .. }
1007 | DebuggerCommand::Mode(None)
1008 | DebuggerCommand::Print(_)
1009 | DebuggerCommand::Tui
1010 | DebuggerCommand::Quit
1011 | DebuggerCommand::Unknown(_)
1012 | DebuggerCommand::Vars => None,
1013 }
1014 }
1015
1016 fn pc_at(&self, step: usize) -> u64 {
1017 self.trace
1018 .as_ref()
1019 .and_then(|trace| trace.steps.get(step))
1020 .map_or(0, |step| step.pc)
1021 }
1022
1023 fn outcome_for_current_step(&self) -> StepOutcome {
1024 let Some(step) = self.current_step_data() else {
1025 return StepOutcome::NoTrace;
1026 };
1027 if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
1028 return StepOutcome::BreakpointHit {
1029 step: self.current_step,
1030 pc: step.pc,
1031 breakpoint,
1032 };
1033 }
1034 StepOutcome::Moved {
1035 step: self.current_step,
1036 pc: step.pc,
1037 op: step.op.to_string(),
1038 }
1039 }
1040}
1041
1042fn is_call_opcode(op: &str) -> bool {
1043 matches!(op, "CALL" | "CALLCODE" | "DELEGATECALL" | "STATICCALL")
1044}
1045
1046fn normalize_slot(input: &str) -> Option<String> {
1049 let input = input.trim();
1050 let hex = if let Some(hex) = input
1051 .strip_prefix("0x")
1052 .or_else(|| input.strip_prefix("0X"))
1053 {
1054 if hex.is_empty() || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1055 return None;
1056 }
1057 hex.to_ascii_lowercase()
1058 } else {
1059 format!("{:x}", input.parse::<u128>().ok()?)
1060 };
1061 let trimmed = hex.trim_start_matches('0');
1062 Some(if trimmed.is_empty() {
1063 "0".to_owned()
1064 } else {
1065 trimmed.to_owned()
1066 })
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use std::collections::BTreeMap;
1072
1073 use serde_json::json;
1074 use soldb_core::{StepSnapshot, TraceStep, TransactionTrace};
1075 use soldb_debugger::{
1076 CodeGenerator, ContractDebugInfo, FunctionId, LineKey, ResolvedFunction, ResolvedLine,
1077 };
1078 use soldb_ethdebug::{EthdebugInfo, Instruction};
1079
1080 use super::{
1081 Breakpoint, BreakpointKind, BreakpointTarget, DebuggerCommand, DebuggerInfoCommand,
1082 DebuggerState, DisplayMode, SourceBreakpointTarget, StepOutcome, VariablesOrigin,
1083 };
1084
1085 #[test]
1086 fn parses_repl_commands_and_aliases() {
1087 assert_eq!(DebuggerCommand::parse(""), DebuggerCommand::Empty);
1088 assert_eq!(DebuggerCommand::parse("n"), DebuggerCommand::Next);
1089 assert_eq!(
1090 DebuggerCommand::parse("stepi"),
1091 DebuggerCommand::NextInstruction
1092 );
1093 assert_eq!(DebuggerCommand::parse("s"), DebuggerCommand::Step);
1094 assert_eq!(DebuggerCommand::parse("c"), DebuggerCommand::Continue);
1095 assert_eq!(DebuggerCommand::parse("finish"), DebuggerCommand::Finish);
1096 assert_eq!(DebuggerCommand::parse("fin"), DebuggerCommand::Finish);
1097 assert_eq!(DebuggerCommand::parse("goto 2"), DebuggerCommand::Goto(2));
1098 assert_eq!(DebuggerCommand::parse("vars"), DebuggerCommand::Vars);
1099 assert_eq!(DebuggerCommand::parse("LOCALS"), DebuggerCommand::Vars);
1100 assert_eq!(
1101 DebuggerCommand::parse("print balance"),
1102 DebuggerCommand::Print("balance".to_owned())
1103 );
1104 assert_eq!(
1105 DebuggerCommand::parse("p balance "),
1106 DebuggerCommand::Print("balance".to_owned())
1107 );
1108 assert_eq!(
1109 DebuggerCommand::parse("print"),
1110 DebuggerCommand::Print(String::new())
1111 );
1112 assert_eq!(
1113 DebuggerCommand::parse("info resources"),
1114 DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: false })
1115 );
1116 assert_eq!(
1117 DebuggerCommand::parse("info resources --json"),
1118 DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: true })
1119 );
1120 assert_eq!(
1121 DebuggerCommand::parse("info breakpoints"),
1122 DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints)
1123 );
1124 assert_eq!(
1125 DebuggerCommand::parse("i b"),
1126 DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints)
1127 );
1128 assert_eq!(
1129 DebuggerCommand::parse("info storage"),
1130 DebuggerCommand::Info(DebuggerInfoCommand::Storage)
1131 );
1132 assert_eq!(
1133 DebuggerCommand::parse("storage"),
1134 DebuggerCommand::Info(DebuggerInfoCommand::Storage)
1135 );
1136 assert_eq!(
1137 DebuggerCommand::parse("mode assembly"),
1138 DebuggerCommand::Mode(Some(DisplayMode::Assembly))
1139 );
1140 assert_eq!(
1141 DebuggerCommand::parse("break 0x10"),
1142 DebuggerCommand::Break(BreakpointTarget::Pc(16), None)
1143 );
1144 assert_eq!(
1145 DebuggerCommand::parse("break Counter.sol:7"),
1146 DebuggerCommand::Break(
1147 BreakpointTarget::SourceLine(SourceBreakpointTarget {
1148 file: Some("Counter.sol".to_owned()),
1149 line: 7
1150 }),
1151 None
1152 )
1153 );
1154 assert_eq!(
1155 DebuggerCommand::parse("break line 7"),
1156 DebuggerCommand::Break(
1157 BreakpointTarget::SourceLine(SourceBreakpointTarget {
1158 file: None,
1159 line: 7
1160 }),
1161 None
1162 )
1163 );
1164 assert_eq!(
1165 DebuggerCommand::parse("break increment"),
1166 DebuggerCommand::Break(BreakpointTarget::Function("increment".to_owned()), None)
1167 );
1168 assert_eq!(
1169 DebuggerCommand::parse("b Counter.increment"),
1170 DebuggerCommand::Break(
1171 BreakpointTarget::Function("Counter.increment".to_owned()),
1172 None
1173 )
1174 );
1175 assert_eq!(
1176 DebuggerCommand::parse("break storage 0x0"),
1177 DebuggerCommand::Break(BreakpointTarget::Storage("0x0".to_owned()), None)
1178 );
1179 assert_eq!(
1180 DebuggerCommand::parse("break revert"),
1181 DebuggerCommand::Break(BreakpointTarget::Revert, None)
1182 );
1183 assert_eq!(
1184 DebuggerCommand::parse("break call"),
1185 DebuggerCommand::Break(BreakpointTarget::Call(None), None)
1186 );
1187 assert_eq!(
1188 DebuggerCommand::parse("break call 0xabc"),
1189 DebuggerCommand::Break(BreakpointTarget::Call(Some("0xabc".to_owned())), None)
1190 );
1191 assert_eq!(
1192 DebuggerCommand::parse("break op sstore"),
1193 DebuggerCommand::Break(BreakpointTarget::Opcode("sstore".to_owned()), None)
1194 );
1195 assert_eq!(
1196 DebuggerCommand::parse("break"),
1197 DebuggerCommand::Unknown("break".to_owned())
1198 );
1199 assert_eq!(
1200 DebuggerCommand::parse("break 12abc"),
1201 DebuggerCommand::Unknown("break 12abc".to_owned())
1202 );
1203 assert_eq!(
1204 DebuggerCommand::parse("clear 8"),
1205 DebuggerCommand::Clear(BreakpointTarget::Pc(8))
1206 );
1207 assert_eq!(
1208 DebuggerCommand::parse("delete 3"),
1209 DebuggerCommand::Delete(3)
1210 );
1211 assert_eq!(DebuggerCommand::parse("d #3"), DebuggerCommand::Delete(3));
1212 assert_eq!(DebuggerCommand::parse("bt"), DebuggerCommand::Backtrace);
1213 assert_eq!(DebuggerCommand::parse("where"), DebuggerCommand::Backtrace);
1214 assert_eq!(DebuggerCommand::parse("l"), DebuggerCommand::List);
1215 assert_eq!(
1216 DebuggerCommand::parse("memory"),
1217 DebuggerCommand::Memory {
1218 offset: None,
1219 length: None
1220 }
1221 );
1222 assert_eq!(
1223 DebuggerCommand::parse("mem 0x40 32"),
1224 DebuggerCommand::Memory {
1225 offset: Some(64),
1226 length: Some(32)
1227 }
1228 );
1229 assert_eq!(
1230 DebuggerCommand::parse("mem 1 2 3"),
1231 DebuggerCommand::Unknown("mem 1 2 3".to_owned())
1232 );
1233 assert_eq!(
1234 DebuggerCommand::parse("calldata"),
1235 DebuggerCommand::Calldata
1236 );
1237 assert_eq!(
1238 DebuggerCommand::parse("stack"),
1239 DebuggerCommand::Stack { limit: None }
1240 );
1241 assert_eq!(
1242 DebuggerCommand::parse("help mode"),
1243 DebuggerCommand::Help(Some("mode".to_owned()))
1244 );
1245 assert_eq!(DebuggerCommand::parse("q"), DebuggerCommand::Quit);
1246 assert_eq!(
1247 DebuggerCommand::parse("wat"),
1248 DebuggerCommand::Unknown("wat".to_owned())
1249 );
1250 }
1251
1252 #[test]
1253 fn loads_trace_and_steps_instruction_by_instruction() {
1254 let mut state = DebuggerState::new();
1255 assert_eq!(state.next_instruction(), StepOutcome::NoTrace);
1256 assert_eq!(state.next_source(), StepOutcome::NoTrace);
1257 assert_eq!(state.finish(), StepOutcome::NoTrace);
1258
1259 state.load_trace(sample_trace());
1260 assert_eq!(state.step_count(), 4);
1261 assert_eq!(&*state.current_step_data().expect("step").op, "PUSH1");
1262 assert!(!state.has_source());
1263
1264 assert_eq!(
1265 state.next_instruction(),
1266 StepOutcome::Moved {
1267 step: 1,
1268 pc: 2,
1269 op: "MSTORE".into()
1270 }
1271 );
1272 assert_eq!(state.current_step, 1);
1273 assert!(matches!(
1275 state.next_source(),
1276 StepOutcome::Moved { step: 2, .. }
1277 ));
1278 assert!(matches!(
1279 state.step_into(),
1280 StepOutcome::Moved { step: 3, .. }
1281 ));
1282 assert!(matches!(
1283 state.previous_source(),
1284 StepOutcome::Moved { step: 2, .. }
1285 ));
1286 assert!(matches!(
1287 state.reverse_step_into(),
1288 StepOutcome::Moved { step: 1, .. }
1289 ));
1290 }
1291
1292 #[test]
1293 fn continues_until_breakpoint_or_end() {
1294 let mut state = DebuggerState::new();
1295 state.load_trace(sample_trace());
1296 let set = state.set_breakpoint(3);
1297 let StepOutcome::BreakpointSet(breakpoint) = &set else {
1298 panic!("{set:?}");
1299 };
1300 assert_eq!(breakpoint.id, 1);
1301 assert_eq!(breakpoint.kind, BreakpointKind::Pc(3));
1302 assert_eq!(breakpoint.label(), "PC 3");
1303 assert_eq!(state.set_breakpoint(3), set);
1305 assert_eq!(state.breakpoints().len(), 1);
1306
1307 assert!(matches!(
1308 state.continue_execution(),
1309 StepOutcome::BreakpointHit { step: 2, pc: 3, .. }
1310 ));
1311 assert_eq!(state.current_step, 2);
1312
1313 assert!(matches!(
1314 state.clear_breakpoint(3),
1315 StepOutcome::BreakpointCleared(_)
1316 ));
1317 assert_eq!(
1318 state.clear_breakpoint(3),
1319 StepOutcome::BreakpointMissing("PC 3".to_owned())
1320 );
1321 assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
1322 assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
1323 }
1324
1325 #[test]
1326 fn numbered_breakpoints_can_be_deleted() {
1327 let mut state = DebuggerState::new();
1328 state.load_trace(sample_trace());
1329 state.set_breakpoint(2);
1330 state.set_breakpoint(3);
1331 assert_eq!(
1332 state
1333 .breakpoints()
1334 .iter()
1335 .map(|breakpoint| breakpoint.id)
1336 .collect::<Vec<_>>(),
1337 vec![1, 2]
1338 );
1339 assert!(matches!(
1340 state.delete_breakpoint(1),
1341 StepOutcome::BreakpointCleared(breakpoint) if breakpoint.kind == BreakpointKind::Pc(2)
1342 ));
1343 assert_eq!(
1344 state.delete_breakpoint(1),
1345 StepOutcome::BreakpointMissing("#1".to_owned())
1346 );
1347 assert!(matches!(
1349 state.set_breakpoint(4),
1350 StepOutcome::BreakpointSet(breakpoint) if breakpoint.id == 3
1351 ));
1352 }
1353
1354 #[test]
1355 fn goto_validates_trace_bounds() {
1356 let mut state = DebuggerState::new();
1357 assert_eq!(state.goto_step(1), StepOutcome::NoTrace);
1358
1359 state.load_trace(sample_trace());
1360 assert_eq!(
1361 state.goto_step(3),
1362 StepOutcome::Moved {
1363 step: 3,
1364 pc: 4,
1365 op: "CALL".into()
1366 }
1367 );
1368 assert_eq!(
1369 state.goto_step(99),
1370 StepOutcome::InvalidStep {
1371 requested: 99,
1372 max_step: Some(3)
1373 }
1374 );
1375 }
1376
1377 #[test]
1378 fn parses_reverse_commands_and_aliases() {
1379 for alias in ["reverse-next", "rnext", "rn"] {
1380 assert_eq!(DebuggerCommand::parse(alias), DebuggerCommand::ReverseNext);
1381 }
1382 for alias in [
1383 "reverse-nexti",
1384 "rnexti",
1385 "rni",
1386 "reverse-stepi",
1387 "rsi",
1388 "back",
1389 ] {
1390 assert_eq!(
1391 DebuggerCommand::parse(alias),
1392 DebuggerCommand::ReverseNextInstruction,
1393 "{alias}"
1394 );
1395 }
1396 for alias in ["reverse-step", "rstep", "rs"] {
1397 assert_eq!(DebuggerCommand::parse(alias), DebuggerCommand::ReverseStep);
1398 }
1399 for alias in ["reverse-continue", "rcontinue", "rc", "RC"] {
1400 assert_eq!(
1401 DebuggerCommand::parse(alias),
1402 DebuggerCommand::ReverseContinue
1403 );
1404 }
1405 for alias in ["reverse-finish", "rfinish", "rfin"] {
1406 assert_eq!(
1407 DebuggerCommand::parse(alias),
1408 DebuggerCommand::ReverseFinish
1409 );
1410 }
1411 }
1412
1413 #[test]
1414 fn steps_backward_over_the_recording_like_a_tape() {
1415 let mut state = DebuggerState::new();
1416 assert_eq!(state.previous_instruction(), StepOutcome::NoTrace);
1417 assert_eq!(state.reverse_continue(), StepOutcome::NoTrace);
1418
1419 state.load_trace(sample_trace());
1420 assert_eq!(
1421 state.previous_instruction(),
1422 StepOutcome::AtStart { step: 0 }
1423 );
1424
1425 state.goto_step(2);
1427 assert!(matches!(
1428 state.previous_instruction(),
1429 StepOutcome::Moved { step: 1, .. }
1430 ));
1431 assert!(matches!(
1432 state.previous_instruction(),
1433 StepOutcome::Moved { step: 0, .. }
1434 ));
1435 assert_eq!(
1436 state.previous_instruction(),
1437 StepOutcome::AtStart { step: 0 }
1438 );
1439 assert_eq!(state.current_step, 0);
1440
1441 assert!(matches!(
1443 state.goto_step(2),
1444 StepOutcome::Moved { step: 2, .. }
1445 ));
1446 assert_eq!(
1447 state.apply_command(DebuggerCommand::ReverseNextInstruction),
1448 Some(StepOutcome::Moved {
1449 step: 1,
1450 pc: sample_trace().steps[1].pc,
1451 op: sample_trace().steps[1].op.to_string(),
1452 })
1453 );
1454 }
1455
1456 #[test]
1457 fn reverse_continue_stops_at_earlier_breakpoints_then_the_start() {
1458 let mut state = DebuggerState::new();
1459 state.load_trace(sample_trace());
1460 let last = state.step_count() - 1;
1461 state.goto_step(last);
1462 let breakpoint_pc = sample_trace().steps[1].pc;
1463 state.set_breakpoint(breakpoint_pc);
1464
1465 assert!(matches!(
1466 state.reverse_continue(),
1467 StepOutcome::BreakpointHit { step: 1, pc, .. } if pc == breakpoint_pc
1468 ));
1469 assert_eq!(state.reverse_continue(), StepOutcome::AtStart { step: 0 });
1470 assert_eq!(state.current_step, 0);
1471 assert_eq!(
1472 state.apply_command(DebuggerCommand::ReverseContinue),
1473 Some(StepOutcome::AtStart { step: 0 })
1474 );
1475
1476 state.goto_step(1);
1478 assert_eq!(state.reverse_continue(), StepOutcome::AtStart { step: 0 });
1479 }
1480
1481 #[test]
1482 fn applies_state_changing_commands() {
1483 let mut state = DebuggerState::new();
1484 state.load_trace(sample_trace());
1485
1486 assert_eq!(
1487 state.apply_command(DebuggerCommand::Mode(Some(DisplayMode::Assembly))),
1488 Some(StepOutcome::ModeChanged(DisplayMode::Assembly))
1489 );
1490 assert_eq!(state.display_mode.as_str(), "asm");
1491 assert!(matches!(
1492 state.apply_command(DebuggerCommand::Break(BreakpointTarget::Pc(2), None)),
1493 Some(StepOutcome::BreakpointSet(_))
1494 ));
1495 assert!(matches!(
1496 state.apply_command(DebuggerCommand::NextInstruction),
1497 Some(StepOutcome::BreakpointHit { step: 1, pc: 2, .. })
1498 ));
1499 assert_eq!(state.apply_command(DebuggerCommand::Help(None)), None);
1500 assert_eq!(state.apply_command(DebuggerCommand::Backtrace), None);
1501 assert_eq!(state.apply_command(DebuggerCommand::Quit), None);
1502 }
1503
1504 #[test]
1505 fn a_state_variable_can_be_named_where_a_slot_would_go() {
1506 assert_eq!(
1509 DebuggerCommand::parse("break counter"),
1510 DebuggerCommand::Break(BreakpointTarget::Function("counter".to_owned()), None)
1511 );
1512 assert_eq!(
1513 DebuggerCommand::parse("break balances[0xabc]"),
1514 DebuggerCommand::Break(BreakpointTarget::State("balances[0xabc]".to_owned()), None)
1515 );
1516 assert_eq!(
1517 DebuggerCommand::parse("break config.limit"),
1518 DebuggerCommand::Break(BreakpointTarget::Function("config.limit".to_owned()), None)
1519 );
1520
1521 let mut state = DebuggerState::new();
1522 state.load_trace(sample_trace());
1523 assert!(matches!(
1525 state.set_breakpoint_target(&BreakpointTarget::State("counter".to_owned())),
1526 StepOutcome::BreakpointError(message) if message.contains("metadata")
1527 ));
1528 }
1529
1530 #[test]
1531 fn a_condition_gates_a_breakpoint_and_says_when_it_cannot_be_read() {
1532 let mut state = DebuggerState::new();
1533 state.load_trace(sample_trace());
1534
1535 assert!(matches!(
1537 state.set_conditional_breakpoint_target(
1538 &BreakpointTarget::Opcode("sstore".to_owned()),
1539 Some("pc == 3")
1540 ),
1541 StepOutcome::BreakpointSet(breakpoint)
1542 if breakpoint.label() == "opcode SSTORE if pc == 3"
1543 ));
1544 assert!(matches!(
1545 state.continue_execution(),
1546 StepOutcome::BreakpointHit { step: 2, .. }
1547 ));
1548
1549 state.goto_step(0);
1551 state.delete_breakpoint(1);
1552 assert!(matches!(
1553 state.set_conditional_breakpoint_target(
1554 &BreakpointTarget::Opcode("sstore".to_owned()),
1555 Some("pc == 999")
1556 ),
1557 StepOutcome::BreakpointSet(_)
1558 ));
1559 assert!(matches!(
1560 state.continue_execution(),
1561 StepOutcome::AtEnd { .. }
1562 ));
1563 assert_eq!(state.take_note(), None);
1564
1565 state.goto_step(0);
1567 state.delete_breakpoint(2);
1568 assert!(matches!(
1569 state.set_conditional_breakpoint_target(
1570 &BreakpointTarget::Opcode("sstore".to_owned()),
1571 Some("pc == 999 || gas >= 94 && depth == 1")
1572 ),
1573 StepOutcome::BreakpointSet(_)
1574 ));
1575 assert!(matches!(
1576 state.continue_execution(),
1577 StepOutcome::BreakpointHit { step: 2, .. }
1578 ));
1579
1580 state.goto_step(0);
1583 state.delete_breakpoint(3);
1584 assert!(matches!(
1585 state.set_conditional_breakpoint_target(
1586 &BreakpointTarget::Opcode("sstore".to_owned()),
1587 Some("counter > 1")
1588 ),
1589 StepOutcome::BreakpointSet(_)
1590 ));
1591 assert!(matches!(
1592 state.continue_execution(),
1593 StepOutcome::AtEnd { .. }
1594 ));
1595 let note = state.take_note().expect("a reason");
1596 assert!(
1597 note.contains("`counter > 1` could not be evaluated"),
1598 "{note}"
1599 );
1600 assert_eq!(state.take_note(), None, "the note is taken once");
1601
1602 assert!(matches!(
1604 state.set_conditional_breakpoint_target(
1605 &BreakpointTarget::Opcode("sstore".to_owned()),
1606 Some("pc ==")
1607 ),
1608 StepOutcome::BreakpointError(_)
1609 ));
1610 }
1611
1612 #[test]
1613 fn tape_breakpoints_search_the_recording() {
1614 let mut state = DebuggerState::new();
1615 state.load_trace(sample_trace());
1616
1617 assert!(matches!(
1619 state.set_breakpoint_target(&BreakpointTarget::Storage("0x00".to_owned())),
1620 StepOutcome::BreakpointSet(breakpoint)
1621 if breakpoint.kind == BreakpointKind::Storage("0".to_owned())
1622 && breakpoint.label() == "storage slot 0x0"
1623 ));
1624 assert!(matches!(
1625 state.continue_execution(),
1626 StepOutcome::BreakpointHit { step: 2, .. }
1627 ));
1628 assert_eq!(
1629 state.set_breakpoint_target(&BreakpointTarget::Storage("zz".to_owned())),
1630 StepOutcome::BreakpointError(
1631 "invalid storage slot `zz`; expected decimal or hex".to_owned()
1632 )
1633 );
1634 assert!(matches!(
1635 state.clear_breakpoint_target(&BreakpointTarget::Storage("0".to_owned())),
1636 StepOutcome::BreakpointCleared(_)
1637 ));
1638
1639 state.goto_step(0);
1640 let callee = "0x0000000000000000000000000000000000000002";
1641 assert!(matches!(
1642 state.set_breakpoint_target(&BreakpointTarget::Call(Some(callee.to_owned()))),
1643 StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == format!("call to {callee}")
1644 ));
1645 assert!(matches!(
1646 state.continue_execution(),
1647 StepOutcome::BreakpointHit { step: 3, .. }
1648 ));
1649 assert!(matches!(
1650 state.set_breakpoint_target(&BreakpointTarget::Call(Some("0x12".to_owned()))),
1651 StepOutcome::BreakpointError(message) if message.contains("20 bytes")
1652 ));
1653 state.goto_step(0);
1654 assert!(matches!(
1655 state.set_breakpoint_target(&BreakpointTarget::Opcode("mstore".to_owned())),
1656 StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == "opcode MSTORE"
1657 ));
1658 assert!(matches!(
1659 state.continue_execution(),
1660 StepOutcome::BreakpointHit { step: 1, .. }
1661 ));
1662 assert!(matches!(
1663 state.set_breakpoint_target(&BreakpointTarget::Revert),
1664 StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == "revert"
1665 ));
1666 assert!(matches!(
1668 state.continue_execution(),
1669 StepOutcome::BreakpointHit { step: 3, .. }
1670 ));
1671 assert_eq!(state.breakpoints().len(), 3);
1672
1673 assert_eq!(
1675 state.set_breakpoint_target(&BreakpointTarget::Function("f".to_owned())),
1676 StepOutcome::BreakpointError(
1677 "function breakpoints require compiler debug metadata; start the session with `--ethdebug-dir <address>:<contract>:<dir>`"
1678 .to_owned()
1679 )
1680 );
1681 assert!(matches!(
1682 state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
1683 file: None,
1684 line: 1
1685 })),
1686 StepOutcome::BreakpointError(message) if message.starts_with("source breakpoints require")
1687 ));
1688 }
1689
1690 #[test]
1691 fn calldata_and_frames_without_debug_info() {
1692 let mut state = DebuggerState::new();
1693 assert!(state.frames().is_empty());
1694 assert_eq!(state.calldata(), None);
1695 state.load_trace(sample_trace());
1696 assert_eq!(state.calldata().as_deref(), Some("0xabcd"));
1697 let frames = state.frames();
1698 assert_eq!(frames.len(), 1);
1699 assert_eq!(frames[0].address.as_deref(), Some("0x2"));
1700 assert!(state.location().is_none());
1701 assert!(state.source_listing(2).is_none());
1702 assert!(state.current_call().is_none());
1703 }
1704
1705 const SOURCE: &str = "\
1710contract C {
1711 function outer(uint256 a) public {
1712 uint256 b = a + 1;
1713 inner(b);
1714 b = 0;
1715 }
1716 function inner(uint256 x) internal {
1717 x += 1;
1718 }
1719}
1720";
1721
1722 fn source_state() -> DebuggerState {
1723 let offset = |needle: &str| SOURCE.find(needle).expect(needle) as u64;
1724 let whole = SOURCE.len() as u64;
1725 let instruction = |pc: u64, offset: u64, length: u64| -> Instruction {
1726 serde_json::from_value(json!({
1727 "offset": pc,
1728 "operation": {"mnemonic": "JUMPDEST"},
1729 "context": {"code": {"source": {"id": 0}, "range": {"offset": offset, "length": length}}}
1730 }))
1731 .expect("instruction")
1732 };
1733 let info = EthdebugInfo {
1734 compilation: serde_json::Value::Null,
1735 contract_name: "C".to_owned(),
1736 environment: "runtime".to_owned(),
1737 instructions: vec![
1738 instruction(0, 0, whole),
1739 instruction(1, 0, whole),
1740 instruction(10, offset("function outer"), 90),
1741 instruction(11, offset("uint256 b = a + 1;"), 18),
1742 instruction(12, offset("inner(b);"), 9),
1743 instruction(13, offset("inner(b);"), 9),
1744 instruction(14, offset("b = 0;"), 6),
1745 instruction(20, offset("function inner"), 50),
1746 instruction(21, offset("x += 1;"), 7),
1747 instruction(30, 0, whole),
1748 ],
1749 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
1750 variable_locations: BTreeMap::new(),
1751 };
1752 let contract =
1753 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
1754 let pcs = [0, 1, 10, 11, 12, 20, 21, 21, 13, 14, 30];
1755 let mut trace = sample_trace();
1756 trace.steps = pcs.iter().map(|pc| step(*pc, "JUMPDEST", 0, &[])).collect();
1757 trace.steps[6].op = "SSTORE".into();
1758 trace.steps[6].stack = vec!["0x2a".into(), "0x5".into()];
1759 let mut state = DebuggerState::new();
1760 state.load_trace(trace);
1761 state.attach_debug_info(vec![contract]);
1762 state
1763 }
1764
1765 #[test]
1766 fn source_stepping_moves_by_line_and_frame() {
1767 let mut state = source_state();
1768 assert!(state.has_source());
1769 assert!(matches!(
1770 state.next_source(),
1771 StepOutcome::Moved { step: 2, .. }
1772 ));
1773 assert_eq!(state.location().expect("location").line, 2);
1774 assert!(matches!(
1775 state.next_source(),
1776 StepOutcome::Moved { step: 3, .. }
1777 ));
1778 assert!(matches!(
1779 state.next_source(),
1780 StepOutcome::Moved { step: 4, .. }
1781 ));
1782 assert!(matches!(
1784 state.next_source(),
1785 StepOutcome::Moved { step: 9, .. }
1786 ));
1787 state.goto_step(4);
1788 assert!(matches!(
1789 state.step_into(),
1790 StepOutcome::Moved { step: 5, .. }
1791 ));
1792 assert_eq!(
1793 state.location().expect("location").function_name.as_deref(),
1794 Some("inner")
1795 );
1796 assert!(matches!(
1797 state.step_into(),
1798 StepOutcome::Moved { step: 6, .. }
1799 ));
1800 assert!(matches!(state.finish(), StepOutcome::Moved { step: 8, .. }));
1802 state.goto_step(6);
1803 assert!(matches!(
1804 state.reverse_finish(),
1805 StepOutcome::Moved { step: 4, .. }
1806 ));
1807 state.goto_step(9);
1809 assert!(matches!(
1810 state.previous_source(),
1811 StepOutcome::Moved { step: 4, .. }
1812 ));
1813 assert!(matches!(
1814 state.reverse_step_into(),
1815 StepOutcome::Moved { step: 3, .. }
1816 ));
1817 state.goto_step(9);
1818 assert!(matches!(
1819 state.reverse_step_into(),
1820 StepOutcome::Moved { step: 8, .. }
1821 ));
1822 state.goto_step(10);
1824 assert_eq!(state.next_source(), StepOutcome::AtEnd { step: 10 });
1825 assert_eq!(state.finish(), StepOutcome::AtEnd { step: 10 });
1826 state.goto_step(9);
1827 assert_eq!(state.finish(), StepOutcome::AtEnd { step: 10 });
1828 state.goto_step(0);
1829 assert_eq!(state.reverse_finish(), StepOutcome::AtStart { step: 0 });
1830 state.goto_step(1);
1831 assert_eq!(state.previous_source(), StepOutcome::AtStart { step: 0 });
1832
1833 let frames = state.frames();
1834 assert_eq!(frames.len(), 1);
1835 state.goto_step(6);
1836 let frames = state.frames();
1837 assert_eq!(frames.len(), 3);
1838 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
1839 assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
1840 let listing = state.source_listing(1).expect("listing");
1841 assert_eq!(listing.current_line, 8);
1842 }
1843
1844 #[test]
1845 fn a_breakpoint_names_each_target_once() {
1846 let line = |contract: usize| ResolvedLine {
1849 key: LineKey {
1850 contract,
1851 source_id: 0,
1852 line: 4,
1853 },
1854 path: "C.sol".to_owned(),
1855 requested_line: 4,
1856 };
1857 let lines = Breakpoint {
1858 id: 1,
1859 kind: BreakpointKind::Line(vec![line(0), line(1)]),
1860 condition: None,
1861 };
1862 assert_eq!(lines.label(), "C.sol:4");
1863
1864 let function = |contract: usize| ResolvedFunction {
1865 id: FunctionId {
1866 contract,
1867 function: 1,
1868 },
1869 name: "inner".to_owned(),
1870 contract_name: "C".to_owned(),
1871 path: "C.sol".to_owned(),
1872 line: 7,
1873 };
1874 let functions = Breakpoint {
1875 id: 2,
1876 kind: BreakpointKind::Function(vec![function(0), function(1)]),
1877 condition: None,
1878 };
1879 assert_eq!(functions.label(), "function C.inner at C.sol:7");
1880
1881 let mut other = line(1);
1883 other.key.line = 1;
1884 other.requested_line = 10;
1885 let mixed = Breakpoint {
1886 id: 3,
1887 kind: BreakpointKind::Line(vec![line(0), other]),
1888 condition: None,
1889 };
1890 assert_eq!(
1891 mixed.label(),
1892 "C.sol:4, C.sol:1 (the statement containing line 10)"
1893 );
1894 }
1895
1896 #[test]
1897 fn variables_are_inferred_from_the_stack_when_the_artifact_has_no_locations() {
1898 let source = "contract C {\n function f(uint256 a) public {\n uint256 b = a;\n b = 0;\n }\n}\n";
1899 let offset = |needle: &str| source.find(needle).expect(needle) as u64;
1900 let instruction = |pc: u64, needle: &str| -> Instruction {
1901 serde_json::from_value(json!({
1902 "offset": pc,
1903 "operation": {"mnemonic": "JUMPDEST"},
1904 "context": {"code": {"source": {"id": 0}, "range": {"offset": offset(needle), "length": needle.len()}}}
1905 }))
1906 .expect("instruction")
1907 };
1908 let contract = |code_generator: Option<CodeGenerator>| {
1909 let info = EthdebugInfo {
1910 compilation: serde_json::Value::Null,
1911 contract_name: "C".to_owned(),
1912 environment: "call".to_owned(),
1913 instructions: vec![
1914 instruction(0, "contract C"),
1915 instruction(1, "function f"),
1916 instruction(2, "uint256 b"),
1917 instruction(3, "b = 0"),
1918 ],
1919 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
1920 variable_locations: BTreeMap::new(),
1921 };
1922 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, source.to_owned())]))
1923 .with_code_generator(code_generator)
1924 };
1925 let mut trace = sample_trace();
1926 trace.steps = vec![
1927 step(0, "PUSH1", 0, &[]),
1928 step(1, "JUMPDEST", 0, &["0x9", "0x5"]),
1929 step(2, "PUSH0", 0, &["0x9", "0x5"]),
1930 step(3, "POP", 0, &["0x9", "0x5", "0x5"]),
1931 ];
1932
1933 let mut state = DebuggerState::new();
1934 state.load_trace(trace.clone());
1935 state.attach_debug_info(vec![contract(Some(CodeGenerator::Legacy))]);
1936 assert_eq!(
1937 state.variables().unwrap_err(),
1938 "no function is executing at this step"
1939 );
1940 state.goto_step(3);
1941 let variables = state.variables().expect("inferred");
1942 assert_eq!(variables.origin, VariablesOrigin::Inferred);
1943 assert_eq!(
1944 variables
1945 .variables
1946 .iter()
1947 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
1948 .collect::<Vec<_>>(),
1949 [("a", "5"), ("b", "5")]
1950 );
1951
1952 let mut state = DebuggerState::new();
1954 state.load_trace(trace);
1955 state.attach_debug_info(vec![contract(Some(CodeGenerator::ViaIr))]);
1956 state.goto_step(3);
1957 assert!(state.variables().unwrap_err().contains("via-IR pipeline"));
1958 }
1959
1960 #[test]
1961 fn line_and_function_breakpoints_stop_on_entry() {
1962 let mut state = source_state();
1963 let set =
1964 state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
1965 file: Some("C.sol".to_owned()),
1966 line: 4,
1967 }));
1968 let StepOutcome::BreakpointSet(breakpoint) = &set else {
1969 panic!("{set:?}");
1970 };
1971 assert_eq!(breakpoint.label(), "C.sol:4");
1972 assert!(matches!(
1974 state.continue_execution(),
1975 StepOutcome::BreakpointHit { step: 4, .. }
1976 ));
1977 assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 10 });
1978 state.goto_step(3);
1980 assert!(matches!(
1981 state.next_source(),
1982 StepOutcome::BreakpointHit { step: 4, .. }
1983 ));
1984 assert!(matches!(
1985 state.clear_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
1986 file: None,
1987 line: 4,
1988 })),
1989 StepOutcome::BreakpointCleared(_)
1990 ));
1991 assert_eq!(
1992 state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
1993 file: Some("Other.sol".to_owned()),
1994 line: 4,
1995 })),
1996 StepOutcome::BreakpointError("source file not found: Other.sol".to_owned())
1997 );
1998 assert!(matches!(
2000 state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
2001 file: None,
2002 line: 10,
2003 })),
2004 StepOutcome::BreakpointSet(breakpoint)
2005 if breakpoint.label() == "C.sol:1 (the statement containing line 10)"
2006 ));
2007
2008 state.goto_step(0);
2009 let set = state.set_breakpoint_target(&BreakpointTarget::Function("inner".to_owned()));
2010 let StepOutcome::BreakpointSet(breakpoint) = &set else {
2011 panic!("{set:?}");
2012 };
2013 assert_eq!(breakpoint.label(), "function C.inner at C.sol:7");
2014 assert!(matches!(
2015 state.continue_execution(),
2016 StepOutcome::BreakpointHit { step: 5, .. }
2017 ));
2018 state.goto_step(10);
2020 assert!(matches!(
2021 state.reverse_continue(),
2022 StepOutcome::BreakpointHit { step: 5, .. }
2023 ));
2024 assert!(matches!(
2025 state.set_breakpoint_target(&BreakpointTarget::Function("nothing".to_owned())),
2026 StepOutcome::BreakpointError(message) if message.contains("no function named")
2027 ));
2028 assert!(matches!(
2029 state.clear_breakpoint_target(&BreakpointTarget::Function("C.inner".to_owned())),
2030 StepOutcome::BreakpointCleared(_)
2031 ));
2032 assert!(matches!(
2033 state.clear_breakpoint_target(&BreakpointTarget::Function("C.inner".to_owned())),
2034 StepOutcome::BreakpointMissing(label) if label == "function C.inner at C.sol:7"
2035 ));
2036 }
2037
2038 #[test]
2039 fn a_breakpoint_condition_reads_an_inferred_local() {
2040 let source = "contract C {\n enum Mode { Off, On }\n function f(uint256 a, Mode m) public {\n uint256 b = a;\n b = 0;\n }\n}\n";
2041 let offset = |needle: &str| source.find(needle).expect(needle) as u64;
2042 let instruction = |pc: u64, needle: &str| -> Instruction {
2043 serde_json::from_value(json!({
2044 "offset": pc,
2045 "operation": {"mnemonic": "JUMPDEST"},
2046 "context": {"code": {"source": {"id": 0}, "range": {"offset": offset(needle), "length": needle.len()}}}
2047 }))
2048 .expect("instruction")
2049 };
2050 let info = EthdebugInfo {
2051 compilation: serde_json::Value::Null,
2052 contract_name: "C".to_owned(),
2053 environment: "call".to_owned(),
2054 instructions: vec![
2055 instruction(0, "contract C"),
2056 instruction(1, "function f"),
2057 instruction(2, "uint256 b"),
2058 instruction(3, "b = 0"),
2059 ],
2060 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
2061 variable_locations: BTreeMap::new(),
2062 };
2063 let contract =
2064 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, source.to_owned())]))
2065 .with_code_generator(Some(CodeGenerator::Legacy));
2066 let mut trace = sample_trace();
2067 trace.steps = vec![
2068 step(0, "PUSH1", 0, &[]),
2069 step(1, "JUMPDEST", 0, &["0x9", "0x5", "0x1"]),
2070 step(2, "PUSH0", 0, &["0x9", "0x5", "0x1"]),
2071 step(3, "POP", 0, &["0x9", "0x5", "0x1", "0x5"]),
2072 ];
2073 let mut state = DebuggerState::new();
2074 state.load_trace(trace);
2075 state.attach_debug_info(vec![contract]);
2076
2077 let hit = |condition: &str| -> (Option<usize>, Option<String>) {
2080 let mut state = state.clone();
2081 let DebuggerCommand::Break(target, condition) =
2082 DebuggerCommand::parse(&format!("break C.sol:5 if {condition}"))
2083 else {
2084 panic!("break command");
2085 };
2086 state.set_conditional_breakpoint_target(&target, condition.as_deref());
2087 let step = match state.continue_execution() {
2088 StepOutcome::BreakpointHit { .. } => Some(state.current_step),
2089 _ => None,
2090 };
2091 (step, state.take_note())
2092 };
2093 assert_eq!(hit("b == 5"), (Some(3), None));
2094 assert_eq!(hit("b == a"), (Some(3), None));
2095 assert_eq!(hit("m == Mode.On"), (Some(3), None));
2096 assert_eq!(hit("m == Mode.Off").0, None);
2097 assert_eq!(hit("b > 5").0, None);
2098 let (step, note) = hit("c == 5");
2100 assert_eq!(step, None);
2101 let note = note.expect("note");
2102 assert!(note.contains("`c == 5` could not be evaluated"), "{note}");
2103 assert!(
2104 note.contains("`c` is not a step value, a local, or an argument here"),
2105 "{note}"
2106 );
2107 }
2108
2109 fn sample_trace() -> TransactionTrace {
2110 TransactionTrace {
2111 tx_hash: Some("0xabc".to_owned()),
2112 from_addr: "0x1".to_owned(),
2113 to_addr: Some("0x2".to_owned()),
2114 value: "0x0".to_owned(),
2115 input_data: "0xabcd".to_owned(),
2116 gas_used: 21_000,
2117 output: "0x".to_owned(),
2118 success: true,
2119 error: None,
2120 debug_trace_available: true,
2121 contract_address: None,
2122 backend: Some("debug-rpc".to_owned()),
2123 capabilities: Default::default(),
2124 artifacts: Default::default(),
2125 steps: vec![
2126 step(0, "PUSH1", 100, &[]),
2127 step(2, "MSTORE", 97, &[]),
2128 step(3, "SSTORE", 94, &["0x2a", "0x0"]),
2129 {
2130 let mut last = step(
2131 4,
2132 "CALL",
2133 92,
2134 &[
2135 "0x0",
2136 "0x0",
2137 "0x0",
2138 "0x0",
2139 "0x0",
2140 "0x0000000000000000000000000000000000000000000000000000000000000002",
2141 "0x5208",
2142 ],
2143 );
2144 last.error = Some("execution reverted".to_owned());
2145 last
2146 },
2147 ],
2148 }
2149 }
2150
2151 fn step(pc: u64, op: &str, gas: u64, stack: &[&str]) -> TraceStep {
2152 TraceStep {
2153 pc,
2154 op: op.into(),
2155 gas,
2156 gas_cost: 1,
2157 depth: 1,
2158 stack: stack
2159 .iter()
2160 .map(|word| soldb_core::Word::from(*word))
2161 .collect(),
2162 memory: None,
2163 storage: Some(BTreeMap::new()),
2164 error: None,
2165 snapshot: StepSnapshot::default(),
2166 }
2167 }
2168}