1use alloy_primitives::{Address, B256, Bytes, hex, keccak256, map::HashSet};
8use eyre::{OptionExt, Result};
9use foundry_common::{
10 ContractsByArtifact, Shell,
11 fs::{self, write_json_file},
12};
13use foundry_compilers::{
14 artifacts::sourcemap::{Jump, SourceElement, SourceMap},
15 multi::MultiCompilerLanguage,
16};
17use foundry_evm_core::{Breakpoints, ic::PcIcMap};
18use foundry_evm_traces::{
19 CallTrace, CallTraceArena, CallTraceNode, CallTraceStep, TraceMemberOrder,
20 debug::{
21 ArtifactData, ContractSources, DebugRecording, RecordedFrame, SourceData, StepConverter,
22 quantity,
23 },
24};
25use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
26use serde_json::{Map, Value, json};
27use soldb_core::{
28 ContractCreation, EvmWord, ExecutionCall, StepRecord, StepTable, TraceArtifacts,
29 TraceCapabilities, TransactionTrace,
30};
31use soldb_debugger::{CodeGenerator, ContractDebugInfo, SourceCache, StorageLayout};
32use soldb_ethdebug::{EthdebugInfo, Instruction, SourceLocation, SourceMapContext, SourceMapJump};
33use soldb_repl::{BreakpointTarget, DebuggerCommand, DebuggerState, Output, Renderer, Session};
34use soldb_tui::Exit;
35use std::{
36 borrow::Cow,
37 collections::{BTreeMap, BTreeSet, HashMap},
38 io::{self, IsTerminal, Write},
39 ops::Range,
40 path::{Component, Path, PathBuf},
41 sync::Arc,
42};
43
44type BreakpointFrames = Vec<(Range<usize>, u64)>;
46
47type BreakpointSteps = BTreeMap<(Address, usize), BreakpointFrames>;
49
50#[derive(Debug)]
52pub(crate) struct SoldbDebugger {
53 trace: TransactionTrace,
54 contracts: Vec<ContractDebugInfo>,
55 breakpoints: Vec<(u64, BreakpointFrames)>,
57}
58
59impl SoldbDebugger {
60 pub(crate) fn new(
65 mut arenas: Vec<CallTraceArena>,
66 recordings: Vec<DebugRecording>,
67 contract_names: &[Vec<Option<String>>],
68 sources: &ContractSources,
69 known_contracts: &ContractsByArtifact,
70 breakpoints: &Breakpoints,
71 ) -> Result<Self> {
72 let mut source_cache = SourceCache::default();
73 let contracts = executed_programs(&arenas, contract_names, sources, known_contracts)
74 .iter()
75 .filter_map(|program| contract_debug_info(program, &mut source_cache))
76 .collect();
77 let (trace, breakpoints) = transaction_trace(&mut arenas, recordings, breakpoints)?;
78 let breakpoints = breakpoints
79 .into_iter()
80 .filter(|(_, frames)| !frames.is_empty())
81 .map(|((_, pc), frames)| (pc as u64, frames))
82 .collect();
83 Ok(Self { trace, contracts, breakpoints })
84 }
85
86 pub(crate) fn try_run(self) -> Result<()> {
91 let mut state = DebuggerState::new();
92 state.load_trace_with_debug_info(self.trace, self.contracts)?;
93 for (pc, frames) in self.breakpoints {
97 let condition = frames
98 .iter()
99 .map(|(steps, depth)| {
100 format!("step >= {} && step < {} && depth == {depth}", steps.start, steps.end)
101 })
102 .collect::<Vec<_>>()
103 .join(" || ");
104 state.set_conditional_breakpoint_target(&BreakpointTarget::Pc(pc), Some(&condition));
105 }
106 let mut session = Session::new(state);
107 let renderer = Renderer::new(Shell::get().out_supports_color());
108 let terminal = io::stdin().is_terminal() && io::stdout().is_terminal();
109
110 emit(&renderer, vec![session.loaded()])?;
111 emit(&renderer, session.initial_stop())?;
112 if terminal {
113 if open_tui(&mut session)? == Flow::Quit {
114 return Ok(());
115 }
116 sh_eprintln!("Type `help` for the command list, `tui` for the full-screen view.")?;
117 }
118
119 let mut line = String::new();
120 loop {
121 if terminal {
122 sh_eprint!("soldb> ")?;
123 io::stderr().flush()?;
124 }
125 line.clear();
126 if io::stdin().read_line(&mut line)? == 0 {
127 break;
128 }
129 match emit(&renderer, session.execute(DebuggerCommand::parse(&line)))? {
130 Flow::Continue => {}
131 Flow::Tui if open_tui(&mut session)? == Flow::Continue => {}
132 Flow::Tui | Flow::Quit => break,
133 }
134 }
135 Ok(())
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141enum Flow {
142 Continue,
143 Tui,
144 Quit,
145}
146
147fn emit(renderer: &Renderer, outputs: Vec<Output>) -> Result<Flow> {
149 let mut flow = Flow::Continue;
150 for output in outputs {
151 match output {
152 Output::Tui => flow = Flow::Tui,
153 Output::Quit => flow = Flow::Quit,
154 _ => {}
155 }
156 sh_print!("{}", renderer.render(&output))?;
157 }
158 Ok(flow)
159}
160
161fn open_tui(session: &mut Session) -> Result<Flow> {
163 match soldb_tui::run(session) {
164 Ok(Exit::Repl) => Ok(Flow::Continue),
165 Ok(Exit::Quit) => Ok(Flow::Quit),
166 Err(error) if error.kind() == io::ErrorKind::Unsupported => {
167 sh_warn!("cannot open the full-screen view: {error}")?;
168 Ok(Flow::Continue)
169 }
170 Err(error) => Err(error.into()),
171 }
172}
173
174fn transaction_trace(
181 arenas: &mut [CallTraceArena],
182 recordings: Vec<DebugRecording>,
183 breakpoints: &Breakpoints,
184) -> Result<(TransactionTrace, BreakpointSteps)> {
185 let nodes = arenas.iter().flat_map(|arena| arena.nodes());
186 let (steps, frames) =
187 nodes.fold((0, 0), |(steps, frames), node| (steps + node.trace.steps.len(), frames + 1));
188 let mut builder = TraceBuilder {
189 calls: Vec::with_capacity(frames),
190 creations: Vec::with_capacity(frames),
191 breakpoints: breakpoints.values().map(|location| (*location, Vec::new())).collect(),
192 ..Default::default()
193 };
194 if recordings.is_empty() {
195 builder.steps = StepTable::with_capacity(steps);
196 for arena in &mut *arenas {
197 builder.visit(arena.nodes_mut(), 0, None);
198 }
199 } else {
200 eyre::ensure!(recordings.len() == arenas.len(), "a debug trace has no recorded steps");
201 let offsets;
202 (builder.steps, offsets) = recorded_steps(&recordings)?;
203 for ((arena, recording), offset) in arenas.iter_mut().zip(&recordings).zip(offsets) {
204 let nodes = arena.nodes_mut();
205 eyre::ensure!(
206 recording.frames.len() == nodes.len()
207 && recording
208 .frames
209 .iter()
210 .zip(&*nodes)
211 .all(|(frame, node)| frame.depth == node.trace.depth),
212 "the recorded steps do not match the call trace"
213 );
214 builder.visit_recorded(nodes, 0, None, &recording.frames, offset);
215 }
216 }
217 let TraceBuilder { steps, calls, creations, breakpoints, .. } = builder;
218
219 let root = &arenas.last().ok_or_eyre("debug arena is empty")?.nodes()[0].trace;
222 let create = root.kind.is_any_create();
223 let trace = TransactionTrace {
224 tx_hash: None,
225 from_addr: hex::encode_prefixed(root.caller),
226 to_addr: (!create).then(|| hex::encode_prefixed(root.address)),
227 value: quantity(root.value),
228 input_data: hex::encode_prefixed(&root.data),
229 gas_used: arenas
230 .iter()
231 .fold(0u64, |gas, arena| gas.saturating_add(arena.nodes()[0].trace.gas_used)),
232 output: hex::encode_prefixed(&root.output),
233 success: arenas.iter().all(|arena| arena.nodes()[0].trace.success),
234 error: arenas.iter().find_map(|arena| status_error(arena.nodes()[0].trace.status)),
235 debug_trace_available: true,
236 contract_address: create.then(|| hex::encode_prefixed(root.address)),
237 backend: Some("foundry".to_string()),
238 capabilities: TraceCapabilities {
239 opcode_steps: true,
240 stack: true,
241 memory: true,
242 storage: true,
243 storage_diff: true,
244 call_trace: true,
245 contract_creation: true,
246 transient_storage: true,
247 return_data: true,
248 ..Default::default()
249 },
250 artifacts: TraceArtifacts { calls, creations, ..Default::default() },
251 steps,
252 };
253 Ok((trace, breakpoints))
254}
255
256#[derive(Default)]
258struct TraceBuilder {
259 steps: StepTable,
260 calls: Vec<ExecutionCall>,
261 creations: Vec<ContractCreation>,
262 converter: StepConverter,
264 breakpoints: BreakpointSteps,
266}
267
268impl TraceBuilder {
269 fn visit(&mut self, nodes: &mut [CallTraceNode], idx: usize, parent: Option<usize>) {
271 let node = &mut nodes[idx];
272 let ordering = std::mem::take(&mut node.ordering);
273 let children = std::mem::take(&mut node.children);
274 let mut steps = std::mem::take(&mut node.trace.steps);
275 let runs_steps = !steps.is_empty();
276 let trace = &node.trace;
277 let address = trace.address;
278 let depth = trace.depth as u64 + 1;
279 let entry_step = self.steps.len();
280 let (creation, call) = self.open_frame(trace, parent, entry_step);
281
282 let mut next_step = 0;
284 let mut hits = Vec::new();
285 for order in ordering {
286 match order {
287 TraceMemberOrder::Step(_) => {
288 if next_step < steps.len() {
289 let (done, rest) = steps.split_at_mut(next_step + 1);
290 let step = &mut done[next_step];
291 if self.breakpoints.contains_key(&(address, step.pc))
292 && !hits.contains(&step.pc)
293 {
294 hits.push(step.pc);
295 }
296 self.push_step(step, rest.first(), depth);
297 step.stack = None;
299 step.memory = None;
300 next_step += 1;
301 }
302 }
303 TraceMemberOrder::Call(child) => {
304 self.visit(nodes, children[child], call.or(parent));
305 }
306 TraceMemberOrder::Log(_) => {}
307 }
308 }
309
310 self.close_frame(creation, call, self.steps.len(), runs_steps);
311 self.record_hits(address, hits, entry_step..self.steps.len(), depth);
312 }
313
314 fn visit_recorded(
317 &mut self,
318 nodes: &mut [CallTraceNode],
319 idx: usize,
320 parent: Option<usize>,
321 frames: &[RecordedFrame],
322 offset: usize,
323 ) {
324 let node = &mut nodes[idx];
325 let ordering = std::mem::take(&mut node.ordering);
326 let children = std::mem::take(&mut node.children);
327 let address = node.trace.address;
328 let frame = frames[idx];
329 let (creation, call) = self.open_frame(&node.trace, parent, offset + frame.entry_step);
330
331 let mut next_step = offset + frame.entry_step;
333 let mut runs_steps = false;
334 let mut hits = Vec::new();
335 for order in ordering {
336 if let TraceMemberOrder::Call(child) = order {
337 let child = children[child];
338 let own = next_step..offset + frames[child].entry_step;
339 runs_steps |= !own.is_empty();
340 self.find_hits(address, own, &mut hits);
341 self.visit_recorded(nodes, child, call.or(parent), frames, offset);
342 next_step = offset + frames[child].exit_step;
343 }
344 }
345 let own = next_step..offset + frame.exit_step;
346 runs_steps |= !own.is_empty();
347 self.find_hits(address, own, &mut hits);
348
349 let steps = offset + frame.entry_step..offset + frame.exit_step;
350 self.close_frame(creation, call, steps.end, runs_steps);
351 self.record_hits(address, hits, steps, frame.depth as u64 + 1);
352 }
353
354 fn find_hits(&self, address: Address, steps: Range<usize>, hits: &mut Vec<usize>) {
357 if self.breakpoints.range((address, 0)..=(address, usize::MAX)).next().is_none() {
358 return;
359 }
360 for index in steps {
361 let pc = self.steps.step(index).pc() as usize;
362 if self.breakpoints.contains_key(&(address, pc)) && !hits.contains(&pc) {
363 hits.push(pc);
364 }
365 }
366 }
367
368 fn record_hits(&mut self, address: Address, hits: Vec<usize>, steps: Range<usize>, depth: u64) {
371 for pc in hits {
372 if let Some(frames) = self.breakpoints.get_mut(&(address, pc)) {
373 frames.push((steps.clone(), depth));
374 }
375 }
376 }
377
378 fn open_frame(
381 &mut self,
382 trace: &CallTrace,
383 parent: Option<usize>,
384 entry_step: usize,
385 ) -> (Option<usize>, Option<usize>) {
386 let depth = trace.depth as u64 + 1;
387 let entry_step = Some(entry_step);
388 let error = status_error(trace.status);
389 let code_hash = trace.bytecode.as_ref().map(|code| keccak256(code).to_string());
392 let creation = trace.kind.is_any_create().then_some(self.creations.len());
393 let mut call = None;
394 if creation.is_some() {
395 self.creations.push(ContractCreation {
396 id: self.creations.len(),
397 parent_id: parent,
398 depth,
399 entry_step,
400 exit_step: None,
401 create_type: trace.kind.to_string(),
402 caller: hex::encode_prefixed(trace.caller),
403 address: Some(hex::encode_prefixed(trace.address)),
404 value: quantity(trace.value),
405 init_code: hex::encode_prefixed(&trace.data),
406 gas_limit: trace.gas_limit,
407 gas_used: Some(trace.gas_used),
408 output: Some(hex::encode_prefixed(&trace.output)),
409 success: Some(trace.success),
410 error,
411 code_hash,
412 });
413 } else {
414 let to = if trace.kind.is_delegate() { trace.caller } else { trace.address };
416 call = Some(self.calls.len());
417 self.calls.push(ExecutionCall {
418 id: self.calls.len(),
419 parent_id: parent,
420 depth,
421 entry_step,
422 exit_step: None,
423 call_type: trace.kind.to_string(),
424 from: hex::encode_prefixed(trace.caller),
425 to: hex::encode_prefixed(to),
426 bytecode_address: hex::encode_prefixed(trace.address),
427 value: quantity(trace.value),
428 input: hex::encode_prefixed(&trace.data),
429 gas_limit: trace.gas_limit,
430 gas_used: Some(trace.gas_used),
431 output: Some(hex::encode_prefixed(&trace.output)),
432 success: Some(trace.success),
433 error,
434 code_hash,
435 });
436 }
437 (creation, call)
438 }
439
440 fn close_frame(
448 &mut self,
449 creation: Option<usize>,
450 call: Option<usize>,
451 exit_step: usize,
452 runs_steps: bool,
453 ) {
454 let (entry, exit) = if let Some(id) = creation {
455 let creation = &mut self.creations[id];
456 (&mut creation.entry_step, &mut creation.exit_step)
457 } else if let Some(id) = call {
458 let call = &mut self.calls[id];
459 (&mut call.entry_step, &mut call.exit_step)
460 } else {
461 return;
462 };
463 if !runs_steps && entry.is_some_and(|entry| entry < exit_step) {
464 *entry = None;
465 }
466 *exit = Some(exit_step);
467 }
468
469 fn push_step(&mut self, step: &CallTraceStep, next: Option<&CallTraceStep>, depth: u64) {
471 let stack = step.stack.as_deref().unwrap_or_default();
472 let memory = step.memory.as_ref().map(|memory| memory.as_bytes().0.clone());
475 let loaded = next.and_then(|next| next.stack.as_deref()?.last().copied());
478 let had_value = step
479 .storage_change
480 .as_ref()
481 .filter(|_| step.op == OpCode::SSTORE)
482 .and_then(|change| change.had_value);
483 let storage = self.converter.storage(step.op.get(), stack, loaded, had_value);
484 let return_data = self.converter.return_data(&step.returndata);
486 self.steps.append(
487 StepRecord {
488 pc: step.pc as u64,
489 op: step.op.get(),
490 gas: step.gas_remaining,
491 gas_cost: step.gas_cost,
492 depth,
493 error: status_error(step.status),
494 memory,
495 storage: Some(storage.storage),
496 storage_diff: storage.storage_diff,
497 transient_storage: storage.transient_storage,
498 return_data: Some(return_data),
499 },
500 stack.iter().map(|word| EvmWord(word.to_be_bytes())),
501 );
502 }
503}
504
505fn recorded_steps(recordings: &[DebugRecording]) -> Result<(StepTable, Vec<usize>)> {
511 let first = &recordings[0].sink;
512 let mut offsets = Vec::with_capacity(recordings.len());
513 let mut next = 0;
514 for recording in recordings {
515 if !recording.sink.same(first)
516 || (!recording.steps.is_empty() && recording.steps.start != next)
517 {
518 offsets.clear();
519 break;
520 }
521 offsets.push(next);
522 next = recording.steps.end.max(next);
523 }
524 let mut shared = first.lock();
525 let len = shared.as_ref().ok_or_eyre("the recorded steps are in use")?.len();
526 if offsets.len() == recordings.len() && next == len {
527 return Ok((shared.take().unwrap_or_default(), offsets));
528 }
529 drop(shared);
530
531 offsets.clear();
532 let mut table = StepTable::with_capacity(recordings.iter().map(|r| r.steps.len()).sum());
533 for recording in recordings {
534 offsets.push(table.len());
535 let source = recording.sink.lock();
536 let source = source.as_ref().ok_or_eyre("the recorded steps are in use")?;
537 for index in recording.steps.clone() {
538 let step = source.step(index);
539 let stack = step.stack();
540 table.append(
541 StepRecord {
542 pc: step.pc(),
543 op: step.op(),
544 gas: step.gas(),
545 gas_cost: step.gas_cost(),
546 depth: step.depth(),
547 error: step.error().map(str::to_owned),
548 memory: step.memory(),
549 storage: Some(Arc::new(step.storage().clone())),
550 storage_diff: Some(step.storage_diff().clone()).filter(|diff| !diff.is_empty()),
551 transient_storage: Some(step.transient_storage().clone())
552 .filter(|slots| !slots.is_empty()),
553 return_data: Some(Arc::from(step.return_data().unwrap_or_default())),
554 },
555 (0..stack.len()).filter_map(|position| stack.get(position)),
556 );
557 }
558 }
559 Ok((table, offsets))
560}
561
562struct ExecutedProgram<'a> {
564 address: Address,
565 name: &'a str,
566 code: &'a Bytes,
567 code_hash: B256,
569 deployed_hash: B256,
572 init: bool,
574 artifact: &'a ArtifactData,
575 solc: bool,
577 source_map: &'a SourceMap,
578 pc_ic_map: Option<&'a PcIcMap>,
579 build_sources: &'a HashMap<u32, Arc<SourceData>>,
581 storage_layout: Option<Value>,
583}
584
585fn executed_programs<'a>(
587 arenas: &'a [CallTraceArena],
588 contract_names: &'a [Vec<Option<String>>],
589 sources: &'a ContractSources,
590 known_contracts: &ContractsByArtifact,
591) -> Vec<ExecutedProgram<'a>> {
592 let mut seen = HashSet::<(Address, bool, B256)>::default();
593 let mut programs = Vec::new();
594 for (node, name) in
595 arenas.iter().zip(contract_names).flat_map(|(arena, names)| arena.nodes().iter().zip(names))
596 {
597 let trace = &node.trace;
598 let init = trace.kind.is_any_create();
599 if let Some(name) = name
600 && let Some(code) = &trace.bytecode
601 && let code_hash = keccak256(code)
602 && seen.insert((trace.address, init, code_hash))
603 && let Some(sources_for_name) = sources.get_sources(name)
604 && let Some((artifact, source)) = sources_for_name.into_iter().find(|(artifact, _)| {
605 if init {
606 artifact.source_map.is_some()
607 } else {
608 artifact.source_map_runtime.is_some()
609 }
610 })
611 && let Some(build_sources) = sources.sources_by_id.get(&artifact.build_id)
612 {
613 let (source_map, pc_ic_map) = if init {
614 (artifact.source_map.as_ref(), artifact.pc_ic_map.as_ref())
615 } else {
616 (artifact.source_map_runtime.as_ref(), artifact.pc_ic_map_runtime.as_ref())
617 };
618 let Some(source_map) = source_map else { continue };
619 let storage_layout = known_contracts
620 .find_by_name_or_identifier(name)
621 .ok()
622 .flatten()
623 .and_then(|(_, contract)| contract.storage_layout.as_deref())
624 .and_then(|layout| serde_json::to_value(layout).ok());
625 programs.push(ExecutedProgram {
626 address: trace.address,
627 name,
628 code,
629 code_hash,
630 deployed_hash: if init { keccak256(&trace.output) } else { code_hash },
631 init,
632 artifact,
633 solc: matches!(source.language, MultiCompilerLanguage::Solc(_)),
634 source_map,
635 pc_ic_map,
636 build_sources,
637 storage_layout,
638 });
639 }
640 }
641 programs
642}
643
644fn contract_debug_info(
646 program: &ExecutedProgram<'_>,
647 source_cache: &mut SourceCache,
648) -> Option<ContractDebugInfo> {
649 let mut instructions = Vec::new();
650 let mut used_sources = BTreeSet::new();
651 let mut pc = 0;
652 while let Some(&byte) = program.code.get(pc) {
653 let op = OpCode::new(byte);
654 let index = if program.solc {
655 program.pc_ic_map.and_then(|map| map.get(pc as u32))
656 } else {
657 Some(pc as u32)
658 };
659 if let Some(element) = index.and_then(|index| program.source_map.get(index as usize)) {
660 let mnemonic = op.map_or_else(
661 || Cow::Owned(format!("UNKNOWN(0x{byte:02x})")),
662 |op| Cow::Borrowed(op.as_str()),
663 );
664 instructions.push(Instruction::from_source_map(
665 pc as u64,
666 mnemonic,
667 instruction_context(element, program.build_sources, &mut used_sources),
668 ));
669 }
670 pc += 1 + op.map_or(0, |op| op.info().immediate_size() as usize);
671 }
672 if instructions.is_empty() {
673 return None;
674 }
675
676 let used_sources = used_sources
677 .into_iter()
678 .filter_map(|id| program.build_sources.get(&id).map(|source| (u64::from(id), source)))
679 .collect::<Vec<_>>();
680 let info = EthdebugInfo {
681 compilation: Value::Null,
682 contract_name: program.name.to_string(),
683 environment: if program.init { "create" } else { "call" }.to_string(),
684 instructions,
685 sources: used_sources
686 .iter()
687 .map(|(id, source)| (*id, source.path.to_string_lossy().into_owned()))
688 .collect(),
689 variable_locations: BTreeMap::new(),
690 };
691 let source_contents =
692 used_sources.iter().map(|(id, source)| (*id, source.source.to_string())).collect();
693 let code_generator = program
695 .artifact
696 .via_ir
697 .filter(|_| program.solc)
698 .map(|via_ir| if via_ir { CodeGenerator::ViaIr } else { CodeGenerator::Legacy });
699 let storage_layout =
700 program.storage_layout.as_ref().and_then(|layout| StorageLayout::parse(layout).ok());
701 Some(
702 ContractDebugInfo::with_source_cache(
703 Some(&hex::encode_prefixed(program.address)),
704 program.name,
705 info,
706 source_contents,
707 source_cache,
708 )
709 .with_code_generator(code_generator)
710 .with_code_hash(Some(&program.code_hash.to_string()))
711 .with_storage_layout(storage_layout),
712 )
713}
714
715pub(crate) fn write_bundle(
722 dir: &Path,
723 mut arenas: Vec<CallTraceArena>,
724 recordings: Vec<DebugRecording>,
725 contract_names: &[Vec<Option<String>>],
726 sources: &ContractSources,
727 known_contracts: &ContractsByArtifact,
728) -> Result<()> {
729 if std::fs::read_dir(dir).is_ok_and(|mut entries| entries.next().is_some()) {
733 eyre::bail!(
734 "cannot write the SolDB bundle to {}: the directory is not empty",
735 dir.display()
736 );
737 }
738 let mut contracts = BTreeMap::<(Address, B256), CombinedJson>::new();
740 let mut written_sources = HashSet::<PathBuf>::default();
741 for program in executed_programs(&arenas, contract_names, sources, known_contracts) {
742 if !program.solc {
744 continue;
745 }
746 let contract =
747 contracts.entry((program.address, program.deployed_hash)).or_insert_with(|| {
748 let path = |id: &u32| {
749 program.build_sources.get(id).map(|source| source.path.to_string_lossy())
750 };
751 let max_id = program.build_sources.keys().max().copied().unwrap_or_default();
752 CombinedJson {
753 name: program.name.to_string(),
754 key: format!(
755 "{}:{}",
756 path(&program.artifact.file_id).unwrap_or_default(),
757 program.name
758 ),
759 source_list: (0..=max_id)
761 .map(|id| path(&id).map_or_else(|| format!("<missing-{id}>"), Into::into))
762 .collect(),
763 fields: Map::new(),
764 }
765 });
766 let (source_map_key, bytecode_key) =
767 if program.init { ("srcmap", "bin") } else { ("srcmap-runtime", "bin-runtime") };
768 let source_map = program
769 .source_map
770 .iter()
771 .map(|element| {
772 format!(
773 "{}:{}:{}:{}:{}",
774 element.offset(),
775 element.length(),
776 element.index_i32(),
777 element.jump().to_str(),
778 element.modifier_depth()
779 )
780 })
781 .collect::<Vec<_>>()
782 .join(";");
783 contract.fields.insert(source_map_key.to_string(), source_map.into());
784 contract.fields.insert(bytecode_key.to_string(), hex::encode(program.code).into());
785 if let Some(layout) = &program.storage_layout {
786 contract.fields.insert("storage-layout".to_string(), layout.clone());
787 }
788
789 for source in program.build_sources.values() {
790 let relative = source.path.is_relative()
792 && source
793 .path
794 .components()
795 .all(|component| matches!(component, Component::Normal(_)));
796 if relative && written_sources.insert(source.path.clone()) {
797 let path = dir.join(&source.path);
798 if let Some(parent) = path.parent() {
799 fs::create_dir_all(parent)?;
800 }
801 fs::write(path, source.source.as_bytes())?;
802 }
803 }
804 }
805
806 let mut programs_at = HashMap::<Address, usize>::new();
807 for (address, _) in contracts.keys() {
808 *programs_at.entry(*address).or_default() += 1;
809 }
810 let mut mapping = Vec::new();
811 for ((address, code_hash), contract) in contracts {
812 let debug_dir = if programs_at[&address] > 1 {
813 format!("{}-{}", hex::encode_prefixed(address), hex::encode(&code_hash[..4]))
814 } else {
815 hex::encode_prefixed(address)
816 };
817 fs::create_dir_all(dir.join(&debug_dir))?;
818 write_json_file(
819 &dir.join(&debug_dir).join("combined.json"),
820 &json!({
821 "sourceList": contract.source_list,
822 "contracts": { contract.key: contract.fields },
823 }),
824 )?;
825 mapping.push(json!({
826 "address": hex::encode_prefixed(address),
827 "name": contract.name,
828 "debug_dir": debug_dir,
829 "code_hash": code_hash.to_string(),
830 }));
831 }
832 write_json_file(&dir.join("contracts.json"), &json!({ "contracts": mapping }))?;
833 let (trace, _) = transaction_trace(&mut arenas, recordings, &Breakpoints::default())?;
834 write_json_file(&dir.join("trace.json"), &trace)?;
835 Ok(())
836}
837
838struct CombinedJson {
840 name: String,
841 key: String,
843 source_list: Vec<String>,
844 fields: Map<String, Value>,
846}
847
848fn instruction_context(
851 element: &SourceElement,
852 build_sources: &HashMap<u32, Arc<SourceData>>,
853 used_sources: &mut BTreeSet<u32>,
854) -> SourceMapContext {
855 let code = element.index().filter(|index| build_sources.contains_key(index)).map(|index| {
857 used_sources.insert(index);
858 SourceLocation {
859 source_id: u64::from(index),
860 offset: u64::from(element.offset()),
861 length: u64::from(element.length()),
862 }
863 });
864 SourceMapContext {
865 code,
866 jump: match element.jump() {
867 Jump::In => SourceMapJump::Into,
868 Jump::Out => SourceMapJump::Out,
869 Jump::Regular => SourceMapJump::Regular,
870 },
871 modifier_depth: element.modifier_depth().into(),
872 }
873}
874
875fn status_error(status: Option<InstructionResult>) -> Option<String> {
877 status.filter(|status| !status.is_ok()).map(|status| format!("{status:?}"))
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883 use alloy_primitives::U256;
884 use foundry_compilers::artifacts::sourcemap;
885 use foundry_evm_traces::CallKind;
886 use soldb_ethdebug::SourceMapEnvironment;
887
888 fn step(pc: usize, op: OpCode, stack: &[u64]) -> CallTraceStep {
890 CallTraceStep {
891 pc,
892 op,
893 stack: Some(stack.iter().map(|word| U256::from(*word)).collect()),
894 push_stack: None,
895 memory: None,
896 returndata: Bytes::new(),
897 gas_remaining: 0,
898 gas_refund_counter: 0,
899 gas_used: 0,
900 gas_cost: 0,
901 state_gas_cost: None,
902 state_gas_reservoir: None,
903 state_gas_spent: 0,
904 storage_change: None,
905 status: (op == OpCode::STOP).then_some(InstructionResult::Stop),
906 immediate_bytes: None,
907 decoded: None,
908 }
909 }
910
911 #[test]
912 fn converts_steps_calls_and_storage_in_execution_order() {
913 let (caller, proxy, implementation) =
914 (Address::repeat_byte(1), Address::repeat_byte(0xa), Address::repeat_byte(0xb));
915 let mut arena = CallTraceArena::default();
916 {
917 let root = &mut arena.nodes_mut()[0];
918 root.trace = CallTrace {
919 caller,
920 address: proxy,
921 kind: CallKind::Call,
922 success: true,
923 steps: vec![
924 step(0, OpCode::PUSH1, &[]),
925 step(2, OpCode::SLOAD, &[0]),
926 step(3, OpCode::DELEGATECALL, &[0x29]),
927 step(4, OpCode::STOP, &[]),
928 ],
929 ..Default::default()
930 };
931 root.children.push(1);
932 root.ordering = vec![
933 TraceMemberOrder::Step(0),
934 TraceMemberOrder::Step(1),
935 TraceMemberOrder::Step(2),
936 TraceMemberOrder::Call(0),
937 TraceMemberOrder::Step(3),
938 ];
939 }
940 arena.nodes_mut().push(CallTraceNode {
942 parent: Some(0),
943 idx: 1,
944 trace: CallTrace {
945 caller: proxy,
946 address: implementation,
947 kind: CallKind::DelegateCall,
948 depth: 1,
949 success: true,
950 steps: vec![step(0, OpCode::SSTORE, &[0x2a, 0]), step(1, OpCode::STOP, &[])],
951 ..Default::default()
952 },
953 ordering: vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)],
954 ..Default::default()
955 });
956
957 let breakpoints = Breakpoints::from_iter([('a', (proxy, 3)), ('b', (implementation, 9))]);
958 let (trace, breakpoints) =
959 transaction_trace(&mut [arena], Vec::new(), &breakpoints).unwrap();
960 assert_eq!(
961 breakpoints,
962 BTreeMap::from([((proxy, 3), vec![(0..6, 1)]), ((implementation, 9), Vec::new())])
963 );
964
965 let steps = trace
966 .steps
967 .iter()
968 .map(|step| (step.pc(), step.mnemonic().to_string(), step.depth()))
969 .collect::<Vec<_>>();
970 assert_eq!(
971 steps,
972 [
973 (0, "PUSH1".to_string(), 1),
974 (2, "SLOAD".to_string(), 1),
975 (3, "DELEGATECALL".to_string(), 1),
976 (0, "SSTORE".to_string(), 2),
977 (1, "STOP".to_string(), 2),
978 (4, "STOP".to_string(), 1),
979 ]
980 );
981 let storage_diff =
982 |index: usize| serde_json::to_value(trace.steps.step(index).storage_diff()).unwrap();
983 assert_eq!(storage_diff(1), json!({ "0x0": { "before": null, "after": "0x29" } }));
984 assert_eq!(storage_diff(3), json!({ "0x0": { "before": null, "after": "0x2a" } }));
985 assert_eq!(storage_diff(4), json!({}));
986
987 let calls = trace
988 .artifacts
989 .calls
990 .iter()
991 .map(|call| {
992 (
993 call.parent_id,
994 call.entry_step,
995 call.exit_step,
996 call.call_type.as_str(),
997 call.to.as_str(),
998 call.bytecode_address.as_str(),
999 )
1000 })
1001 .collect::<Vec<_>>();
1002 let (proxy, implementation) =
1003 (hex::encode_prefixed(proxy), hex::encode_prefixed(implementation));
1004 assert_eq!(
1005 calls,
1006 [
1007 (None, Some(0), Some(6), "CALL", proxy.as_str(), proxy.as_str()),
1008 (
1009 Some(0),
1010 Some(3),
1011 Some(5),
1012 "DELEGATECALL",
1013 proxy.as_str(),
1014 implementation.as_str()
1015 ),
1016 ]
1017 );
1018 assert_eq!(trace.to_addr.as_deref(), Some(proxy.as_str()));
1019 assert_eq!(trace.from_addr, hex::encode_prefixed(caller));
1020 }
1021
1022 fn executed_c() -> (ContractSources, CallTraceArena, Vec<Option<String>>) {
1025 const CODE: [u8; 4] = [0x60, 0x80, 0x5b, 0x00];
1026 let mut sources = ContractSources::default();
1027 sources.sources_by_id.entry("build".to_string()).or_default().insert(
1028 0,
1029 Arc::new(SourceData {
1030 source: Arc::new("contract C {}".to_string()),
1031 language: Default::default(),
1032 path: PathBuf::from("src/C.sol"),
1033 contract_definitions: Vec::new(),
1034 }),
1035 );
1036 sources.artifacts_by_name.insert(
1037 "C".to_string(),
1038 vec![ArtifactData {
1039 source_map: None,
1040 source_map_runtime: Some(sourcemap::parse("0:8:0:-;9:3:0:i;1:2:1:o").unwrap()),
1041 pc_ic_map: None,
1042 pc_ic_map_runtime: Some(PcIcMap::new(&CODE)),
1043 build_id: "build".to_string(),
1044 file_id: 0,
1045 via_ir: Some(false),
1046 }],
1047 );
1048 let mut arena = CallTraceArena::default();
1049 let root = &mut arena.nodes_mut()[0].trace;
1050 root.address = Address::repeat_byte(0xc);
1051 root.bytecode = Some(Bytes::from_static(&CODE));
1052 root.steps = vec![step(0, OpCode::PUSH1, &[]), step(2, OpCode::JUMPDEST, &[0x80])];
1053 arena.nodes_mut()[0].ordering = vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)];
1054 (sources, arena, vec![Some("C".to_string())])
1055 }
1056
1057 #[test]
1058 fn adapts_source_maps_to_ethdebug_instructions() {
1059 let (sources, arena, names) = executed_c();
1060 let address = Address::repeat_byte(0xc);
1061
1062 let arenas = [arena];
1063 let names = [names];
1064 let programs = executed_programs(&arenas, &names, &sources, &Default::default());
1065 let contract = contract_debug_info(&programs[0], &mut SourceCache::default()).unwrap();
1066
1067 assert_eq!(contract.address, Some(hex::encode_prefixed(address)));
1068 assert_eq!(contract.code_generator, Some(CodeGenerator::Legacy));
1069 assert_eq!(contract.info.environment, "call");
1070 assert_eq!(contract.info.sources, BTreeMap::from([(0, "src/C.sol".to_string())]));
1071 let instructions = contract
1072 .info
1073 .instructions
1074 .iter()
1075 .map(|instruction| {
1076 json!({
1077 "offset": instruction.offset,
1078 "mnemonic": instruction.mnemonic(),
1079 "context": serde_json::to_value(instruction).unwrap()["context"],
1080 })
1081 })
1082 .collect::<Vec<_>>();
1083 assert_eq!(
1084 Value::Array(instructions),
1085 json!([
1086 {
1087 "offset": 0,
1088 "mnemonic": "PUSH1",
1089 "context": {
1090 "modifierDepth": 0,
1091 "code": { "source": { "id": 0 }, "range": { "offset": 0, "length": 8 } },
1092 },
1093 },
1094 {
1095 "offset": 2,
1096 "mnemonic": "JUMPDEST",
1097 "context": {
1098 "modifierDepth": 0,
1099 "code": { "source": { "id": 0 }, "range": { "offset": 9, "length": 3 } },
1100 "invoke": {},
1101 },
1102 },
1103 { "offset": 3, "mnemonic": "STOP", "context": { "modifierDepth": 0, "return": {} } },
1104 ])
1105 );
1106 }
1107
1108 #[test]
1109 fn keeps_all_root_calls_and_breakpoints() {
1110 let (_, mut deployment, _) = executed_c();
1111 deployment.nodes_mut()[0].trace.kind = CallKind::Create;
1112 deployment.nodes_mut()[0].trace.gas_used = 10;
1113 deployment.nodes_mut()[0].trace.success = true;
1114 let (_, mut set_up, _) = executed_c();
1115 set_up.nodes_mut()[0].trace.gas_used = 20;
1116 set_up.nodes_mut()[0].trace.success = true;
1117 let (_, mut test, _) = executed_c();
1118 test.nodes_mut()[0].trace.gas_used = 30;
1119 test.nodes_mut()[0].trace.status = Some(InstructionResult::Revert);
1120 let breakpoints = Breakpoints::from_iter([('a', (Address::repeat_byte(0xc), 2))]);
1121 let (trace, hits) =
1122 transaction_trace(&mut [deployment, set_up, test], Vec::new(), &breakpoints).unwrap();
1123 soldb_debugger::check_root_calls(&trace).unwrap();
1124 assert_eq!(trace.gas_used, 60);
1125 assert_eq!(trace.to_addr, Some(hex::encode_prefixed(Address::repeat_byte(0xc))));
1127 assert_eq!(trace.contract_address, None);
1128 assert!(!trace.success);
1129 assert_eq!(trace.error.as_deref(), Some("Revert"));
1130 assert_eq!(trace.steps.len(), 6);
1131 assert_eq!(trace.artifacts.creations[0].entry_step, Some(0));
1132 assert_eq!(trace.artifacts.creations[0].exit_step, Some(2));
1133 assert_eq!(trace.artifacts.calls[0].entry_step, Some(2));
1134 assert_eq!(trace.artifacts.calls[1].entry_step, Some(4));
1135 assert_eq!(trace.artifacts.calls[1].exit_step, Some(6));
1136 assert!(trace.artifacts.calls.iter().all(|call| call.parent_id.is_none()));
1137 assert_eq!(hits[&(Address::repeat_byte(0xc), 2)], [(0..2, 1), (2..4, 1), (4..6, 1)]);
1138 assert!(transaction_trace(&mut [], Vec::new(), &Breakpoints::default()).is_err());
1139 }
1140
1141 #[test]
1142 fn records_transient_storage_and_return_data() {
1143 let mut builder = TraceBuilder::default();
1144 let store = step(0, OpCode::TSTORE, &[42, 7]);
1145 let load = step(1, OpCode::TLOAD, &[7]);
1146 let mut stop = step(2, OpCode::STOP, &[42]);
1147 stop.returndata = Bytes::from_static(&[0xab, 0xcd]);
1148 builder.push_step(&store, Some(&load), 1);
1149 builder.push_step(&load, Some(&stop), 1);
1150 builder.push_step(&stop, None, 1);
1151 builder.push_step(&stop, None, 1);
1152 builder.push_step(&store, None, 1);
1153 let expected = BTreeMap::from([("0x7".to_string(), "0x2a".to_string())]);
1154 assert_eq!(*builder.steps.step(0).transient_storage(), expected);
1155 assert_eq!(*builder.steps.step(1).transient_storage(), expected);
1156 assert!(builder.steps.step(2).transient_storage().is_empty());
1157 assert!(builder.steps.step(0).return_data().is_none());
1158 assert_eq!(builder.steps.step(2).return_data(), Some("abcd"));
1159 assert!(builder.steps.step(4).return_data().is_none());
1160 assert_eq!(
1161 builder.steps.step(2).return_data().unwrap().as_ptr(),
1162 builder.steps.step(3).return_data().unwrap().as_ptr(),
1163 );
1164 }
1165
1166 #[test]
1167 fn writes_a_bundle_soldb_loads() {
1168 let (sources, arena, names) = executed_c();
1169 let dir = tempfile::tempdir().unwrap();
1170
1171 write_bundle(dir.path(), vec![arena], Vec::new(), &[names], &sources, &Default::default())
1172 .unwrap();
1173
1174 let address = hex::encode_prefixed(Address::repeat_byte(0xc));
1175 let mapping = soldb_ethdebug::read_json_file(&dir.path().join("contracts.json")).unwrap();
1176 let code_hash = keccak256([0x60, 0x80, 0x5b, 0x00]).to_string();
1177 assert_eq!(
1178 mapping,
1179 json!({ "contracts": [{
1180 "address": address,
1181 "name": "C",
1182 "debug_dir": address,
1183 "code_hash": code_hash,
1184 }] })
1185 );
1186 let program = soldb_ethdebug::load_debug_program_with_sources(
1187 &dir.path().join(&address),
1188 "C",
1189 SourceMapEnvironment::Runtime,
1190 &[],
1191 )
1192 .unwrap()
1193 .unwrap();
1194 assert!(program.legacy);
1195 assert!(program.missing_sources.is_empty());
1196 assert_eq!(program.source_contents, BTreeMap::from([(0, "contract C {}".to_string())]));
1197 let offsets = program.info.instructions.iter().map(|i| i.offset).collect::<Vec<_>>();
1198 assert_eq!(offsets, [0, 2, 3]);
1199 let trace = std::fs::read_to_string(dir.path().join("trace.json")).unwrap();
1200 let trace = serde_json::from_str::<TransactionTrace>(&trace).unwrap();
1201 assert_eq!(trace.steps.len(), 2);
1202
1203 let (sources, arena, names) = executed_c();
1205 let error = write_bundle(
1206 dir.path(),
1207 vec![arena],
1208 Vec::new(),
1209 &[names],
1210 &sources,
1211 &Default::default(),
1212 )
1213 .unwrap_err();
1214 assert!(error.to_string().ends_with("the directory is not empty"));
1215 }
1216}