use std::cell::RefCell;
use soldb_core::{opcode, SoldbResult, StepView, TransactionTrace};
mod command;
mod render;
mod response;
mod session;
pub use command::{
command_spec, CommandGroup, CommandSpec, DebuggerCommand, DebuggerInfoCommand, COMMANDS,
};
pub use render::{shorten_hex, Renderer};
pub use response::{
ArgumentInfo, BreakpointEvent, BreakpointInfo, FrameInfo, Level, ListedLine, LoadedContract,
MemoryInfo, MemoryWord, Output, ResourceInfo, SlotInfo, StateInfo, Stop, StopLocation,
StopReason, ValueStatus, VariableInfo,
};
pub use session::{
breakpoint_lines, Session, FRAME_ARGUMENTS_NOTE, FRAME_ARGUMENTS_WARNING, LISTING_RADIUS,
};
use soldb_debugger::{
call_target, check_root_calls, normalize_address, variables_for_step, ChainStorage, Condition,
ConditionContext, ContractDebugInfo, DebugVariable, Evaluation, Frame, FrameState,
LocalsStatus, ResolvedFunction, ResolvedLine, SourceListing, StepLocation, StepMap,
StorageLayout, StorageTape, StorageWords,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariablesOrigin {
Ethdebug,
Inferred,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepVariables {
pub variables: Vec<DebugVariable>,
pub origin: VariablesOrigin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayMode {
Source,
Assembly,
}
impl DisplayMode {
pub fn parse(input: &str) -> Option<Self> {
match input.trim().to_ascii_lowercase().as_str() {
"source" | "src" => Some(Self::Source),
"asm" | "assembly" => Some(Self::Assembly),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Source => "source",
Self::Assembly => "asm",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BreakpointTarget {
Pc(u64),
SourceLine(SourceBreakpointTarget),
Function(String),
Storage(String),
State(String),
Revert,
Call(Option<String>),
Opcode(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceBreakpointTarget {
pub file: Option<String>,
pub line: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Breakpoint {
pub id: u32,
pub kind: BreakpointKind,
pub condition: Option<Condition>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BreakpointKind {
Pc(u64),
Line(Vec<ResolvedLine>),
Function(Vec<ResolvedFunction>),
Storage(String),
StateWrite {
path: String,
slot: String,
},
Revert,
Call(Option<String>),
Opcode(String),
}
impl Breakpoint {
#[must_use]
pub fn label(&self) -> String {
let target = match &self.kind {
BreakpointKind::Pc(pc) => format!("PC {pc}"),
BreakpointKind::Line(lines) => unique(lines.iter().map(|line| {
if line.requested_line == line.key.line {
format!("{}:{}", line.path, line.key.line)
} else {
format!(
"{}:{} (the statement containing line {})",
line.path, line.key.line, line.requested_line
)
}
}))
.join(", "),
BreakpointKind::Function(functions) => unique(functions.iter().map(|function| {
format!(
"function {}.{} at {}:{}",
function.contract_name, function.name, function.path, function.line
)
}))
.join(", "),
BreakpointKind::Storage(slot) => format!("storage slot 0x{slot}"),
BreakpointKind::StateWrite { path, slot } => {
format!("a write to `{path}` (storage slot 0x{slot})")
}
BreakpointKind::Revert => "revert".to_owned(),
BreakpointKind::Call(Some(address)) => format!("call to {address}"),
BreakpointKind::Call(None) => "any call".to_owned(),
BreakpointKind::Opcode(mnemonic) => format!("opcode {mnemonic}"),
};
match &self.condition {
Some(condition) => format!("{target} if {}", condition.text()),
None => target,
}
}
}
fn unique(names: impl Iterator<Item = String>) -> Vec<String> {
let mut seen = Vec::new();
for name in names {
if !seen.contains(&name) {
seen.push(name);
}
}
seen
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepOutcome {
NoTrace,
Moved {
step: usize,
pc: u64,
op: String,
},
BreakpointHit {
step: usize,
pc: u64,
breakpoint: Breakpoint,
},
AtEnd {
step: usize,
},
AtStart {
step: usize,
},
InvalidStep {
requested: usize,
max_step: Option<usize>,
},
ModeChanged(DisplayMode),
BreakpointSet(Breakpoint),
BreakpointCleared(Breakpoint),
BreakpointMissing(String),
BreakpointError(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DebuggerState {
pub current_step: usize,
pub display_mode: DisplayMode,
breakpoints: Vec<Breakpoint>,
next_breakpoint_id: u32,
trace: Option<TransactionTrace>,
step_map: Option<StepMap>,
storage_tape: Option<StorageTape>,
transient_tape: Option<StorageTape>,
note: RefCell<Option<String>>,
}
impl Default for DebuggerState {
fn default() -> Self {
Self {
current_step: 0,
display_mode: DisplayMode::Source,
breakpoints: Vec::new(),
next_breakpoint_id: 1,
trace: None,
step_map: None,
storage_tape: None,
transient_tape: None,
note: RefCell::new(None),
}
}
}
impl DebuggerState {
pub fn new() -> Self {
Self::default()
}
pub fn load_trace(&mut self, trace: TransactionTrace) -> SoldbResult<()> {
self.load_trace_with_debug_info(trace, Vec::new())
}
pub fn load_trace_with_debug_info(
&mut self,
trace: TransactionTrace,
contracts: Vec<ContractDebugInfo>,
) -> SoldbResult<()> {
check_root_calls(&trace)?;
let map = StepMap::new(&trace, contracts);
let (storage_tape, transient_tape) = StorageTape::persistent_and_transient(&trace, &map);
self.storage_tape = Some(storage_tape);
self.transient_tape = Some(transient_tape);
self.step_map = Some(map);
self.trace = Some(trace);
self.current_step = 0;
Ok(())
}
pub fn attach_debug_info(&mut self, contracts: Vec<ContractDebugInfo>) {
if let Some(trace) = &self.trace {
let map = StepMap::new(trace, contracts);
let (storage_tape, transient_tape) = StorageTape::persistent_and_transient(trace, &map);
self.storage_tape = Some(storage_tape);
self.transient_tape = Some(transient_tape);
self.step_map = Some(map);
}
}
#[must_use]
pub fn storage_words(&self) -> Option<StorageWords<'_>> {
let map = self.step_map.as_ref()?;
Some(self.storage_tape.as_ref()?.at_step(map, self.current_step))
}
#[must_use]
pub fn transient_words(&self) -> Option<StorageWords<'_>> {
let map = self.step_map.as_ref()?;
Some(
self.transient_tape
.as_ref()?
.at_step(map, self.current_step),
)
}
#[must_use]
pub fn storage_words_with_chain<'a>(
&'a self,
chain: Option<&'a dyn ChainStorage>,
) -> Option<StorageWords<'a>> {
Some(self.storage_words()?.with_chain(chain))
}
#[must_use]
pub fn storage_address(&self) -> Option<&str> {
self.step_map.as_ref()?.storage_address(self.current_step)
}
#[must_use]
pub fn storage_layout(&self) -> Option<&StorageLayout> {
self.step_map
.as_ref()?
.contract_at_step(self.current_step)?
.storage_layout
.as_ref()
}
#[must_use]
pub fn transient_storage_layout(&self) -> Option<&StorageLayout> {
self.step_map
.as_ref()?
.contract_at_step(self.current_step)?
.transient_storage_layout
.as_ref()
}
pub fn trace(&self) -> Option<&TransactionTrace> {
self.trace.as_ref()
}
#[must_use]
pub fn step_map(&self) -> Option<&StepMap> {
self.step_map.as_ref()
}
#[must_use]
pub fn has_source(&self) -> bool {
self.step_map.as_ref().is_some_and(StepMap::has_source)
}
pub fn current_step_data(&self) -> Option<StepView<'_>> {
self.trace
.as_ref()
.and_then(|trace| trace.steps.get(self.current_step))
}
#[must_use]
pub fn location(&self) -> Option<StepLocation> {
self.step_map.as_ref()?.location(self.current_step)
}
pub fn step_count(&self) -> usize {
self.trace
.as_ref()
.map(|trace| trace.steps.len())
.unwrap_or(0)
}
#[must_use]
pub fn breakpoints(&self) -> &[Breakpoint] {
&self.breakpoints
}
pub fn set_display_mode(&mut self, mode: DisplayMode) -> StepOutcome {
self.display_mode = mode;
StepOutcome::ModeChanged(mode)
}
pub fn next_instruction(&mut self) -> StepOutcome {
let Some(trace) = &self.trace else {
return StepOutcome::NoTrace;
};
if self.current_step >= trace.steps.len().saturating_sub(1) {
return StepOutcome::AtEnd {
step: self.current_step,
};
}
self.current_step += 1;
self.outcome_for_current_step()
}
pub fn next_source(&mut self) -> StepOutcome {
if !self.has_source() {
return self.next_instruction();
}
let target = self
.step_map
.as_ref()
.and_then(|map| map.next_source(self.current_step));
self.advance_to(target)
}
pub fn step_into(&mut self) -> StepOutcome {
if !self.has_source() {
return self.next_instruction();
}
let target = self
.step_map
.as_ref()
.and_then(|map| map.step_into(self.current_step));
self.advance_to(target)
}
pub fn finish(&mut self) -> StepOutcome {
if self.trace.is_none() {
return StepOutcome::NoTrace;
}
let target = self
.step_map
.as_ref()
.and_then(|map| map.finish(self.current_step));
self.advance_to(target)
}
pub fn continue_execution(&mut self) -> StepOutcome {
if self.trace.is_none() {
return StepOutcome::NoTrace;
}
self.advance_to(None)
}
pub fn previous_instruction(&mut self) -> StepOutcome {
if self.trace.is_none() {
return StepOutcome::NoTrace;
}
if self.current_step == 0 {
return StepOutcome::AtStart { step: 0 };
}
self.current_step -= 1;
self.outcome_for_current_step()
}
pub fn previous_source(&mut self) -> StepOutcome {
if !self.has_source() {
return self.previous_instruction();
}
let target = self
.step_map
.as_ref()
.and_then(|map| map.previous_source(self.current_step));
self.retreat_to(target)
}
pub fn reverse_step_into(&mut self) -> StepOutcome {
if !self.has_source() {
return self.previous_instruction();
}
let target = self
.step_map
.as_ref()
.and_then(|map| map.reverse_step_into(self.current_step));
self.retreat_to(target)
}
pub fn reverse_finish(&mut self) -> StepOutcome {
if self.trace.is_none() {
return StepOutcome::NoTrace;
}
let target = self
.step_map
.as_ref()
.and_then(|map| map.reverse_finish(self.current_step));
self.retreat_to(target)
}
pub fn reverse_continue(&mut self) -> StepOutcome {
if self.trace.is_none() {
return StepOutcome::NoTrace;
}
self.retreat_to(None)
}
fn advance_to(&mut self, target: Option<usize>) -> StepOutcome {
let Some(trace) = &self.trace else {
return StepOutcome::NoTrace;
};
let last = trace.steps.len().saturating_sub(1);
if self.current_step >= last {
return StepOutcome::AtEnd {
step: self.current_step,
};
}
let target = target.unwrap_or(last).min(last);
while self.current_step < target {
self.current_step += 1;
if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
return StepOutcome::BreakpointHit {
step: self.current_step,
pc: self.pc_at(self.current_step),
breakpoint,
};
}
}
if target == last {
return StepOutcome::AtEnd { step: last };
}
self.outcome_for_current_step()
}
fn retreat_to(&mut self, target: Option<usize>) -> StepOutcome {
if self.trace.is_none() {
return StepOutcome::NoTrace;
}
if self.current_step == 0 {
return StepOutcome::AtStart { step: 0 };
}
let target = target.unwrap_or(0);
while self.current_step > target {
self.current_step -= 1;
if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
return StepOutcome::BreakpointHit {
step: self.current_step,
pc: self.pc_at(self.current_step),
breakpoint,
};
}
}
if target == 0 {
return StepOutcome::AtStart { step: 0 };
}
self.outcome_for_current_step()
}
pub fn goto_step(&mut self, step: usize) -> StepOutcome {
let Some(trace) = &self.trace else {
return StepOutcome::NoTrace;
};
if step >= trace.steps.len() {
return StepOutcome::InvalidStep {
requested: step,
max_step: trace.steps.len().checked_sub(1),
};
}
self.current_step = step;
self.outcome_for_current_step()
}
pub fn set_breakpoint(&mut self, pc: u64) -> StepOutcome {
self.add_breakpoint(BreakpointKind::Pc(pc), None)
}
pub fn clear_breakpoint(&mut self, pc: u64) -> StepOutcome {
self.remove_breakpoint(&BreakpointKind::Pc(pc), &format!("PC {pc}"))
}
pub fn set_breakpoint_target(&mut self, target: &BreakpointTarget) -> StepOutcome {
self.set_conditional_breakpoint_target(target, None)
}
pub fn set_conditional_breakpoint_target(
&mut self,
target: &BreakpointTarget,
condition: Option<&str>,
) -> StepOutcome {
let condition = match condition.map(Condition::parse).transpose() {
Ok(condition) => condition,
Err(message) => return StepOutcome::BreakpointError(message),
};
match self.resolve_target(target) {
Ok(kind) => {
if !self.matches_any_step(&kind) {
let label = Breakpoint {
id: 0,
kind: kind.clone(),
condition: None,
}
.label();
self.push_note(format!(
"no step of this trace reaches {label}, so this breakpoint cannot be hit"
));
}
self.add_breakpoint(kind, condition)
}
Err(message) => StepOutcome::BreakpointError(message),
}
}
pub fn clear_breakpoint_target(&mut self, target: &BreakpointTarget) -> StepOutcome {
match self.resolve_target(target) {
Ok(kind) => {
let label = Breakpoint {
id: 0,
kind: kind.clone(),
condition: None,
}
.label();
self.remove_breakpoint(&kind, &label)
}
Err(message) => StepOutcome::BreakpointError(message),
}
}
pub fn delete_breakpoint(&mut self, id: u32) -> StepOutcome {
match self
.breakpoints
.iter()
.position(|breakpoint| breakpoint.id == id)
{
Some(index) => StepOutcome::BreakpointCleared(self.breakpoints.remove(index)),
None => StepOutcome::BreakpointMissing(format!("#{id}")),
}
}
fn add_breakpoint(
&mut self,
kind: BreakpointKind,
condition: Option<Condition>,
) -> StepOutcome {
if let Some(existing) = self
.breakpoints
.iter()
.find(|breakpoint| breakpoint.kind == kind && breakpoint.condition == condition)
{
return StepOutcome::BreakpointSet(existing.clone());
}
let breakpoint = Breakpoint {
id: self.next_breakpoint_id,
kind,
condition,
};
self.next_breakpoint_id += 1;
self.breakpoints.push(breakpoint.clone());
StepOutcome::BreakpointSet(breakpoint)
}
fn remove_breakpoint(&mut self, kind: &BreakpointKind, label: &str) -> StepOutcome {
match self
.breakpoints
.iter()
.position(|breakpoint| breakpoint.kind == *kind)
{
Some(index) => StepOutcome::BreakpointCleared(self.breakpoints.remove(index)),
None => StepOutcome::BreakpointMissing(label.to_owned()),
}
}
fn resolve_state_target(&self, path: &str) -> Result<BreakpointKind, String> {
let map = self.source_map_for("state breakpoints")?;
let layouts = map
.contracts()
.iter()
.filter_map(|contract| contract.storage_layout.as_ref());
let mut failure = None;
for layout in layouts {
match layout.resolve(path) {
Ok(reference) => {
let slot = soldb_debugger::short_hex(&reference.slot);
return Ok(BreakpointKind::StateWrite {
path: reference.path,
slot: slot.trim_start_matches("0x").to_owned(),
});
}
Err(error) => failure = Some(error.to_string()),
}
}
Err(failure.unwrap_or_else(|| {
"no storage layout is loaded, so state variables cannot be named; compile with `--storage-layout`".to_owned()
}))
}
fn resolve_target(&self, target: &BreakpointTarget) -> Result<BreakpointKind, String> {
match target {
BreakpointTarget::Pc(pc) => Ok(BreakpointKind::Pc(*pc)),
BreakpointTarget::SourceLine(target) => {
let map = self.source_map_for("source breakpoints")?;
map.resolve_line(target.file.as_deref(), target.line)
.map(BreakpointKind::Line)
}
BreakpointTarget::Function(name) => {
let map = self.source_map_for("function breakpoints")?;
match map.resolve_function(name) {
Ok(functions) => Ok(BreakpointKind::Function(functions)),
Err(error) => self.resolve_state_target(name).map_err(|state_error| {
if state_error.contains("no storage layout") {
error
} else {
format!("{error}; and {state_error}")
}
}),
}
}
BreakpointTarget::State(path) => self.resolve_state_target(path),
BreakpointTarget::Storage(slot) => normalize_slot(slot)
.map(BreakpointKind::Storage)
.ok_or_else(|| format!("invalid storage slot `{slot}`; expected decimal or hex")),
BreakpointTarget::Revert => Ok(BreakpointKind::Revert),
BreakpointTarget::Call(None) => Ok(BreakpointKind::Call(None)),
BreakpointTarget::Call(Some(address)) => {
let normalized = normalize_address(address);
let hex = &normalized[2..];
if hex.len() != 40 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(format!(
"invalid address `{address}`; expected 20 bytes of hex"
));
}
Ok(BreakpointKind::Call(Some(normalized)))
}
BreakpointTarget::Opcode(mnemonic) => {
Ok(BreakpointKind::Opcode(mnemonic.to_ascii_uppercase()))
}
}
}
fn source_map_for(&self, what: &str) -> Result<&StepMap, String> {
if self.trace.is_none() {
return Err("no trace is loaded".to_owned());
}
self.step_map
.as_ref()
.filter(|map| !map.contracts().is_empty())
.ok_or_else(|| {
format!("{what} require compiler debug metadata; start the session with `--ethdebug-dir <address>:<contract>:<dir>`")
})
}
#[must_use]
pub fn breakpoint_hit(&self, step: usize) -> Option<Breakpoint> {
let trace_step = self.trace.as_ref()?.steps.get(step)?;
self.breakpoints
.iter()
.filter(|breakpoint| self.kind_matches(&breakpoint.kind, step, trace_step))
.find(|breakpoint| match &breakpoint.condition {
Some(condition) => {
matches!(self.evaluate_condition(condition, step), Evaluation::True)
}
None => true,
})
.cloned()
}
fn kind_matches(&self, kind: &BreakpointKind, step: usize, trace_step: StepView<'_>) -> bool {
let map = self.step_map.as_ref();
match kind {
BreakpointKind::Pc(pc) => trace_step.pc() == *pc,
BreakpointKind::Line(lines) => map.is_some_and(|map| {
map.is_line_start(step)
&& map
.line_key(step)
.is_some_and(|key| lines.iter().any(|line| line.key == key))
}),
BreakpointKind::Function(functions) => map.is_some_and(|map| {
map.is_frame_entry(step)
&& map
.function_id(step)
.is_some_and(|id| functions.iter().any(|function| function.id == id))
}),
BreakpointKind::Storage(slot) | BreakpointKind::StateWrite { slot, .. } => {
trace_step.op() == opcode::SSTORE
&& trace_step
.stack()
.top()
.and_then(|word| normalize_slot(&word.to_string()))
.is_some_and(|written| written == *slot)
}
BreakpointKind::Revert => {
trace_step.op() == opcode::REVERT || trace_step.error().is_some()
}
BreakpointKind::Call(address) => {
let target = call_target(trace_step);
match address {
Some(address) => target.as_deref() == Some(address.as_str()),
None => target.is_some() || opcode::is_call(trace_step.op()),
}
}
BreakpointKind::Opcode(mnemonic) => {
trace_step.mnemonic().eq_ignore_ascii_case(mnemonic)
}
}
}
fn matches_any_step(&self, kind: &BreakpointKind) -> bool {
let Some(trace) = &self.trace else {
return true;
};
trace
.steps
.iter()
.enumerate()
.any(|(step, trace_step)| self.kind_matches(kind, step, trace_step))
}
#[must_use]
pub fn evaluate_condition(&self, condition: &Condition, step: usize) -> Evaluation {
let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
return Evaluation::Unavailable("no trace is loaded".to_owned());
};
let Some(trace_step) = trace.steps.get(step) else {
return Evaluation::Unavailable(format!("step {step} is outside the trace"));
};
let words = self
.storage_tape
.as_ref()
.map(|tape| tape.at_step(map, step));
let frames = self.frames_at(step);
let frame = frames.iter().find(|frame| !frame.arguments.is_empty());
let context = ConditionContext::new(map, step, trace_step, words)
.with_frame(frame)
.with_trace(trace);
let outcome = condition.evaluate(&context);
if let Evaluation::Unavailable(reason) = &outcome {
self.push_note(format!(
"`{}` could not be evaluated: {reason}",
condition.text()
));
}
outcome
}
fn push_note(&self, note: String) {
let mut slot = self.note.borrow_mut();
if slot.is_none() {
*slot = Some(note);
}
}
#[must_use]
pub fn take_note(&self) -> Option<String> {
self.note.borrow_mut().take()
}
#[must_use]
pub fn frames_at(&self, step: usize) -> Vec<Frame> {
let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
return Vec::new();
};
let mut frames = map.frames(step);
for frame in &mut frames {
let Some(entry) = trace.steps.get(frame.entry_step) else {
continue;
};
let stack = entry.stack().to_vec();
frame.arguments = map.frame_arguments(
frame,
FrameState {
stack: &stack,
memory: entry.memory(),
},
);
}
frames
}
#[must_use]
pub fn frames(&self) -> Vec<Frame> {
self.frames_at(self.current_step)
}
#[must_use]
pub fn source_listing(&self, radius: u64) -> Option<SourceListing> {
self.step_map
.as_ref()?
.source_listing(self.current_step, radius)
}
pub fn variables(&self) -> Result<StepVariables, String> {
let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
return Err("no trace is loaded".to_owned());
};
if self.current_step >= trace.steps.len() {
return Err(format!("step {} is outside the trace", self.current_step));
}
let Some(contract) = map.contract_at_step(self.current_step) else {
return Err("no sources matched the contract executing at this step".to_owned());
};
if contract.info.has_variable_locations() {
return Ok(StepVariables {
variables: variables_for_step(trace, &contract.info, self.current_step),
origin: VariablesOrigin::Ethdebug,
});
}
match map.locals_at(self.current_step) {
LocalsStatus::Inferred(_) => {
let words = self
.storage_tape
.as_ref()
.map(|tape| tape.at_step(map, self.current_step));
Ok(StepVariables {
variables: map.inferred_variables(trace, self.current_step, words.as_ref()),
origin: VariablesOrigin::Inferred,
})
}
LocalsStatus::Unavailable(reason) => Err(reason.to_owned()),
}
}
#[must_use]
pub fn local_path(&self, path: &str) -> Option<Result<DebugVariable, String>> {
let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
return None;
};
let words = self
.storage_tape
.as_ref()
.map(|tape| tape.at_step(map, self.current_step));
map.local_path(trace, self.current_step, words.as_ref(), path)
}
#[must_use]
pub fn calldata(&self) -> Option<String> {
let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
return None;
};
let step = trace.steps.get(self.current_step)?;
let calldata = map.calldata_at(trace, self.current_step);
let root_depth = trace.steps.first().map_or(0, |first| first.depth());
if step.depth() == root_depth || !calldata.is_empty() {
return Some(calldata.to_owned());
}
None
}
pub fn apply_command(&mut self, command: DebuggerCommand) -> Option<StepOutcome> {
match command {
DebuggerCommand::Next => Some(self.next_source()),
DebuggerCommand::NextInstruction => Some(self.next_instruction()),
DebuggerCommand::Step => Some(self.step_into()),
DebuggerCommand::Continue => Some(self.continue_execution()),
DebuggerCommand::Finish => Some(self.finish()),
DebuggerCommand::ReverseNext => Some(self.previous_source()),
DebuggerCommand::ReverseNextInstruction => Some(self.previous_instruction()),
DebuggerCommand::ReverseStep => Some(self.reverse_step_into()),
DebuggerCommand::ReverseContinue => Some(self.reverse_continue()),
DebuggerCommand::ReverseFinish => Some(self.reverse_finish()),
DebuggerCommand::Goto(step) => Some(self.goto_step(step)),
DebuggerCommand::Mode(Some(mode)) => Some(self.set_display_mode(mode)),
DebuggerCommand::Break(target, condition) => {
Some(self.set_conditional_breakpoint_target(&target, condition.as_deref()))
}
DebuggerCommand::Clear(target) => Some(self.clear_breakpoint_target(&target)),
DebuggerCommand::Delete(id) => Some(self.delete_breakpoint(id)),
DebuggerCommand::Empty
| DebuggerCommand::Help(_)
| DebuggerCommand::Info(_)
| DebuggerCommand::Backtrace
| DebuggerCommand::List
| DebuggerCommand::Memory { .. }
| DebuggerCommand::Calldata
| DebuggerCommand::ReturnData
| DebuggerCommand::Stack { .. }
| DebuggerCommand::Mode(None)
| DebuggerCommand::Print(_)
| DebuggerCommand::Tui
| DebuggerCommand::Quit
| DebuggerCommand::Unknown(_)
| DebuggerCommand::Vars => None,
}
}
fn pc_at(&self, step: usize) -> u64 {
self.trace
.as_ref()
.and_then(|trace| trace.steps.get(step))
.map_or(0, |step| step.pc())
}
fn outcome_for_current_step(&self) -> StepOutcome {
let Some(step) = self.current_step_data() else {
return StepOutcome::NoTrace;
};
if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
return StepOutcome::BreakpointHit {
step: self.current_step,
pc: step.pc(),
breakpoint,
};
}
StepOutcome::Moved {
step: self.current_step,
pc: step.pc(),
op: step.mnemonic().to_owned(),
}
}
}
fn normalize_slot(input: &str) -> Option<String> {
let input = input.trim();
let hex = if let Some(hex) = input
.strip_prefix("0x")
.or_else(|| input.strip_prefix("0X"))
{
if hex.is_empty() || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return None;
}
hex.to_ascii_lowercase()
} else {
format!("{:x}", input.parse::<u128>().ok()?)
};
let trimmed = hex.trim_start_matches('0');
Some(if trimmed.is_empty() {
"0".to_owned()
} else {
trimmed.to_owned()
})
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::json;
use soldb_core::{StepSnapshot, TraceStep, TransactionTrace};
use soldb_debugger::{
CodeGenerator, ContractDebugInfo, FunctionId, LineKey, ResolvedFunction, ResolvedLine,
};
use soldb_ethdebug::{source_range, EthdebugInfo, Instruction};
use super::{
Breakpoint, BreakpointKind, BreakpointTarget, DebuggerCommand, DebuggerInfoCommand,
DebuggerState, DisplayMode, SourceBreakpointTarget, StepOutcome, VariablesOrigin,
};
#[test]
fn parses_repl_commands_and_aliases() {
assert_eq!(DebuggerCommand::parse(""), DebuggerCommand::Empty);
assert_eq!(DebuggerCommand::parse("n"), DebuggerCommand::Next);
assert_eq!(
DebuggerCommand::parse("stepi"),
DebuggerCommand::NextInstruction
);
assert_eq!(DebuggerCommand::parse("s"), DebuggerCommand::Step);
assert_eq!(DebuggerCommand::parse("c"), DebuggerCommand::Continue);
assert_eq!(DebuggerCommand::parse("finish"), DebuggerCommand::Finish);
assert_eq!(DebuggerCommand::parse("fin"), DebuggerCommand::Finish);
assert_eq!(DebuggerCommand::parse("goto 2"), DebuggerCommand::Goto(2));
assert_eq!(DebuggerCommand::parse("vars"), DebuggerCommand::Vars);
assert_eq!(DebuggerCommand::parse("LOCALS"), DebuggerCommand::Vars);
assert_eq!(
DebuggerCommand::parse("print balance"),
DebuggerCommand::Print("balance".to_owned())
);
assert_eq!(
DebuggerCommand::parse("p balance "),
DebuggerCommand::Print("balance".to_owned())
);
assert_eq!(
DebuggerCommand::parse("print"),
DebuggerCommand::Print(String::new())
);
assert_eq!(
DebuggerCommand::parse("info resources"),
DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: false })
);
assert_eq!(
DebuggerCommand::parse("info resources --json"),
DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: true })
);
assert_eq!(
DebuggerCommand::parse("info breakpoints"),
DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints)
);
assert_eq!(
DebuggerCommand::parse("i b"),
DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints)
);
assert_eq!(
DebuggerCommand::parse("info storage"),
DebuggerCommand::Info(DebuggerInfoCommand::Storage)
);
assert_eq!(
DebuggerCommand::parse("storage"),
DebuggerCommand::Info(DebuggerInfoCommand::Storage)
);
assert_eq!(
DebuggerCommand::parse("mode assembly"),
DebuggerCommand::Mode(Some(DisplayMode::Assembly))
);
assert_eq!(
DebuggerCommand::parse("break 0x10"),
DebuggerCommand::Break(BreakpointTarget::Pc(16), None)
);
assert_eq!(
DebuggerCommand::parse("break Counter.sol:7"),
DebuggerCommand::Break(
BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: Some("Counter.sol".to_owned()),
line: 7
}),
None
)
);
assert_eq!(
DebuggerCommand::parse("break line 7"),
DebuggerCommand::Break(
BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: None,
line: 7
}),
None
)
);
assert_eq!(
DebuggerCommand::parse("break increment"),
DebuggerCommand::Break(BreakpointTarget::Function("increment".to_owned()), None)
);
assert_eq!(
DebuggerCommand::parse("b Counter.increment"),
DebuggerCommand::Break(
BreakpointTarget::Function("Counter.increment".to_owned()),
None
)
);
assert_eq!(
DebuggerCommand::parse("break storage 0x0"),
DebuggerCommand::Break(BreakpointTarget::Storage("0x0".to_owned()), None)
);
assert_eq!(
DebuggerCommand::parse("break revert"),
DebuggerCommand::Break(BreakpointTarget::Revert, None)
);
assert_eq!(
DebuggerCommand::parse("break call"),
DebuggerCommand::Break(BreakpointTarget::Call(None), None)
);
assert_eq!(
DebuggerCommand::parse("break call 0xabc"),
DebuggerCommand::Break(BreakpointTarget::Call(Some("0xabc".to_owned())), None)
);
assert_eq!(
DebuggerCommand::parse("break op sstore"),
DebuggerCommand::Break(BreakpointTarget::Opcode("sstore".to_owned()), None)
);
assert_eq!(
DebuggerCommand::parse("break"),
DebuggerCommand::Unknown("break".to_owned())
);
assert_eq!(
DebuggerCommand::parse("break 12abc"),
DebuggerCommand::Unknown("break 12abc".to_owned())
);
assert_eq!(
DebuggerCommand::parse("clear 8"),
DebuggerCommand::Clear(BreakpointTarget::Pc(8))
);
assert_eq!(
DebuggerCommand::parse("delete 3"),
DebuggerCommand::Delete(3)
);
assert_eq!(DebuggerCommand::parse("d #3"), DebuggerCommand::Delete(3));
assert_eq!(DebuggerCommand::parse("bt"), DebuggerCommand::Backtrace);
assert_eq!(DebuggerCommand::parse("where"), DebuggerCommand::Backtrace);
assert_eq!(DebuggerCommand::parse("l"), DebuggerCommand::List);
assert_eq!(
DebuggerCommand::parse("memory"),
DebuggerCommand::Memory {
offset: None,
length: None
}
);
assert_eq!(
DebuggerCommand::parse("mem 0x40 32"),
DebuggerCommand::Memory {
offset: Some(64),
length: Some(32)
}
);
assert_eq!(
DebuggerCommand::parse("mem 1 2 3"),
DebuggerCommand::Unknown("mem 1 2 3".to_owned())
);
assert_eq!(
DebuggerCommand::parse("calldata"),
DebuggerCommand::Calldata
);
assert_eq!(
DebuggerCommand::parse("stack"),
DebuggerCommand::Stack { limit: None }
);
assert_eq!(
DebuggerCommand::parse("help mode"),
DebuggerCommand::Help(Some("mode".to_owned()))
);
assert_eq!(DebuggerCommand::parse("q"), DebuggerCommand::Quit);
assert_eq!(
DebuggerCommand::parse("wat"),
DebuggerCommand::Unknown("wat".to_owned())
);
}
#[test]
fn loads_trace_and_steps_instruction_by_instruction() {
let mut state = DebuggerState::new();
assert_eq!(state.next_instruction(), StepOutcome::NoTrace);
assert_eq!(state.next_source(), StepOutcome::NoTrace);
assert_eq!(state.finish(), StepOutcome::NoTrace);
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert_eq!(state.step_count(), 4);
assert_eq!(state.current_step_data().expect("step").mnemonic(), "PUSH1");
assert!(!state.has_source());
assert_eq!(
state.next_instruction(),
StepOutcome::Moved {
step: 1,
pc: 2,
op: "MSTORE".into()
}
);
assert_eq!(state.current_step, 1);
assert!(matches!(
state.next_source(),
StepOutcome::Moved { step: 2, .. }
));
assert!(matches!(
state.step_into(),
StepOutcome::Moved { step: 3, .. }
));
assert!(matches!(
state.previous_source(),
StepOutcome::Moved { step: 2, .. }
));
assert!(matches!(
state.reverse_step_into(),
StepOutcome::Moved { step: 1, .. }
));
}
#[test]
fn continues_until_breakpoint_or_end() {
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
let set = state.set_breakpoint(3);
let StepOutcome::BreakpointSet(breakpoint) = &set else {
panic!("{set:?}");
};
assert_eq!(breakpoint.id, 1);
assert_eq!(breakpoint.kind, BreakpointKind::Pc(3));
assert_eq!(breakpoint.label(), "PC 3");
assert_eq!(state.set_breakpoint(3), set);
assert_eq!(state.breakpoints().len(), 1);
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 2, pc: 3, .. }
));
assert_eq!(state.current_step, 2);
assert!(matches!(
state.clear_breakpoint(3),
StepOutcome::BreakpointCleared(_)
));
assert_eq!(
state.clear_breakpoint(3),
StepOutcome::BreakpointMissing("PC 3".to_owned())
);
assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
}
#[test]
fn numbered_breakpoints_can_be_deleted() {
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
state.set_breakpoint(2);
state.set_breakpoint(3);
assert_eq!(
state
.breakpoints()
.iter()
.map(|breakpoint| breakpoint.id)
.collect::<Vec<_>>(),
vec![1, 2]
);
assert!(matches!(
state.delete_breakpoint(1),
StepOutcome::BreakpointCleared(breakpoint) if breakpoint.kind == BreakpointKind::Pc(2)
));
assert_eq!(
state.delete_breakpoint(1),
StepOutcome::BreakpointMissing("#1".to_owned())
);
assert!(matches!(
state.set_breakpoint(4),
StepOutcome::BreakpointSet(breakpoint) if breakpoint.id == 3
));
}
#[test]
fn goto_validates_trace_bounds() {
let mut state = DebuggerState::new();
assert_eq!(state.goto_step(1), StepOutcome::NoTrace);
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert_eq!(
state.goto_step(3),
StepOutcome::Moved {
step: 3,
pc: 4,
op: "CALL".into()
}
);
assert_eq!(
state.goto_step(99),
StepOutcome::InvalidStep {
requested: 99,
max_step: Some(3)
}
);
}
#[test]
fn parses_reverse_commands_and_aliases() {
for alias in ["reverse-next", "rnext", "rn"] {
assert_eq!(DebuggerCommand::parse(alias), DebuggerCommand::ReverseNext);
}
for alias in [
"reverse-nexti",
"rnexti",
"rni",
"reverse-stepi",
"rsi",
"back",
] {
assert_eq!(
DebuggerCommand::parse(alias),
DebuggerCommand::ReverseNextInstruction,
"{alias}"
);
}
for alias in ["reverse-step", "rstep", "rs"] {
assert_eq!(DebuggerCommand::parse(alias), DebuggerCommand::ReverseStep);
}
for alias in ["reverse-continue", "rcontinue", "rc", "RC"] {
assert_eq!(
DebuggerCommand::parse(alias),
DebuggerCommand::ReverseContinue
);
}
for alias in ["reverse-finish", "rfinish", "rfin"] {
assert_eq!(
DebuggerCommand::parse(alias),
DebuggerCommand::ReverseFinish
);
}
}
#[test]
fn steps_backward_over_the_recording_like_a_tape() {
let mut state = DebuggerState::new();
assert_eq!(state.previous_instruction(), StepOutcome::NoTrace);
assert_eq!(state.reverse_continue(), StepOutcome::NoTrace);
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert_eq!(
state.previous_instruction(),
StepOutcome::AtStart { step: 0 }
);
state.goto_step(2);
assert!(matches!(
state.previous_instruction(),
StepOutcome::Moved { step: 1, .. }
));
assert!(matches!(
state.previous_instruction(),
StepOutcome::Moved { step: 0, .. }
));
assert_eq!(
state.previous_instruction(),
StepOutcome::AtStart { step: 0 }
);
assert_eq!(state.current_step, 0);
assert!(matches!(
state.goto_step(2),
StepOutcome::Moved { step: 2, .. }
));
assert_eq!(
state.apply_command(DebuggerCommand::ReverseNextInstruction),
Some(StepOutcome::Moved {
step: 1,
pc: sample_trace().steps.step(1).pc(),
op: sample_trace().steps.step(1).mnemonic().to_owned(),
})
);
}
#[test]
fn reverse_continue_stops_at_earlier_breakpoints_then_the_start() {
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
let last = state.step_count() - 1;
state.goto_step(last);
let breakpoint_pc = sample_trace().steps.step(1).pc();
state.set_breakpoint(breakpoint_pc);
assert!(matches!(
state.reverse_continue(),
StepOutcome::BreakpointHit { step: 1, pc, .. } if pc == breakpoint_pc
));
assert_eq!(state.reverse_continue(), StepOutcome::AtStart { step: 0 });
assert_eq!(state.current_step, 0);
assert_eq!(
state.apply_command(DebuggerCommand::ReverseContinue),
Some(StepOutcome::AtStart { step: 0 })
);
state.goto_step(1);
assert_eq!(state.reverse_continue(), StepOutcome::AtStart { step: 0 });
}
#[test]
fn applies_state_changing_commands() {
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert_eq!(
state.apply_command(DebuggerCommand::Mode(Some(DisplayMode::Assembly))),
Some(StepOutcome::ModeChanged(DisplayMode::Assembly))
);
assert_eq!(state.display_mode.as_str(), "asm");
assert!(matches!(
state.apply_command(DebuggerCommand::Break(BreakpointTarget::Pc(2), None)),
Some(StepOutcome::BreakpointSet(_))
));
assert!(matches!(
state.apply_command(DebuggerCommand::NextInstruction),
Some(StepOutcome::BreakpointHit { step: 1, pc: 2, .. })
));
assert_eq!(state.apply_command(DebuggerCommand::Help(None)), None);
assert_eq!(state.apply_command(DebuggerCommand::Backtrace), None);
assert_eq!(state.apply_command(DebuggerCommand::Quit), None);
}
#[test]
fn a_state_variable_can_be_named_where_a_slot_would_go() {
assert_eq!(
DebuggerCommand::parse("break counter"),
DebuggerCommand::Break(BreakpointTarget::Function("counter".to_owned()), None)
);
assert_eq!(
DebuggerCommand::parse("break balances[0xabc]"),
DebuggerCommand::Break(BreakpointTarget::State("balances[0xabc]".to_owned()), None)
);
assert_eq!(
DebuggerCommand::parse("break config.limit"),
DebuggerCommand::Break(BreakpointTarget::Function("config.limit".to_owned()), None)
);
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::State("counter".to_owned())),
StepOutcome::BreakpointError(message) if message.contains("metadata")
));
}
#[test]
fn a_condition_gates_a_breakpoint_and_says_when_it_cannot_be_read() {
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert!(matches!(
state.set_conditional_breakpoint_target(
&BreakpointTarget::Opcode("sstore".to_owned()),
Some("pc == 3")
),
StepOutcome::BreakpointSet(breakpoint)
if breakpoint.label() == "opcode SSTORE if pc == 3"
));
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 2, .. }
));
state.goto_step(0);
state.delete_breakpoint(1);
assert!(matches!(
state.set_conditional_breakpoint_target(
&BreakpointTarget::Opcode("sstore".to_owned()),
Some("pc == 999")
),
StepOutcome::BreakpointSet(_)
));
assert!(matches!(
state.continue_execution(),
StepOutcome::AtEnd { .. }
));
assert_eq!(state.take_note(), None);
state.goto_step(0);
state.delete_breakpoint(2);
assert!(matches!(
state.set_conditional_breakpoint_target(
&BreakpointTarget::Opcode("sstore".to_owned()),
Some("pc == 999 || gas >= 94 && depth == 1")
),
StepOutcome::BreakpointSet(_)
));
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 2, .. }
));
state.goto_step(0);
state.delete_breakpoint(3);
assert!(matches!(
state.set_conditional_breakpoint_target(
&BreakpointTarget::Opcode("sstore".to_owned()),
Some("counter > 1")
),
StepOutcome::BreakpointSet(_)
));
assert!(matches!(
state.continue_execution(),
StepOutcome::AtEnd { .. }
));
let note = state.take_note().expect("a reason");
assert!(
note.contains("`counter > 1` could not be evaluated"),
"{note}"
);
assert_eq!(state.take_note(), None, "the note is taken once");
assert!(matches!(
state.set_conditional_breakpoint_target(
&BreakpointTarget::Opcode("sstore".to_owned()),
Some("pc ==")
),
StepOutcome::BreakpointError(_)
));
}
#[test]
fn tape_breakpoints_search_the_recording() {
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::Storage("0x00".to_owned())),
StepOutcome::BreakpointSet(breakpoint)
if breakpoint.kind == BreakpointKind::Storage("0".to_owned())
&& breakpoint.label() == "storage slot 0x0"
));
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 2, .. }
));
assert_eq!(
state.set_breakpoint_target(&BreakpointTarget::Storage("zz".to_owned())),
StepOutcome::BreakpointError(
"invalid storage slot `zz`; expected decimal or hex".to_owned()
)
);
assert!(matches!(
state.clear_breakpoint_target(&BreakpointTarget::Storage("0".to_owned())),
StepOutcome::BreakpointCleared(_)
));
state.goto_step(0);
let callee = "0x0000000000000000000000000000000000000002";
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::Call(Some(callee.to_owned()))),
StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == format!("call to {callee}")
));
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 3, .. }
));
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::Call(Some("0x12".to_owned()))),
StepOutcome::BreakpointError(message) if message.contains("20 bytes")
));
state.goto_step(0);
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::Opcode("mstore".to_owned())),
StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == "opcode MSTORE"
));
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 1, .. }
));
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::Revert),
StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == "revert"
));
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 3, .. }
));
assert_eq!(state.breakpoints().len(), 3);
assert_eq!(
state.set_breakpoint_target(&BreakpointTarget::Function("f".to_owned())),
StepOutcome::BreakpointError(
"function breakpoints require compiler debug metadata; start the session with `--ethdebug-dir <address>:<contract>:<dir>`"
.to_owned()
)
);
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: None,
line: 1
})),
StepOutcome::BreakpointError(message) if message.starts_with("source breakpoints require")
));
}
#[test]
fn calldata_and_frames_without_debug_info() {
let mut state = DebuggerState::new();
assert!(state.frames().is_empty());
assert_eq!(state.calldata(), None);
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
assert_eq!(state.calldata().as_deref(), Some("0xabcd"));
let mut empty = sample_trace();
empty.input_data = String::new();
let mut empty_state = DebuggerState::new();
empty_state
.load_trace(empty)
.expect("top-level calls must start at different steps");
assert_eq!(empty_state.calldata().as_deref(), Some(""));
let frames = state.frames();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].address.as_deref(), Some("0x2"));
assert!(state.location().is_none());
assert!(state.source_listing(2).is_none());
}
const SOURCE: &str = "\
contract C {
function outer(uint256 a) public {
uint256 b = a + 1;
inner(b);
b = 0;
}
function inner(uint256 x) internal {
x += 1;
}
}
";
fn source_state() -> DebuggerState {
let offset = |needle: &str| SOURCE.find(needle).expect(needle) as u64;
let whole = SOURCE.len() as u64;
let instruction = |pc: u64, offset: u64, length: u64| {
Instruction::new(
pc,
"JUMPDEST",
Some(json!({"code": source_range(0, offset, length)})),
)
};
let info = EthdebugInfo {
compilation: serde_json::Value::Null,
contract_name: "C".to_owned(),
environment: "runtime".to_owned(),
instructions: vec![
instruction(0, 0, whole),
instruction(1, 0, whole),
instruction(10, offset("function outer"), 90),
instruction(11, offset("uint256 b = a + 1;"), 18),
instruction(12, offset("inner(b);"), 9),
instruction(13, offset("inner(b);"), 9),
instruction(14, offset("b = 0;"), 6),
instruction(20, offset("function inner"), 50),
instruction(21, offset("x += 1;"), 7),
instruction(30, 0, whole),
],
sources: BTreeMap::from([(0, "C.sol".to_owned())]),
variable_locations: BTreeMap::new(),
};
let contract =
ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
let pcs = [0, 1, 10, 11, 12, 20, 21, 21, 13, 14, 30];
let mut trace = sample_trace();
let mut steps = pcs
.iter()
.map(|pc| step(*pc, "JUMPDEST", 0, &[]))
.collect::<Vec<_>>();
steps[6].op = "SSTORE".into();
steps[6].stack = vec!["0x2a".into(), "0x5".into()];
trace.steps = steps.into();
let mut state = DebuggerState::new();
state
.load_trace(trace)
.expect("top-level calls must start at different steps");
state.attach_debug_info(vec![contract]);
state
}
#[test]
fn loading_with_debug_info_matches_attaching_it_later() {
let attached = source_state();
let trace = attached.trace().expect("trace").clone();
let contracts = attached.step_map().expect("map").contracts().to_vec();
let mut loaded = DebuggerState::new();
loaded
.load_trace_with_debug_info(trace, contracts)
.expect("top-level calls must start at different steps");
assert_eq!(loaded.step_map, attached.step_map);
assert_eq!(loaded.storage_tape, attached.storage_tape);
assert_eq!(loaded.transient_tape, attached.transient_tape);
}
#[test]
fn transient_storage_shows_by_slot_and_by_name() {
use crate::response::{Output, StateInfo};
use crate::Session;
assert_eq!(
DebuggerCommand::parse("transient"),
DebuggerCommand::Info(DebuggerInfoCommand::TransientStorage)
);
assert_eq!(
DebuggerCommand::parse("info transient"),
DebuggerCommand::Info(DebuggerInfoCommand::TransientStorage)
);
let layout = soldb_ethdebug::StorageLayout::parse(&json!({
"storage": [
{"astId": 1, "contract": "C.sol:C", "label": "count", "offset": 0, "slot": "0", "type": "t_uint256"},
{"astId": 2, "contract": "C.sol:C", "label": "locked", "offset": 0, "slot": "1", "type": "t_bool"}
],
"types": {
"t_uint256": {"encoding": "inplace", "label": "uint256", "numberOfBytes": "32"},
"t_bool": {"encoding": "inplace", "label": "bool", "numberOfBytes": "1"}
}
}))
.expect("layout");
let mut state = source_state();
let mut trace = state.trace().expect("trace").clone();
trace.capabilities.transient_storage = true;
let mut steps = trace.steps.to_steps();
steps[3].op = "TSTORE".into();
steps[3].snapshot.transient_storage =
BTreeMap::from([("0x0".to_owned(), "0x7".to_owned())]);
trace.steps = steps.into();
let contracts = state.step_map().expect("map").contracts().to_vec();
state
.load_trace(trace)
.expect("top-level calls must start at different steps");
state.attach_debug_info(
contracts
.into_iter()
.map(|contract| contract.with_transient_storage_layout(Some(layout.clone())))
.collect(),
);
let mut session = Session::new(state);
session.execute(DebuggerCommand::Goto(3));
assert_eq!(
session.execute(DebuggerCommand::Info(DebuggerInfoCommand::TransientStorage)),
[Output::TransientStorage {
address: Some("0x2".to_owned()),
slots: Vec::new(),
}]
);
session.execute(DebuggerCommand::Goto(4));
let [Output::TransientStorage { slots, .. }] =
&session.execute(DebuggerCommand::Info(DebuggerInfoCommand::TransientStorage))[..]
else {
panic!("transient storage");
};
assert_eq!(
(slots[0].slot.as_str(), slots[0].value.as_str()),
("0x0", "0x7")
);
let [Output::Variable { variable, .. }] =
&session.execute(DebuggerCommand::Print("count".to_owned()))[..]
else {
panic!("count");
};
assert_eq!(variable.value, "7");
assert_eq!(variable.place.as_deref(), Some("transient slot 0x0"));
let [Output::Variables {
state, transient, ..
}] = &session.execute(DebuggerCommand::Vars)[..]
else {
panic!("vars");
};
assert_eq!(*state, StateInfo::NoLayout);
let shown = transient
.iter()
.map(|variable| (variable.name.as_str(), variable.value.as_str()))
.collect::<Vec<_>>();
assert_eq!(shown, [("count", "7"), ("locked", "false")]);
let mut state = DebuggerState::new();
state
.load_trace(sample_trace())
.expect("top-level calls must start at different steps");
let [Output::Message { text, .. }] = &Session::new(state)
.execute(DebuggerCommand::Info(DebuggerInfoCommand::TransientStorage))[..]
else {
panic!("message");
};
assert!(text.contains("not in this trace"), "{text}");
}
#[test]
fn return_data_shows_the_buffer_or_says_it_was_not_recorded() {
use crate::response::Output;
use crate::Session;
assert_eq!(
DebuggerCommand::parse("returndata"),
DebuggerCommand::ReturnData
);
let mut trace = sample_trace();
let mut steps = trace.steps.to_steps();
steps[2].snapshot = steps[2]
.normalized_snapshot()
.with_return_data(Some("2a".to_owned()));
trace.steps = steps.into();
let mut state = DebuggerState::new();
state
.load_trace(trace.clone())
.expect("top-level calls must start at different steps");
assert!(matches!(
&Session::new(state).execute(DebuggerCommand::ReturnData)[..],
[Output::Message { text, .. }] if text.contains("not in this trace")
));
trace.capabilities.return_data = true;
let mut state = DebuggerState::new();
state
.load_trace(trace)
.expect("top-level calls must start at different steps");
let mut session = Session::new(state);
assert_eq!(
session.execute(DebuggerCommand::ReturnData),
[Output::ReturnData {
bytes: 0,
data: "0x".to_owned()
}]
);
session.execute(DebuggerCommand::Goto(2));
assert_eq!(
session.execute(DebuggerCommand::ReturnData),
[Output::ReturnData {
bytes: 1,
data: "0x2a".to_owned()
}]
);
}
#[test]
fn memory_is_shown_in_words_of_hex() {
let mut trace = sample_trace();
let mut steps = trace.steps.to_steps();
let mut snapshot = steps[2].normalized_snapshot();
snapshot.memory = Some(soldb_core::Bytes::from(
[vec![0_u8; 31], vec![0x2a], vec![0xff; 8]].concat(),
));
steps[2].snapshot = snapshot;
trace.steps = steps.into();
trace.capabilities.memory = true;
let mut state = DebuggerState::new();
state
.load_trace(trace)
.expect("top-level calls must start at different steps");
let mut session = crate::Session::new(state);
session.execute(DebuggerCommand::Goto(2));
let whole = session.execute(DebuggerCommand::Memory {
offset: None,
length: None,
});
assert_eq!(
whole,
[crate::Output::Memory(crate::MemoryInfo {
total: 40,
start: 0,
end: 40,
words: vec![
crate::MemoryWord {
offset: 0,
hex: format!("{}2a", "00".repeat(31)),
},
crate::MemoryWord {
offset: 32,
hex: "ff".repeat(8),
},
],
})]
);
assert_eq!(
crate::Renderer::new(false).lines(&whole[0]),
[
"Memory: 40 bytes".to_owned(),
format!("0x0000: {}2a", "00".repeat(31)),
format!("0x0020: {}", "ff".repeat(8)),
]
);
assert_eq!(
session.execute(DebuggerCommand::Memory {
offset: Some(30),
length: Some(4),
}),
[crate::Output::Memory(crate::MemoryInfo {
total: 40,
start: 30,
end: 34,
words: vec![crate::MemoryWord {
offset: 30,
hex: "002affff".to_owned(),
}],
})]
);
}
#[test]
fn source_stepping_moves_by_line_and_frame() {
let mut state = source_state();
assert!(state.has_source());
assert!(matches!(
state.next_source(),
StepOutcome::Moved { step: 2, .. }
));
assert_eq!(state.location().expect("location").line, 2);
assert!(matches!(
state.next_source(),
StepOutcome::Moved { step: 3, .. }
));
assert!(matches!(
state.next_source(),
StepOutcome::Moved { step: 4, .. }
));
assert!(matches!(
state.next_source(),
StepOutcome::Moved { step: 9, .. }
));
state.goto_step(4);
assert!(matches!(
state.step_into(),
StepOutcome::Moved { step: 5, .. }
));
assert_eq!(
state.location().expect("location").function_name.as_deref(),
Some("inner")
);
assert!(matches!(
state.step_into(),
StepOutcome::Moved { step: 6, .. }
));
assert!(matches!(state.finish(), StepOutcome::Moved { step: 8, .. }));
state.goto_step(6);
assert!(matches!(
state.reverse_finish(),
StepOutcome::Moved { step: 4, .. }
));
state.goto_step(9);
assert!(matches!(
state.previous_source(),
StepOutcome::Moved { step: 4, .. }
));
assert!(matches!(
state.reverse_step_into(),
StepOutcome::Moved { step: 3, .. }
));
state.goto_step(9);
assert!(matches!(
state.reverse_step_into(),
StepOutcome::Moved { step: 8, .. }
));
state.goto_step(10);
assert_eq!(state.next_source(), StepOutcome::AtEnd { step: 10 });
assert_eq!(state.finish(), StepOutcome::AtEnd { step: 10 });
state.goto_step(9);
assert_eq!(state.finish(), StepOutcome::AtEnd { step: 10 });
state.goto_step(0);
assert_eq!(state.reverse_finish(), StepOutcome::AtStart { step: 0 });
state.goto_step(1);
assert_eq!(state.previous_source(), StepOutcome::AtStart { step: 0 });
let frames = state.frames();
assert_eq!(frames.len(), 1);
state.goto_step(6);
let frames = state.frames();
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
let listing = state.source_listing(1).expect("listing");
assert_eq!(listing.current_line, 8);
}
#[test]
fn a_breakpoint_names_each_target_once() {
let line = |contract: usize| ResolvedLine {
key: LineKey {
contract,
source_id: 0,
line: 4,
},
path: "C.sol".to_owned(),
requested_line: 4,
};
let lines = Breakpoint {
id: 1,
kind: BreakpointKind::Line(vec![line(0), line(1)]),
condition: None,
};
assert_eq!(lines.label(), "C.sol:4");
let function = |contract: usize| ResolvedFunction {
id: FunctionId {
contract,
function: 1,
},
name: "inner".to_owned(),
contract_name: "C".to_owned(),
path: "C.sol".to_owned(),
line: 7,
};
let functions = Breakpoint {
id: 2,
kind: BreakpointKind::Function(vec![function(0), function(1)]),
condition: None,
};
assert_eq!(functions.label(), "function C.inner at C.sol:7");
let mut other = line(1);
other.key.line = 1;
other.requested_line = 10;
let mixed = Breakpoint {
id: 3,
kind: BreakpointKind::Line(vec![line(0), other]),
condition: None,
};
assert_eq!(
mixed.label(),
"C.sol:4, C.sol:1 (the statement containing line 10)"
);
}
#[test]
fn variables_are_inferred_from_the_stack_when_the_artifact_has_no_locations() {
let source = "contract C {\n function f(uint256 a) public {\n uint256 b = a;\n b = 0;\n }\n}\n";
let offset = |needle: &str| source.find(needle).expect(needle) as u64;
let instruction = |pc: u64, needle: &str| {
let code = source_range(0, offset(needle), needle.len() as u64);
Instruction::new(pc, "JUMPDEST", Some(json!({"code": code})))
};
let contract = |code_generator: Option<CodeGenerator>| {
let info = EthdebugInfo {
compilation: serde_json::Value::Null,
contract_name: "C".to_owned(),
environment: "call".to_owned(),
instructions: vec![
instruction(0, "contract C"),
instruction(1, "function f"),
instruction(2, "uint256 b"),
instruction(3, "b = 0"),
],
sources: BTreeMap::from([(0, "C.sol".to_owned())]),
variable_locations: BTreeMap::new(),
};
ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, source.to_owned())]))
.with_code_generator(code_generator)
};
let mut trace = sample_trace();
trace.steps = vec![
step(0, "PUSH1", 0, &[]),
step(1, "JUMPDEST", 0, &["0x9", "0x5"]),
step(2, "PUSH0", 0, &["0x9", "0x5"]),
step(3, "POP", 0, &["0x9", "0x5", "0x5"]),
]
.into();
let mut state = DebuggerState::new();
state
.load_trace(trace.clone())
.expect("top-level calls must start at different steps");
state.attach_debug_info(vec![contract(Some(CodeGenerator::Legacy))]);
assert_eq!(
state.variables().unwrap_err(),
"no function is executing at this step"
);
state.goto_step(3);
let variables = state.variables().expect("inferred");
assert_eq!(variables.origin, VariablesOrigin::Inferred);
assert_eq!(
variables
.variables
.iter()
.map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
.collect::<Vec<_>>(),
[("a", "5"), ("b", "5")]
);
let mut state = DebuggerState::new();
state
.load_trace(trace)
.expect("top-level calls must start at different steps");
state.attach_debug_info(vec![contract(Some(CodeGenerator::ViaIr))]);
state.goto_step(3);
assert!(state.variables().unwrap_err().contains("via-IR pipeline"));
}
#[test]
fn line_and_function_breakpoints_stop_on_entry() {
let mut state = source_state();
let set =
state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: Some("C.sol".to_owned()),
line: 4,
}));
let StepOutcome::BreakpointSet(breakpoint) = &set else {
panic!("{set:?}");
};
assert_eq!(breakpoint.label(), "C.sol:4");
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 4, .. }
));
assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 10 });
state.goto_step(3);
assert!(matches!(
state.next_source(),
StepOutcome::BreakpointHit { step: 4, .. }
));
assert!(matches!(
state.clear_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: None,
line: 4,
})),
StepOutcome::BreakpointCleared(_)
));
assert_eq!(
state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: Some("Other.sol".to_owned()),
line: 4,
})),
StepOutcome::BreakpointError("source file not found: Other.sol".to_owned())
);
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
file: None,
line: 10,
})),
StepOutcome::BreakpointSet(breakpoint)
if breakpoint.label() == "C.sol:1 (the statement containing line 10)"
));
state.goto_step(0);
let set = state.set_breakpoint_target(&BreakpointTarget::Function("inner".to_owned()));
let StepOutcome::BreakpointSet(breakpoint) = &set else {
panic!("{set:?}");
};
assert_eq!(breakpoint.label(), "function C.inner at C.sol:7");
assert!(matches!(
state.continue_execution(),
StepOutcome::BreakpointHit { step: 5, .. }
));
state.goto_step(10);
assert!(matches!(
state.reverse_continue(),
StepOutcome::BreakpointHit { step: 5, .. }
));
assert!(matches!(
state.set_breakpoint_target(&BreakpointTarget::Function("nothing".to_owned())),
StepOutcome::BreakpointError(message) if message.contains("no function named")
));
assert!(matches!(
state.clear_breakpoint_target(&BreakpointTarget::Function("C.inner".to_owned())),
StepOutcome::BreakpointCleared(_)
));
assert!(matches!(
state.clear_breakpoint_target(&BreakpointTarget::Function("C.inner".to_owned())),
StepOutcome::BreakpointMissing(label) if label == "function C.inner at C.sol:7"
));
}
#[test]
fn a_breakpoint_condition_reads_an_inferred_local() {
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";
let offset = |needle: &str| source.find(needle).expect(needle) as u64;
let instruction = |pc: u64, needle: &str| {
let code = source_range(0, offset(needle), needle.len() as u64);
Instruction::new(pc, "JUMPDEST", Some(json!({"code": code})))
};
let info = EthdebugInfo {
compilation: serde_json::Value::Null,
contract_name: "C".to_owned(),
environment: "call".to_owned(),
instructions: vec![
instruction(0, "contract C"),
instruction(1, "function f"),
instruction(2, "uint256 b"),
instruction(3, "b = 0"),
],
sources: BTreeMap::from([(0, "C.sol".to_owned())]),
variable_locations: BTreeMap::new(),
};
let contract =
ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, source.to_owned())]))
.with_code_generator(Some(CodeGenerator::Legacy));
let mut trace = sample_trace();
trace.steps = vec![
step(0, "PUSH1", 0, &[]),
step(1, "JUMPDEST", 0, &["0x9", "0x5", "0x1"]),
step(2, "PUSH0", 0, &["0x9", "0x5", "0x1"]),
step(3, "POP", 0, &["0x9", "0x5", "0x1", "0x5"]),
]
.into();
let mut state = DebuggerState::new();
state
.load_trace(trace)
.expect("top-level calls must start at different steps");
state.attach_debug_info(vec![contract]);
let hit = |condition: &str| -> (Option<usize>, Option<String>) {
let mut state = state.clone();
let DebuggerCommand::Break(target, condition) =
DebuggerCommand::parse(&format!("break C.sol:5 if {condition}"))
else {
panic!("break command");
};
state.set_conditional_breakpoint_target(&target, condition.as_deref());
let step = match state.continue_execution() {
StepOutcome::BreakpointHit { .. } => Some(state.current_step),
_ => None,
};
(step, state.take_note())
};
assert_eq!(hit("b == 5"), (Some(3), None));
assert_eq!(hit("b == a"), (Some(3), None));
assert_eq!(hit("m == Mode.On"), (Some(3), None));
assert_eq!(hit("m == Mode.Off").0, None);
assert_eq!(hit("b > 5").0, None);
let (step, note) = hit("c == 5");
assert_eq!(step, None);
let note = note.expect("note");
assert!(note.contains("`c == 5` could not be evaluated"), "{note}");
assert!(
note.contains("`c` is not a step value, a local, or an argument here"),
"{note}"
);
}
fn sample_trace() -> TransactionTrace {
TransactionTrace {
tx_hash: Some("0xabc".to_owned()),
from_addr: "0x1".to_owned(),
to_addr: Some("0x2".to_owned()),
value: "0x0".to_owned(),
input_data: "0xabcd".to_owned(),
gas_used: 21_000,
output: "0x".to_owned(),
success: true,
error: None,
debug_trace_available: true,
contract_address: None,
backend: Some("debug-rpc".to_owned()),
capabilities: Default::default(),
artifacts: Default::default(),
steps: vec![
step(0, "PUSH1", 100, &[]),
step(2, "MSTORE", 97, &[]),
step(3, "SSTORE", 94, &["0x2a", "0x0"]),
{
let mut last = step(
4,
"CALL",
92,
&[
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0000000000000000000000000000000000000000000000000000000000000002",
"0x5208",
],
);
last.error = Some("execution reverted".to_owned());
last
},
]
.into(),
}
}
fn step(pc: u64, op: &str, gas: u64, stack: &[&str]) -> TraceStep {
TraceStep {
pc,
op: op.into(),
gas,
gas_cost: 1,
depth: 1,
stack: stack
.iter()
.map(|word| soldb_core::Word::from(*word))
.collect(),
memory: None,
storage: Some(BTreeMap::new()),
error: None,
snapshot: StepSnapshot::default(),
}
}
}