Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ reports/
state.kore
.claude/
test/fixtures/sample-contract/target/
test/fixtures/*/target/
examples/*/target/
# Fixture crates are built by hand only, to regenerate the committed .wasm.
test/fixtures/*/Cargo.lock
74 changes: 69 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,77 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Multi-transaction debug configurations.** A launch config now states an
ordered `transactions` sequence of `deploy` / `invoke` steps, run against one
accumulating local ledger, with `trace` selecting which of them feeds the
session (`"last"` by default, or an index, or a step `id`). Constructors,
seeded state and multi-step flows are debuggable, not just a single bare call.
Each transaction's status is reported in the debug console, and a step that
fails or traps no longer aborts the run — its trace is still fetched and
replayed.
- **Spec-driven arguments.** An invoke's `args` is an object keyed by the
function's own parameter names, encoded against the contract's
`contractspecv0` spec, so structs, enums, tuples, vecs and maps work without
hand-written ScVal type tags. The tokens `${sourceAddress}` and
`${contract:<id>}` expand inside string values, wiring a deployed contract's
address into a later call.
- **Just-my-code stepping** (`justMyCode`, default true): source stepping rests
only in workspace files, skipping Rust `std`/`core` and crates.io dependency
sources. `--no-just-my-code` opts out in the CLI.
- **Stellar ledger inspection.** A new **Ledger** scope shows the chain state at
every step: contract storage across all three durabilities with their TTLs,
account balances, the ledger sequence and close time, the executing contract's
wasm hash and instance TTL, the host object table, and the open contract-call
stack. Values render as Soroban types with `C…`/`G…` addresses, and composites
expand. Storage is reconstructed from the trace's own call baselines and write
events — including undoing the writes of a sub-call that trapped — so it
matches what the contract would read.
- **WebAssembly globals.** A new **Globals** scope lists the executing module's
globals by module-relative index, for traces that carry them.
- The `soroban-trace` CLI reports the same state per stop as `globals` and
`ledger`, with a `changed` flag marking the storage entries that moved since
the previous stop, and `hasGlobals`/`hasLedger` announced in `meta`.
- New contributor spec: [`docs/state-inspection.md`](docs/state-inspection.md),
whose numbered rules (G1–G4, L1–L15) the test suite pins.

### Changed

- **The single-invoke launch config is gone.** `contract`, `function`, `args`,
`buildCommand` and `debugInfo` no longer sit at the top level: wrap them in a
`transactions` array (see [`docs/debug-config.md`](docs/debug-config.md)). A
config still using the old shape is rejected with a message pointing at the
new one, rather than silently ignored.
- **Invoke arguments are spec-driven only.** The positional
`[{ "type", "value" }]` form is removed, along with the hand-written ScVal
encoder behind it; the contract's own spec now decides how each argument
encodes. `soroban-trace --args-json` takes the same named object.
- **Requires komet v0.1.87 or newer.** That release reorganised the trace: every
record now names itself with a `kind` field, and the operands that used to ride
inside `instr` are named fields of the record. The parser reads that shape and
rejects a record without a `kind`, so a trace recorded against an older komet
no longer replays — re-record it. Failing loudly is deliberate: a trace this
parser cannot classify would otherwise open a session with every state view
mysteriously empty.
- The cross-contract gate no longer relies on komet-node tagging each trace
record with the contract executing at it. The adapter folds that out of the
`callContract`/`endWasm` boundaries the trace already carries, so nothing needs
to be sent per record for it.

### Fixed

- Launch argument encoding now rejects invalid integer values instead of
silently accepting them: non-integer and out-of-range `u32`/`i32` values (the
SDK encoded these verbatim) and out-of-range wide integers such as `u64`
`2^64` (the SDK silently wrapped these to `0`) now raise a clear
`ScValEncodeError`.
- Debug sessions start ~8 seconds faster: rendering Stellar addresses no longer
pulls `@stellar/stellar-sdk` into the debug adapter's module graph (a local
strkey encoder replaces it), which had been delaying every session past the
DAP handshake timeout.
- The invocation's return value is reported again in the debug console (and in
the CLI's `result` line), read from the trace's own call-exit record; a call
that trapped says so.
- Byte-identical transactions in one run are no longer deduplicated by
komet-node into a single execution: every envelope carries its own account
sequence, so calling the same function twice with the same arguments really
runs twice.
- DWARF type resolution no longer hangs on malformed debug info containing a
cyclic `typedef`/qualifier chain; `stripTypedefs` now terminates on cycles.

Expand Down
18 changes: 17 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,30 @@ debugAdapter/
SorobanDebugSession DAP handlers (cursor moves + StoppedEvents, disassembly)
TraceModel records, cursor, call-depth, line + instruction stepping
artifacts.ts wasm bytes -> { mapper, disassembly, validated positions }
TraceModel records + replay cursor; owns the two state images below,
built lazily and shared by every consumer
MemoryImage linear memory at a cursor (snapshot-on-change index)
LedgerImage Stellar ledger at a cursor: storage/TTLs, balances,
ledger info, host objects, call stack, and the executing
contract; undoes the writes of a trapped sub-call
(docs/state-inspection.md)
ledgerView.ts the ledger presentation both views share: one snapshot
per stop, rendered as a lazy ChildVar tree for DAP
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)
traceEvents.ts Soroban VM event payloads -> TraceEvent (a malformed or
unknown payload degrades to no event, never fails a session)
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)
soroban/specEncode.ts invoke args -> ScVals, encoded against the contract's own
contractspecv0 spec; `${...}` substitution
soroban/scvalJson.ts trace ScVal JSON -> DecodedValue (display + lazy children)
soroban/strkey.ts raw address bytes -> C…/G… strkey (SDK-free: the SDK costs
~8s of module load inside the adapter, which alone blows
the DAP handshake)
wasm/
sections.ts wasm section walker (offsets, custom-section lookup)
Disassembly.ts static disassembly (wasmparser), code-offset addressed
Expand Down
74 changes: 53 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ directions.
- ⏪ **Step backward.** Step back and reverse-continue as easily as going
forward. Overshot the bug? Just step back. Backward stepping is instant.
- 🔎 **Inspect state at every step.** See the values in play at the current
point of execution.
point of execution — your Rust variables, the wasm locals, stack and globals.
- 🏦 **See the ledger, not just the code.** Contract storage (instance,
persistent, temporary) with TTLs, account balances, ledger sequence and close
time, the host object table, and the contract-call stack — all at the step
you're on, and all time-travelling with you.
- 🚀 **One click from source to debugger.** Point the debugger at a contract and
a function; it builds, deploys to a local network, runs the call, and drops
you into the debug session. No manual setup.
Expand Down Expand Up @@ -51,41 +55,69 @@ at all — so you can see the debugger working in seconds. See

## Usage

Add a `soroban` configuration to your `.vscode/launch.json`. The common case is:
build a contract, run a function on a local network, and debug the result.
Add a `soroban` configuration to your `.vscode/launch.json`. A configuration describes an ordered sequence of transactions run against one fresh local ledger, and names which transaction to trace and debug — the last one by default. This lets you set up whatever state the call under test depends on (deploy other contracts, run a constructor, seed storage) before the transaction you actually want to step through.

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "Debug add(1, 2)",
"contract": "${workspaceFolder}", // crate dir containing Cargo.toml
"function": "add",
"args": [
{ "value": 1, "type": "u32" },
{ "value": 2, "type": "u32" }
]
"name": "Debug supply",
"transactions": [
// deploy: build a crate dir (or point `wasm` at a prebuilt .wasm) and
// register it under a handle `id` that later invokes reference.
{ "kind": "deploy", "id": "pool", "contract": "${workspaceFolder}" },

// invoke: call a function on a deployed handle. `args` is an object keyed
// by the function's parameter names.
{ "kind": "invoke", "contract": "pool", "function": "__constructor",
"args": { "admin": "${sourceAddress}" } },
{ "kind": "invoke", "contract": "pool", "function": "supply",
"args": { "requests": [[{ "tag": "Native" }, "1000"]] } }
],
"trace": "last"
}
```

Set a breakpoint in your contract's Rust source, start the configuration, and
step through — forward or backward.
Set a breakpoint in your contract's Rust source, start the configuration, and step through the traced transaction — forward or backward.

Even the simplest single-call session is one `deploy` plus one `invoke` — there is one config shape, so a two-contract system is just a longer `transactions` array. See [`docs/debug-config.md`](docs/debug-config.md) for the complete reference, including multi-contract systems, composite argument types, and offline replay.

### Configuration reference

Top-level attributes:

| Attribute | Description |
|-----------|-------------|
| `function` *(required)* | Name of the contract function to invoke and debug. |
| `args` | Function arguments, each `{ "value": …, "type": "u32" \| "i128" \| "symbol" \| "address" \| … }`. |
| `contract` | Path to the contract crate directory (with `Cargo.toml`). Defaults to `${workspaceFolder}`. |
| `wasmPath` | Path to a prebuilt `.wasm`. Overrides building from `contract`. |
| `debugInfo` | Build with debug info for Rust source mapping (default `true`; set `false` to debug at the wasm level only). |
| `rawTrace` | Replay a previously recorded run from a file instead of building and deploying. |
| `transactions` | Ordered, non-empty array of `deploy` / `invoke` steps (see below) — the live sequence to run. |
| `trace` | Which transaction feeds the debug session: `"last"` (default), a 0-based index into `transactions`, or a step `id` (a deploy's `id` or an invoke's optional `id`). |
| `sourceSecret` | Source account secret (`S…`) used to sign every transaction. A deterministic account is derived if omitted. Its address is available in `args` as `${sourceAddress}`. |
| `node` | Local-network connection/spawn settings: `attach`, `host`, `port`, `command`, `ioDir`. |
| `sourceSecret` | Optional source account secret (`S…`). A fresh account is used if omitted. |
| `rawTrace` | Replay a previously recorded run from a file instead of building and deploying (optionally with `wasmPath` for source mapping). |

A **`deploy`** step uploads a contract and registers a handle:

| Field | Description |
|-------|-------------|
| `kind` *(required)* | `"deploy"`. |
| `id` *(required)* | Handle name that later `invoke` steps reference via their `contract` field, and that `trace` can select. |
| `contract` | Path to the contract crate directory (with `Cargo.toml`) to build. |
| `wasm` | Path to a prebuilt `.wasm`. Overrides building from `contract`; one of `contract` / `wasm` is required. |
| `buildCommand` | Command used to build a `contract` dir (default `stellar contract build`). |
| `debugInfo` | Build with debug info for Rust source mapping (default `true`; set `false` to debug at the wasm level only). |

An **`invoke`** step calls a function on a deployed handle:

| Field | Description |
|-------|-------------|
| `kind` *(required)* | `"invoke"`. |
| `contract` *(required)* | Handle `id` of an earlier `deploy` step. |
| `function` *(required)* | Name of the contract function to call. |
| `args` | Arguments, as an object keyed by the function's parameter names. Values follow the contract's own spec, so composites work: an enum is `{ "tag": "Native" }` or `{ "tag": "Other", "values": [7] }`, a tuple or vec is a JSON array, an `i128` is a decimal string, an address is a `G…`/`C…` string. |
| `id` | Optional label so `trace` can select this invoke's transaction. |

Two substitution tokens are expanded inside string `args` values: `${sourceAddress}` (the source account's address) and `${contract:<id>}` (the deployed address behind a handle).

Two settings let you point at executables that aren't on your `PATH`:
`soroban.stellar.path` and `soroban.kometNode.path`.
Two settings let you point at executables that aren't on your `PATH`: `soroban.stellar.path` and `soroban.kometNode.path`. For the full reference — multi-contract systems, every argument shape, and offline replay — see [`docs/debug-config.md`](docs/debug-config.md).

### Beyond the editor

Expand Down
Loading