Skip to content

Latest commit

 

History

History
233 lines (187 loc) · 10.7 KB

File metadata and controls

233 lines (187 loc) · 10.7 KB

Contributing

Audience: contributor · maintainer

TL;DR: How to hack on the extension — set up a dev environment (devcontainer or by hand), the everyday build/lint/test commands, the test-first convention and how to regenerate fixtures, and a tour of how the trace-replay adapter works internally (architecture map included).

Thanks for your interest in improving the Soroban Debugger! This document covers how to get a development environment running and the conventions we follow.

Development setup

The quickest path is the included devcontainer, which installs the full toolchain — Rust with the wasm targets, the Stellar CLI, and komet-node (via kup install komet-node, which also pulls the K toolchain and prebuilt semantics). Open the repo in VSCode and choose Reopen in Container, or use the GitHub Codespaces button. See .devcontainer/Dockerfile.

To set things up by hand instead, you need:

  • Node.js ≥ 22
  • For the live pipeline only: a Rust toolchain with the wasm32v1-none (or wasm32-unknown-unknown) target, the Stellar CLI, and komet-node.

Then:

npm install

SSH keys in the devcontainer

~/.ssh is a named Docker volume, so keys persist across container rebuilds — but the volume starts empty on first build, so add a key once. The volume lives outside /workspace, so keys can't be committed to the repo, and Docker excludes volumes from docker commit / image builds. (Anyone with root or docker-group access on the host can still read the volume, so prefer a passphrase or a dedicated, revocable key.)

Inside the container, generate a key:

ssh-keygen -t ed25519 -C "you@example.com"   # writes ~/.ssh/id_ed25519[.pub]
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

Then add the public key (~/.ssh/id_ed25519.pub) to your GitHub account at https://github.com/settings/ssh/new. To reuse an existing host key instead, docker cp it into the container's ~/.ssh and apply the same chmod.

Everyday commands

npm run build        # bundle to dist/extension.js (esbuild)
npm run watch        # rebuild on change
npm run check-types  # tsc --noEmit
npm run lint         # eslint
npm test             # tsc -p tsconfig.test.json, then mocha (~2 min)

Press F5 (Run Extension) to open an Extension Development Host with the extension loaded and the examples/ workspace open. Pick a configuration from the Run and Debug view — the Replay … with symbols configs need no toolchain at all.

Changing the semantics (komet / wasm-semantics)

The debugger consumes an upstream chain, komet-node → komet → wasm-semantics, but you rarely touch it. Pick your mode:

1. Just debugging (almost always). Do nothing special — the devcontainer already has the komet-node binary (from kup). Press F5. You never need nix, uv, or any of the tooling below.

2. Changing the semantics and seeing it in the debugger. Use the single front-end scripts/dev.sh:

./scripts/dev.sh setup   # check out the chain into .deps/ and wire it up
# …edit .deps/wasm-semantics or .deps/komet…
./scripts/dev.sh build   # fast incremental rebuild
./scripts/dev.sh use     # make it the debugger's komet-node, then press F5

3. Upstreaming the change as PRs. Once you have commits on a shared branch in the checkouts, dev.sh pr open <branch> opens a cross-linked PR in each repo (pr status <branch> / --dry-run to preview first).

