Step through a transaction
This page steps through real transactions in this browser tab, offline, from saved files. At each EVM step, it highlights the source range. A debugger engine does the debugging, in the browser. Each tab names its engine. The page only shows what the engine reports.
Part of the ethdebug post appendix, with Storage, by name and ethdebug in depth.
Engine: soldb, Walnut's debugger
Engine: ethdebug's reference implementation, not soldb
Engine: a source map
stepper on this page (srcmap-worker.js), with no
ethdebug
Call stack
Innermost first, from
invoke and return contexts.
Innermost first, from the
source map's jump types: a JUMP marked i
opens a frame, one marked o closes it.
Variables
Contract state, from ethdebug alone: solc's pointers for storage. No storage layout.
Variables in scope, from the
context's variables. Each is read through its pointer
with dereference.
A trace holds only the storage that the transaction touches. A value shows as unknown until the transaction reads or writes it.
Decoding is a simple stand-in (uint, address, and arrays and structs of these) for the format's planned interpretation layer.
Inlining
Every tab steps the same contract, Arcade
(Arcade.sol), and the same transaction. In
Arcade, players join with a name, then call
play(), and a roll from the block gives a hit or a miss. A
hit raises the player's combo by one and scores 10 × combo (up to
5×); a miss resets the combo. The story: alice, bob and carol
join; alice hits twice, bob hits, carol hits four times, then
misses. The transaction is alice's third play(), a hit at combo 3:
_applyCombo(10, 3) is 30, and totalScore goes
from 140 to 170. Each call ran on a local anvil node. anvil cannot set the
block's randomness, so the script replays a play() from a
snapshot until it rolls the story's outcome.
soldb and this page step through Solidity and Fe with the same code. Both compilers write ethdebug.
Compiler: Walnut's solc fork (walnuthq/solidity PR #10, head
c434b2ea), with ethdebug output. Its pointers let soldb
read the contract's state. solc writes ethdebug only with the optimizer
off, so this build is via-IR without the optimizer. The trace is soldb's
saved trace of the transaction (1.5 MB, 2,228 EVM steps). soldb parses
the trace (Trace.fromJson) and reads the ethdebug
(attachEthdebug). Then it maps each step to its source
(step(i)) and reads the contract's state
(state(i)).
soldb, a Solidity debugger, steps through a Fe contract. It uses only Fe's ethdebug output. The page changes only the file layout (see the list below).
Arcade in Fe (arcade.fe).
Compiler: Fe 26.4.1, at -O 0. A Fe StorageMap
value must fit in one word, so this port keeps one map per
Player field; hits is a map keyed by index.
The page loads the node's saved responses with soldb's
Trace.fromTransaction, then calls
attachEthdebug and step(i), as on the
Solidity tab.
This tab runs ethdebug's reference implementation,
not soldb: @ethdebug/pointers, @ethdebug/evm
and the trace reconstruction from
@ethdebug/programs-react. Compiler: bugc, BUG's compiler,
from ethdebug/format main; the local variables come from it. Only the
decoding of values is a stand-in, for the format's planned
interpretation layer.
Arcade in BUG (arcade.bug),
statement for statement as in Arcade.sol where BUG allows. It differs
in three ways: BUG has one entry point, so join and
setMotd are dispatched by hand; BUG has no
require, so join and play check
nothing; and the reset is written inline. The roll is the same as in
Solidity. Look for: playerList (the three players),
motd (a 50-byte string), totalScore going from
140 to 170; at -O0, the calls to _rolledHit and
_applyCombo(points: 10, combo: 3) in the call stack; at
-O2, the inlined _applyCombo, with its local mult
going from 5 to 3, then gained (30). The tab has the same
program at two optimization levels.
At both levels, bugc gives a local variable a pointer
where its value is in memory. Where a local has no location (its value
is not stored yet, or the optimizer folded it), bugc lists it by type
only: the format cannot yet say "the value is this expression"
(ethdebug/format#291). At -O2, bugc inlines _rolledHit and
_applyCombo. The inlined instructions carry
transform: ["inline"] and keep the source range of the
call. The locals of an inlined function are marked "inline". They are
listed from the step after the inlined call starts to the step of the
instruction that returns from it; the next step lists the caller's
variables again. In the inlined body, the storage variables stay
listed. The pointer for players, a mapping, names only its
base slot, so the page shows no entries. bugc's pointer for a string
names only its slot; the page reads the text by Solidity's string rule
(part of the decoding stand-in).
No ethdebug on this tab. Compiler: stock solc 0.8.37 (the official
build), with --via-ir --optimize, on the same
Arcade. The tab has what tools had before ethdebug: the
bytecode, its source map and the AST, from solc
--combined-json. A source map gives each instruction one entry,
s:l:f:j: the start and length of a source range, a file
index (-1: no source), and a jump type: i into a function,
o out of one, - neither. The stepper on this
page highlights each step's range and shows its entry. It builds the
call stack as older debuggers did, from the i and
o jumps, and names a frame by the AST function that holds
the jump's target.
The transaction: 484 steps; 325 map to the whole contract, 95 to no
source and 64 to a smaller range. solc inlined _rolledHit,
_applyCombo and _resetCombo into the functions
that call them. Look for: the step just before the roll (line 40),
which pushes the 3 of % 3 and shows
players on line 26; and the call stack while
_rolledHit and _applyCombo run.
"over" and "out" use the call stack depth. "over" steps over a call or an inlined body. "out" runs until the call stack is one frame shorter.
Shiki has no BUG grammar, so the code is coloured as Rust.
The page computes each
step from two fields that soldb reports: the source span and the EVM
call depth. "into" goes to the next new span. "over" goes to the next
new span at the same or a lower depth. "out" goes to the first span
after the depth drops. The EVM depth does not count internal calls.
Every step in these transactions is at depth 1, so "over" and "out"
are off here. soldb's own step over and out
(soldb-debugger) follow internal calls. Its WebAssembly
build does not expose them yet.
What the page changes (file layout only):
program: Fe's file holds two programs in a Fe-specificprogramslist. The page passes the runtime one (environment: "call").sources: Fe lists its sources without their text. The page supplies the text:arcade.fe, and 11 standard-library files (builtin-core:/…,builtin-std:/…) copied from the Fe repository at tag v26.4.1. Their blake3 hashes match the ones in Fe's file.metadata: Fe's whole file, unchanged. soldb reads the source ids and paths from itscompilation. soldb needs no types or pointers, so the page adds none.nameis the program's contract name.
Display only: Shiki has no Fe grammar, so the code is coloured as Rust. soldb reports byte offsets. The page converts them to string positions to draw the span, because one std file has non-ASCII text. File names show without their directory. soldb reports no function names for Fe, because its function detection targets Solidity. It reports no variables, because Fe emits none.
Details: timings, offline replay, requests
Each engine runs in a Web Worker, so the page stays responsive. The workers measure the times below.
Each tab, and each check below, loads its files when it first opens. Once the Solidity tab is ready, the page loads the other tabs, one at a time, when the browser is idle, and fetches the replay build. It does not on a connection that asks to save data or is 2G.
Solidity: lean build, saved trace
Running...
Fe: lean build, node responses
Loads with the Fe tab.
BUG: ethdebug's reference implementation
The BUG tab's engine (ref-worker.js) is
ethdebug/format's packages, bundled in
vendor/ethdebug-ref.js (make-ref-vendor.sh).
When it loads, it maps each step and builds its call stack. It reads
variables and call arguments when the page shows a step.
Loads with the BUG tab.
The old way: a source map stepper
The old way's engine (srcmap-worker.js, about 150 lines)
reads solc's bytecode, source map and AST, and the node's trace. It
reads no ethdebug.
Loads with the tab "The old way".
BUG: soldb check, lean build, node responses
A saved transaction of Tally
(tally.bug, bugc at -O2). soldb gets it as
on the Fe tab (Trace.fromTransaction, then
attachEthdebug). Two changes: bugc writes no resources
file, so the page gives metadata as
{compilation: {sources: [{id, path}]}}. bugc's source id is
the string "tally.bug", and soldb reads numeric ids only,
so the second row gives the id as 0.
Runs when this section opens.
Replay build: re-run a transaction offline
The replay module (1,092 KB) contains an EVM. It re-runs a
Token.transfer transaction from a 6.5 KB replay file. It
contacts no node: soldb requests only static files from this
server.
Runs when this section opens.
Requests from this page
From the browser's resource timing list. Each entry must be a static file on this server.