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
7 changes: 2 additions & 5 deletions packages/bugc/src/evmgen/call-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { describe, it, expect } from "vitest";
import { compile } from "#compiler";
import type * as Format from "@ethdebug/format";
import { Pointer, Program } from "@ethdebug/format";
import { createMachineState } from "@ethdebug/evm";
import { dereference } from "@ethdebug/pointers";
import { traceLocals } from "#test/evm/locals";

Expand Down Expand Up @@ -118,7 +117,7 @@ code {
);

it("should give the caller JUMP's arguments their values", async () => {
const { executor, steps, instructionAt } = await traceLocals(source);
const { steps, stateAt, instructionAt } = await traceLocals(source);

const index = steps.findIndex((step) => {
const instruction = instructionAt(step);
Expand All @@ -133,9 +132,7 @@ code {
// the state after it, at the callee's entry JUMPDEST
const { invoke } = instructionAt(steps[index])!
.context as Format.Program.Context.Invoke;
const state = createMachineState(executor, {
traceStep: steps[index + 1],
});
const state = stateAt(index + 1);
const cursor = await dereference(
(invoke as InternalCall).arguments!.pointer,
{ state },
Expand Down
7 changes: 2 additions & 5 deletions packages/bugc/src/evmgen/call-timing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* `return` data pointer must read the returned value.
*/
import { describe, it, expect } from "vitest";
import { createMachineState } from "@ethdebug/evm";
import { dereference } from "@ethdebug/pointers";
import { Program } from "@ethdebug/format";
import type * as Format from "@ethdebug/format";
Expand Down Expand Up @@ -115,7 +114,7 @@ describe("call frame timing", () => {
("voids" in testCase ? testCase.voids : undefined) ?? [];
for (const level of [0, 1, 2, 3] as Level[]) {
it(`${name} at O${level}`, async () => {
const { program, executor, steps, instructionAt } = await traceLocals(
const { program, steps, stateAt, instructionAt } = await traceLocals(
source,
level,
);
Expand Down Expand Up @@ -155,9 +154,7 @@ describe("call frame timing", () => {
const ret = (previous!.context as Format.Program.Context.Return)
.return;
if (ret.data) {
const state = createMachineState(executor, {
traceStep: steps[i],
});
const state = stateAt(i);
const cursor = await dereference(ret.data.pointer, { state });
const view = await cursor.view(state);
read.push((await view.read(view.regions[0])).asUint());
Expand Down
9 changes: 4 additions & 5 deletions packages/bugc/src/evmgen/storage-array-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
import { describe, it, expect } from "vitest";
import { keccak256 } from "ethereum-cryptography/keccak";
import { bytesToHex } from "ethereum-cryptography/utils";
import { createMachineState } from "@ethdebug/evm";
import { dereference } from "@ethdebug/pointers";
import type * as Format from "@ethdebug/format";

import { executeProgram } from "#test/evm/behavioral";
import { createMachineState } from "@ethdebug/evm";
import { traceLocals } from "#test/evm/locals";

const levels = [0, 1, 2, 3] as const;
Expand Down Expand Up @@ -155,12 +155,11 @@ describe("storage array pointers", () => {
for (const [name, { source, pointers }] of Object.entries(cases)) {
if (!pointers) continue;
it(name, async () => {
const { program, executor, steps } = await traceLocals(
const { program, executor } = await traceLocals(
`name Layout;\n${source}`,
);
const state = createMachineState(executor, {
traceStep: steps[steps.length - 1],
});
// the storage after the transaction
const state = createMachineState(await executor.currentState());
for (const [identifier, regions] of Object.entries(pointers)) {
const variable = (
program.context as Format.Program.Context.Variables
Expand Down
9 changes: 4 additions & 5 deletions packages/bugc/src/evmgen/storage-array-push.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import { describe, it, expect } from "vitest";
import { keccak256 } from "ethereum-cryptography/keccak";
import { bytesToHex } from "ethereum-cryptography/utils";
import { createMachineState } from "@ethdebug/evm";
import { dereference } from "@ethdebug/pointers";
import type * as Format from "@ethdebug/format";

import { compile } from "#compiler";
import { executeProgram } from "#test/evm/behavioral";
import { createMachineState } from "@ethdebug/evm";
import { traceLocals } from "#test/evm/locals";

const levels = [0, 1, 2, 3] as const;
Expand Down Expand Up @@ -129,13 +129,12 @@ describe("push on a storage array", () => {
}

it("gives the pushed elements to the array's pointer", async () => {
const { program, executor, steps } = await traceLocals(
const { program, executor } = await traceLocals(
`name Push;\nstorage { [0] h: array<uint16>; }
code { h.push(5); h.push(7); h.push(9); }`,
);
const state = createMachineState(executor, {
traceStep: steps[steps.length - 1],
});
// the storage after the transaction
const state = createMachineState(await executor.currentState());
const variable = (
program.context as Format.Program.Context.Variables
).variables.find((v) => v.identifier === "h")!;
Expand Down
6 changes: 2 additions & 4 deletions packages/bugc/src/irgen/debug/variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,7 @@ describe("storage string and bytes pointers", () => {
it(`read ${length} bytes (level ${level})`, async () => {
const program = source(length);
const run = await traceLocals(program, level);
const state = createMachineState(run.executor, {
traceStep: run.steps[run.steps.length - 1],
});
const state = run.stateAt(run.steps.length - 1);
const pointerOf = (identifier: string) =>
(
run.program.context as Format.Program.Context.Variables
Expand Down Expand Up @@ -190,7 +188,7 @@ code {
const run = await executor.execute({ data });
expect(run.success).toBe(true);
}
const state = createMachineState(executor);
const state = createMachineState(await executor.currentState());
const pointerOf = (identifier: string) =>
(
bytecode.runtimeProgram.context as Format.Program.Context.Variables
Expand Down
2 changes: 1 addition & 1 deletion packages/bugc/src/irgen/generate/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe("packed storage struct field pointers", () => {
: undefined;
expect(variable?.pointer).toBeDefined();

const state = createMachineState(executor);
const state = createMachineState(await executor.currentState());
const cursor = await dereference(variable!.pointer!, { state });
const view = await cursor.view(state);
const read = async (name: string) =>
Expand Down
60 changes: 27 additions & 33 deletions packages/bugc/test/evm/locals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
*/
import { expect } from "vitest";
import { compile } from "#compiler";
import { Executor, createMachineState, type TraceStep } from "@ethdebug/evm";
import { Data, dereference, type Machine } from "@ethdebug/pointers";
import {
Executor,
createMachineState,
createTrace,
type Trace,
type TraceStep,
} from "@ethdebug/evm";
import { dereference, type Machine } from "@ethdebug/pointers";
import { bytesToHex } from "ethereum-cryptography/utils";
import type * as Format from "@ethdebug/format";

Expand All @@ -16,9 +22,10 @@ export type Level = 0 | 1 | 2 | 3;
export interface LocalsTrace {
program: Format.Program;
executor: Executor;
steps: TraceStep[];
/** The transaction's calldata */
calldata: Uint8Array;
trace: Trace;
steps: readonly TraceStep[];
/** The machine state at a step (before its instruction runs) */
stateAt(index: number): Machine.State;
/** The program's instruction at a step's pc */
instructionAt(step: TraceStep): Format.Program.Instruction | undefined;
}
Expand All @@ -45,8 +52,8 @@ export async function traceLocals(
: bytecode.runtime,
),
);
const steps: TraceStep[] = [];
await executor.execute({ data: calldata }, (step) => steps.push(step));
const trace = createTrace();
await executor.execute({ data: calldata }, trace);

const program = bytecode.runtimeProgram;
const byOffset = new Map(
Expand All @@ -55,8 +62,9 @@ export async function traceLocals(
return {
program,
executor,
steps,
calldata: Buffer.from(calldata, "hex"),
trace,
steps: trace.steps,
stateAt: (index) => createMachineState(trace.stateAt(index)),
instructionAt: (step) => byOffset.get(step.pc),
};
}
Expand Down Expand Up @@ -214,8 +222,8 @@ export async function check(
const local = locals[key!];

const read = local.shape
? await readReference(pointer, name, local.shape, run, steps[k + 1])
: await readScalar(pointer, name, run, steps[k + 1]);
? await readReference(pointer, name, local.shape, run, k + 1)
: await readScalar(pointer, name, run, k + 1);

// Never an earlier value than the last one read, nor than the
// latest one whose statement has finished (that would be stale);
Expand Down Expand Up @@ -293,25 +301,11 @@ function stringify(value: unknown): string {
* Dereference a pointer at a step and read its regions in order,
* checking each against the step's memory or the calldata.
*/
async function regionsAt(
pointer: unknown,
{ executor, calldata }: LocalsTrace,
step: TraceStep,
) {
const state: Machine.State = {
...createMachineState(executor, { traceStep: step }),
calldata: {
length: Promise.resolve(BigInt(calldata.length)),
read: async ({ slice: { offset, length } }) => {
const data = new Uint8Array(Number(length));
data.set(calldata.slice(Number(offset), Number(offset + length)));
return Data.fromBytes(data);
},
},
};
async function regionsAt(pointer: unknown, run: LocalsTrace, index: number) {
const state = run.stateAt(index);
const { memory, calldata } = run.trace.stateAt(index);
const cursor = await dereference(pointer as Format.Pointer, { state });
const view = await cursor.view(state);
const memory = step.memory ?? new Uint8Array();
const regions = [...view.regions];
let next = 0;
return {
Expand Down Expand Up @@ -341,7 +335,7 @@ async function regionsAt(
}
const actual = new Uint8Array(Number(length));
actual.set(bytes.slice(Number(start), Number(start + length)));
if (data.toHex() !== "0x" + Buffer.from(actual).toString("hex")) {
if (data.toHex() !== "0x" + bytesToHex(actual)) {
throw new Error(
`${name} does not read ${region.location} at ${start}`,
);
Expand All @@ -357,9 +351,9 @@ async function readScalar(
pointer: unknown,
name: string,
run: LocalsTrace,
step: TraceStep,
index: number,
): Promise<string> {
const regions = await regionsAt(pointer, run, step);
const regions = await regionsAt(pointer, run, index);
regions.seek(name);
return (await regions.read(name)).toHex();
}
Expand All @@ -377,9 +371,9 @@ async function readReference(
name: string,
shape: Shape,
run: LocalsTrace,
step: TraceStep,
index: number,
): Promise<RefValue> {
const regions = await regionsAt(pointer, run, step);
const regions = await regionsAt(pointer, run, index);
regions.seek(name);

if (shape.kind === "calldata") {
Expand Down
4 changes: 2 additions & 2 deletions packages/bugc/test/examples/runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async function checkScalarValue(
}

// Dereference the pointer
const state = createMachineState(executor);
const state = createMachineState(await executor.currentState());
const cursor = await dereference(pointer, { state });
const view = await cursor.view(state);

Expand Down Expand Up @@ -234,7 +234,7 @@ async function checkRegionValues(
}

// Dereference the pointer
const state = createMachineState(executor);
const state = createMachineState(await executor.currentState());
const cursor = await dereference(pointer, { state });
const view = await cursor.view(state);

Expand Down
53 changes: 53 additions & 0 deletions packages/evm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,58 @@ specification itself are tracked in the root

## Unreleased

### Added

- Transactions from any account: `deploy({ from, create, value })`
returns the new contract's address, `call({ from, to, input, value })`
calls any address, and `fund(address, balance)` sets an account's
balance. `getStorage`, `setStorage` and `getCode` take an optional
address ([#375]).
- A `block` option for each call (number, timestamp, prevrandao,
coinbase, base fee, blob base fee, gas limit), and a `chainId`
executor option, so contracts read these values. `BLOCKHASH` of one of
the 256 previous blocks is keccak256 of its number ([#375]).
- `createTrace({ memory })` records one transaction: pass it to `call`,
`deploy` or `execute`. `trace.stateAt(index)` gives the complete state
at a step (stack, memory, storage, transient storage, calldata, return
data, code), with storage written in frames that later reverted undone
from the revert on. `trace.frames` lists the message frames. The
`memory` option records a copy at every step (`"full"`, the default),
a new copy only when memory changed (`"changed"`), or none (`"none"`,
and then `stateAt` gives no memory) ([#375]).
- Message frame events: a `frame` handler gets an `enter` and an `exit`
event for each call or create, with its depth, addresses, caller,
calldata, value, return data, whether it reverted, and its first and
last step indexes ([#375]).
- A trace's step state carries its position (`at`), so
`createMachineState(trace.stateAt(i))` has the step's pc, opcode and
index ([#375]).
- `executor.currentState()` gives the executor's state now, as a step
state ([#375]).
- `endTransaction()` clears transient storage, the warm address and
slot sets and the slots' original values. `deploy(options)` and `call`
end their transaction; `execute` does not ([#375]).

### Changed

- **Breaking:** a `TraceStep` is the event only: `pc`, `opcode`, `op`
(the opcode byte), `gasRemaining`, `gasCost`, `depth`, `address` and
`codeAddress`. Its stack and memory moved to `Trace.stateAt` ([#375]).
- **Breaking:** `createMachineState(state, { pc, opcode, traceIndex })`
takes a complete step state (from `Trace.stateAt` or
`Executor.currentState`) instead of an executor, and never reads the
executor's current state for a past step ([#375]).
- **Breaking:** `createTraceCollector` is removed; use `createTrace`
([#375]).
- A step handler gets the step's index as its second argument ([#375]).
- The package no longer uses Node's `Buffer`, so it runs in browsers
without a polyfill ([#375]).

### Fixed

- A run that throws (as BASEFEE does without a block) no longer leaves
the executor's state checkpoints open ([#375]).

## 0.1.0-preview.2 — 2026-10-07

Updated `@ethdebug/pointers` to `0.1.0-preview.2`.
Expand Down Expand Up @@ -40,3 +92,4 @@ First publication.

[#300]: https://github.com/ethdebug/format/pull/300
[#314]: https://github.com/ethdebug/format/pull/314
[#375]: https://github.com/ethdebug/format/pull/375
18 changes: 18 additions & 0 deletions packages/evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,21 @@ the [documentation](https://ethdebug.github.io/format/).
This package uses `package.json` `imports` for internal modules.
Consumers must use `moduleResolution` `node16`, `nodenext`, or
`bundler`. The legacy `node10` resolution is not supported.

## Usage

```typescript
import { Executor, createTrace, createMachineState } from "@ethdebug/evm";

const executor = new Executor({ chainId: 1n });
await executor.fund(alice, 10n ** 18n);
const { address } = await executor.deploy({ from: alice, create });

// Record one transaction, then read the state at any of its steps
const trace = createTrace({ memory: "changed" });
await executor.call(
{ from: alice, to: address!, input, block: { number: 1n } },
trace,
);
const state = createMachineState(trace.stateAt(i));
```
Loading
Loading