That's the whole developer surface. Everything else is plumbing the script drives for you:

  • You never run nix, uv, or kdist by hand — setup/build do.
  • .deps/ (leading dot) is generated and gitignored — throwaway local checkouts; delete it any time to reset. Not to be confused with the committed deps/ folders in komet/komet-node, which hold version pins (below).
  • This repo carries no version-pin files. The pins that connect the chain (deps/*_release, uv.lock, flake inputs) live upstream in komet/komet-node and are bumped by CI, not by you.
Why it needs checkouts (background)

The chain is pinned by uv git dependencies, not Nix flake inputs, so kup --override can't reach komet/wasm-semantics. setup therefore checks the repos out side-by-side (at the versions currently pinned, or --tip for latest) and wires them with uv path sources so an edit flows up the chain with no version bumps. build/shell are the fast inner loop (kdist caches per target, toolchain from komet-node's Nix dev shell); use does one Nix realize so the debugger gets the exact release build — doubling as a parity check. Revert with kup install komet-node.

Testing conventions

  • Write tests first. New behavior should arrive with a failing test that describes it, then the implementation that makes it pass. The suite is the contract; keep it green (npm test) before opening a PR.
  • Replay logic is deliberately free of the vscode API so it can be unit-tested in plain Node. Keep vscode-only code in extension.ts.
  • Tests run automatically in CI on every push and pull request (Node 22): type-check, lint, build, and test.

Regenerating fixtures

The DWARF/trace fixtures under test/fixtures/ are real build + trace outputs and must stay matched (the wasm's DWARF and the trace's positions are checked against each other). Regenerate them as a pair:

scripts/make-fixtures.sh          # rebuild the debug wasms + capture matching traces
node scripts/verify-addresses.mjs # re-derive the address-space ground truth vs a live komet-node

These need the full toolchain (Rust + Stellar CLI + komet-node).

How it works

The debug adapter is a trace-replay cursor machine. komet-node executes a whole transaction and returns the entire execution trace — one record per WebAssembly instruction — and the adapter loads that into an in-memory model and services every DAP stepping request by moving a cursor. Because the whole recording is in memory, stepping backward is just as cheap as stepping forward. The adapter runs in-process in the extension host (DebugAdapterInlineImplementation).

flowchart TB
    subgraph build["Build (LiveBackend)"]
      CRATE["contract crate"] -->|"CARGO_PROFILE_RELEASE_DEBUG=true<br/>STRIP=none, OPT_LEVEL=0"| WASM["wasm + DWARF<br/>(pristine linker output)"]
    end
    WASM --> KOMET["komet-node<br/>executes the whole transaction"]
    KOMET --> TRACE["entire trace<br/>one record per wasm instruction"]

    subgraph model["In-memory model — vscode-free"]
      TRACE --> VAL["validate positions<br/>vs static disassembly"]
      WASM -. "DWARF" .-> MAP["map code offset → Rust file:line"]
      VAL --> CUR["cursor machine<br/>(forward == backward cost)"]
      MAP --> CUR
    end

    CUR -->|"each DAP request<br/>just moves the cursor"| DAP["SorobanDebugSession<br/>StoppedEvents / frames / disassembly"]
Loading

A rawTrace replay skips the Build and komet-node stages entirely — the JSONL trace is loaded straight into the model (and a paired wasmPath still feeds the DWARF/disassembly seams).

  • The build injects debug info without touching your Cargo.toml. It sets CARGO_PROFILE_RELEASE_DEBUG=true / CARGO_PROFILE_RELEASE_STRIP=none for stellar contract build, so the wasm carries DWARF. The pristine linker output (target/…/release/deps/*.wasm) is what gets uploaded, because the Stellar CLI's metadata-injection step rewrites the wasm and strips the DWARF line programs.
  • DWARF → Rust. An in-repo DWARF v4/v5 line-table parser (src/dwarf/) maps wasm code offsets to Rust file:line. Breakpoints set in Rust source verify against the executed trace (sliding forward to the nearest executed line).
  • No-DWARF fallback. A prebuilt wasm without debug info — or a rawTrace replay without wasmPath — degrades gracefully to disassembly-only debugging: frames carry an instruction pointer but no source.
  • Positions are validated. komet-node's pos is relative to the section being executed (e.g. the code section for function code, the globals section for global initializers), so every record is cross-checked against the static disassembly and only trusted when the mnemonics agree. komet-node's tracer stops at instructions it cannot decode (printing them as unknown, e.g. if), so a trace can be a prefix of the full execution.

Architecture

extension.ts            VSCode glue: config provider + inline adapter factory
debugAdapter/
  SorobanDebugSession   DAP handlers (cursor moves + StoppedEvents, disassembly)
  TraceModel            records, cursor, call-depth, line + instruction stepping
  artifacts.ts          wasm bytes -> { mapper, disassembly, validated positions }
  backends/
    RawTraceBackend     replay a JSONL trace file (+ optional wasmPath for symbols)
    LiveBackend         turnkey build + spawn + deploy + trace
komet/
  trace.ts              JSONL -> TraceRecord[] (K-style mnemonics, section-relative pos)
  mnemonics.ts          K-style instr arrays -> wasm mnemonics ('i64.const 255')
  KometClient.ts        JSON-RPC client (getHealth/sendTransaction/traceTransaction/...)
soroban/scval.ts        launch args -> ScVals (@stellar/stellar-sdk)
wasm/
  sections.ts           wasm section walker (offsets, custom-section lookup)
  Disassembly.ts        static disassembly (wasmparser), code-offset addressed
dwarf/                  DWARF v4/v5 .debug_line/.debug_info parser -> LineTable
sourcemap/
  SourceMapper          the mapping seam the adapter talks to
  DwarfSourceMapper     trace index / code offset -> Rust file:line (+ breakpoints)
  NullSourceMapper      no-DWARF fallback (disassembly-only)

All replay logic is free of the vscode API, so it can be unit-tested in plain Node; the vscode-only glue lives in extension.ts. For a deep dive on the stepping model, see docs/stepping.md.

Pull requests

  • Branch off main and keep PRs focused on a single change.
  • Make sure npm run check-types, npm run lint, and npm test all pass.
  • Write clear commit messages that explain the why, not just the what.
  • Update the CHANGELOG under Unreleased for user-facing changes.

Reporting bugs

Because the debugger replays a captured trace, a JSONL trace file is often the most useful thing to attach to a bug report — it reproduces a session with no toolchain or node required (rawTrace in a launch config). See the issue templates when you open an issue.