Skip to main content

foundry_debugger/
soldb.rs

1//! [soldb](https://github.com/walnuthq/soldb) frontend.
2//!
3//! Converts a recorded call trace into a soldb transaction trace and the local artifacts' source
4//! maps into soldb debug info, then runs a soldb session over them or writes them as a bundle that
5//! soldb's own tools read.
6
7use 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
44/// The frames that run a breakpoint location, as the steps they span and their depth.
45type BreakpointFrames = Vec<(Range<usize>, u64)>;
46
47/// The frames that run each breakpoint's code address and program counter.
48type BreakpointSteps = BTreeMap<(Address, usize), BreakpointFrames>;
49
50/// A soldb session over the recorded call traces.
51#[derive(Debug)]
52pub(crate) struct SoldbDebugger {
53    trace: TransactionTrace,
54    contracts: Vec<ContractDebugInfo>,
55    /// The program counter of each `vm.breakpoint` location and the frames that run it.
56    breakpoints: Vec<(u64, BreakpointFrames)>,
57}
58
59impl SoldbDebugger {
60    /// Converts `arenas`, whose node contracts are identified by `contract_names`.
61    ///
62    /// The steps are the ones `recordings` hold, one per arena, or the arenas' own steps when
63    /// `recordings` is empty.
64    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    /// Runs the session.
87    ///
88    /// If stdin and stdout are terminals, the full-screen view opens first. Then commands are
89    /// read from stdin, one per line, until `quit` or the end of the input.
90    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        // soldb breakpoints match a program counter in any contract, so each one is limited to
94        // the own steps of the frames that run the `vm.breakpoint` location: the steps they span
95        // at their depth, which leaves out their subcalls.
96        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/// What the session asks the frontend to do after a command.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141enum Flow {
142    Continue,
143    Tui,
144    Quit,
145}
146
147/// Prints the session's answers.
148fn 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
161/// Opens the full-screen view until the user leaves it.
162fn 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
174/// Converts `arenas` into a soldb trace: their steps in execution order, with each call and
175/// creation recorded at the steps it spans.
176///
177/// The steps are the ones `recordings` hold, one per arena, or the arenas' own steps when
178/// `recordings` is empty. Also returns the steps that run each `breakpoints` location. The steps
179/// move out of `arenas`, so each one is dropped once converted.
180fn 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    // The arenas run in order, such as the deployment, the setup, and the test, so the last one
220    // is the call being debugged.
221    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/// Collects the soldb steps, calls, and creations of a trace in execution order.
257#[derive(Default)]
258struct TraceBuilder {
259    steps: StepTable,
260    calls: Vec<ExecutionCall>,
261    creations: Vec<ContractCreation>,
262    /// Converts the return data and storage of the steps.
263    converter: StepConverter,
264    /// The frames that run each `vm.breakpoint` location, as code address and program counter.
265    breakpoints: BreakpointSteps,
266}
267
268impl TraceBuilder {
269    /// Records node `idx` and its subcalls. `parent` is the id of the enclosing call.
270    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        // The ordering lists the steps in recording order.
283        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                        // Free the converted snapshots now, not when the frame ends.
298                        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    /// Records node `idx` and its subcalls, whose steps `frames` locate in the step table from
315    /// `offset`. `parent` is the id of the enclosing call.
316    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        // The frame's own steps are the ones between its subcalls.
332        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    /// Adds the program counters of the `steps` that run a breakpoint location of `address` to
355    /// `hits`.
356    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    /// Records that the frame of `address` that spans `steps` at `depth` runs the breakpoint
369    /// locations at the program counters `hits`.
370    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    /// Records the call or creation `trace` that starts at `entry_step`, and returns its
379    /// creation or call id.
380    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        // The code the frame runs, as `executed_programs` hashes it, so each frame maps through
390        // its own program when several run at one address.
391        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            // A delegated call runs `address`'s code against the storage of `caller`.
415            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    /// Records that the creation or call that `open_frame` returned ends before `exit_step`, and
441    /// whether it `runs_steps` of its own.
442    ///
443    /// SolDB finds the frame that runs a step by the step it was entered at. A frame that runs no
444    /// step of its own but whose subcalls do, such as `vm.deployCode` around the creation it
445    /// makes, would share its entry step with its first subcall and take the subcall's steps, so
446    /// it records no entry step.
447    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    /// Records `step`. `next` is the following step of the same frame, if any.
470    fn push_step(&mut self, step: &CallTraceStep, next: Option<&CallTraceStep>, depth: u64) {
471        let stack = step.stack.as_deref().unwrap_or_default();
472        // The tracer shares one buffer between steps that leave memory unchanged, so this only
473        // bumps a reference count.
474        let memory = step.memory.as_ref().map(|memory| memory.as_bytes().0.clone());
475        // The full stack snapshots do not record pushes, but an `SLOAD` or `TLOAD` leaves its
476        // value on top of the next step's stack.
477        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        // `None` would keep the previous step's buffer, so an empty one is spelled out too.
485        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
505/// The step table that holds the steps of `recordings`, and where each recording's steps start in
506/// it.
507///
508/// The recorders of one session append to one table in execution order, so it is taken as it is
509/// when the recordings cover it in order. Otherwise their steps are copied into a new table.
510fn 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
562/// A program the trace executes and the local artifact that describes it.
563struct ExecutedProgram<'a> {
564    address: Address,
565    name: &'a str,
566    code: &'a Bytes,
567    /// The hash of `code`, which tells apart programs run at one address.
568    code_hash: B256,
569    /// The hash of the deployed code: `code_hash` for deployed code, and for creation code the
570    /// hash of the code it returned, so a contract's two programs share a bundle entry.
571    deployed_hash: B256,
572    /// Whether the program is creation code.
573    init: bool,
574    artifact: &'a ArtifactData,
575    /// Solc indexes source maps by instruction counter, but Vyper indexes by program counter.
576    solc: bool,
577    source_map: &'a SourceMap,
578    pc_ic_map: Option<&'a PcIcMap>,
579    /// The sources of the build that compiled the program, by source id.
580    build_sources: &'a HashMap<u32, Arc<SourceData>>,
581    /// The contract's storage layout, as solc writes it.
582    storage_layout: Option<Value>,
583}
584
585/// The programs `arenas` run that local artifacts describe, once per address and environment.
586fn 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
644/// Adapts the source map of `program` to its instructions, in the ETHDebug shape soldb reads.
645fn 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    // The code generator decides whether soldb can infer local variables from the stack.
694    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
715/// Writes a bundle that soldb's own tools read, for example
716/// `soldb profile --trace-file <dir>/trace.json --contracts <dir>/contracts.json`.
717///
718/// The bundle holds the transaction trace, a contracts mapping, one `combined.json` per contract
719/// address with the legacy source maps and bytecode `solc --combined-json` writes, and the sources
720/// under their project paths, where soldb looks for them.
721pub(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    // The bundle copies the compiler's sources under their project paths, and they can differ
730    // from the files on disk, so a directory with files, such as the project root, could have its
731    // files replaced.
732    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    // One entry per deployed code, so code replaced at an address keeps its own source maps.
739    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        // soldb reads legacy source maps by instruction counter, which Vyper's are not.
743        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                    // Ids without a build source stay unreadable placeholders.
760                    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            // Only project-relative paths are copied, so nothing is written outside `dir`.
791            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
838/// One contract of a bundle's `combined.json`.
839struct CombinedJson {
840    name: String,
841    /// The contract's key, `<source path>:<name>`.
842    key: String,
843    source_list: Vec<String>,
844    /// The source maps, bytecode, and storage layout, under solc's field names.
845    fields: Map<String, Value>,
846}
847
848/// The ETHDebug context of one source map element: its source range, if the build has the
849/// source, and the `invoke` or `return` marker of a jump into or out of a function.
850fn instruction_context(
851    element: &SourceElement,
852    build_sources: &HashMap<u32, Arc<SourceData>>,
853    used_sources: &mut BTreeSet<u32>,
854) -> SourceMapContext {
855    // Compiler-generated code points at sources that are not in the build.
856    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
875/// The error a failed step or call ended with.
876fn 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    /// A step as debug mode records it: the full stack, without pushes.
889    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        // A delegated call records the storage context as its caller.
941        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    /// `PUSH1 0x80; JUMPDEST; STOP` as contract `C` at `0x0c..0c`. The last source map element
1023    /// points at a generated source.
1024    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        // The header describes the test call, not the deployment.
1126        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        // A second bundle would replace the files of the first, as it would a project's sources.
1204        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}