diff --git a/packages/evm/src/executor.test.ts b/packages/evm/src/executor.test.ts new file mode 100644 index 000000000..48fbfc131 --- /dev/null +++ b/packages/evm/src/executor.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { bytesToHex } from "ethereum-cryptography/utils"; +import { Executor } from "#executor"; + +// Simple bytecodes for testing: +// +// storeValue: PUSH1 0x2a PUSH1 0x00 SSTORE STOP +// Stores 42 at slot 0. +const storeValueCode = "602a60005500"; + +// returnValue: PUSH1 0x2a PUSH1 0x00 MSTORE +// PUSH1 0x20 PUSH1 0x00 RETURN +// Returns 42 as a 32-byte word. +const returnValueCode = "602a60005260206000f3"; + +// Simple CREATE constructor that deploys storeValueCode: +// PUSH6 PUSH1 0x00 MSTORE +// PUSH1 0x06 PUSH1 0x1a RETURN +// We build it by hand: deploy code that copies runtime +// to memory then returns it. +// +// Runtime: 602a60005500 (6 bytes) +// Constructor: +// PUSH6 602a60005500 => 65602a60005500 +// PUSH1 00 => 6000 +// MSTORE => 52 +// PUSH1 06 => 6006 +// PUSH1 1a => 601a +// RETURN => f3 +const constructorCode = "65602a600055006000526006601af3"; + +describe("Executor", () => { + let executor: Executor; + + beforeEach(() => { + executor = new Executor(); + }); + + describe("deploy", () => { + it("deploys bytecode via CREATE", async () => { + await executor.deploy(constructorCode); + const code = await executor.getCode(); + expect(bytesToHex(code)).toBe(storeValueCode); + }); + + it("throws on failed deployment", async () => { + // FE = INVALID opcode + await expect(executor.deploy("fe")).rejects.toThrow("Deployment failed"); + }); + }); + + describe("execute", () => { + it("calls deployed contract", async () => { + await executor.deploy(constructorCode); + const result = await executor.execute(); + expect(result.success).toBe(true); + expect(result.gasUsed).toBeGreaterThan(0n); + }); + + it("reads storage after execution", async () => { + await executor.deploy(constructorCode); + await executor.execute(); + const value = await executor.getStorage(0n); + expect(value).toBe(42n); + }); + }); + + describe("executeCode", () => { + it("runs bytecode directly", async () => { + const result = await executor.executeCode(returnValueCode); + expect(result.success).toBe(true); + expect(result.returnValue.length).toBe(32); + + const value = BigInt("0x" + bytesToHex(result.returnValue)); + expect(value).toBe(42n); + }); + }); + + describe("storage", () => { + it("reads and writes storage", async () => { + await executor.deploy(constructorCode); + await executor.setStorage(5n, 123n); + const value = await executor.getStorage(5n); + expect(value).toBe(123n); + }); + + it("handles large slot values", async () => { + await executor.deploy(constructorCode); + const largeSlot = 2n ** 128n + 7n; + await executor.setStorage(largeSlot, 999n); + const value = await executor.getStorage(largeSlot); + expect(value).toBe(999n); + }); + + it("returns 0 for unset slots", async () => { + await executor.deploy(constructorCode); + const value = await executor.getStorage(99n); + expect(value).toBe(0n); + }); + }); + + describe("reset", () => { + it("clears all state", async () => { + await executor.deploy(constructorCode); + await executor.execute(); + expect(await executor.getStorage(0n)).toBe(42n); + + await executor.reset(); + // After reset, deploy again to have a valid address + await executor.deploy(constructorCode); + expect(await executor.getStorage(0n)).toBe(0n); + }); + }); + + describe("addresses", () => { + it("provides deployer address", () => { + const addr = executor.getDeployerAddress(); + expect(addr).toBeDefined(); + }); + + it("provides contract address", () => { + const addr = executor.getContractAddress(); + expect(addr).toBeDefined(); + }); + + it("updates contract address after deploy", async () => { + const before = executor.getContractAddress(); + await executor.deploy(constructorCode); + const after = executor.getContractAddress(); + // CREATE computes a new address + expect(after).not.toEqual(before); + }); + }); +}); diff --git a/packages/evm/src/executor.ts b/packages/evm/src/executor.ts index eb1a0129f..6a85073cf 100644 --- a/packages/evm/src/executor.ts +++ b/packages/evm/src/executor.ts @@ -6,6 +6,7 @@ */ import { EVM } from "@ethereumjs/evm"; +import type { InterpreterStep } from "@ethereumjs/evm"; import { SimpleStateManager } from "@ethereumjs/statemanager"; import { Common, Mainnet } from "@ethereumjs/common"; import { Address, Account } from "@ethereumjs/util"; @@ -165,24 +166,25 @@ export class Executor { gasLimit: options.gasLimit ?? 10_000_000n, }; + let listener: ((step: InterpreterStep) => void) | undefined; if (traceHandler) { - this.evm.events.on( - "step", - (step: { pc: number; opcode: { name: string }; stack: bigint[] }) => { - const traceStep: TraceStep = { - pc: step.pc, - opcode: step.opcode.name, - stack: [...step.stack], - }; - traceHandler(traceStep); - }, - ); + listener = (step: InterpreterStep) => { + const traceStep: TraceStep = { + pc: step.pc, + opcode: step.opcode.name, + stack: [...step.stack], + memory: new Uint8Array(step.memory), + gasRemaining: step.gasLeft, + }; + traceHandler(traceStep); + }; + this.evm.events.on("step", listener); } const result = await this.evm.runCall(runCallOpts); - if (traceHandler) { - this.evm.events.removeAllListeners("step"); + if (listener) { + this.evm.events.removeListener("step", listener); } const rawResult = result as ResultWithExec; @@ -221,8 +223,8 @@ export class Executor { data: options.data ? hexToBytes(options.data) : new Uint8Array(), gasLimit: options.gasLimit ?? 10_000_000n, value: options.value ?? 0n, - origin: options.origin ?? new Address(Buffer.alloc(20)), - caller: options.caller ?? new Address(Buffer.alloc(20)), + origin: options.origin ?? new Address(hexToBytes("00".repeat(20))), + caller: options.caller ?? new Address(hexToBytes("00".repeat(20))), address: tempAddress, }; diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts index 1784e5521..b64b44e83 100644 --- a/packages/evm/src/index.ts +++ b/packages/evm/src/index.ts @@ -32,6 +32,6 @@ export type { ExecutionOptions, ExecutionResult } from "#executor"; export { createMachineState } from "#machine"; export type { MachineStateOptions } from "#machine"; -// Trace types -export { createTraceCollector } from "#trace"; +// Trace types and Machine +export { createTraceCollector, createMachine } from "#trace"; export type { TraceStep, TraceHandler, Trace } from "#trace"; diff --git a/packages/evm/src/machine.test.ts b/packages/evm/src/machine.test.ts new file mode 100644 index 000000000..7230f5583 --- /dev/null +++ b/packages/evm/src/machine.test.ts @@ -0,0 +1,203 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { Data } from "@ethdebug/pointers"; +import { Executor } from "#executor"; +import { createMachineState } from "#machine"; + +// Constructor that deploys: PUSH1 0x2a PUSH1 0x00 SSTORE STOP +const constructorCode = "65602a600055006000526006601af3"; + +describe("createMachineState", () => { + let executor: Executor; + + beforeEach(async () => { + executor = new Executor(); + await executor.deploy(constructorCode); + await executor.execute(); + }); + + describe("end-state (no traceStep)", () => { + it("reads storage", async () => { + const state = createMachineState(executor); + const val = await state.storage.read({ + slot: Data.fromUint(0n), + }); + expect(val.asUint()).toBe(42n); + }); + + it("reads storage with slice", async () => { + const state = createMachineState(executor); + const val = await state.storage.read({ + slot: Data.fromUint(0n), + slice: { offset: 31n, length: 1n }, + }); + // 42 = 0x2a, in a 32-byte big-endian word the + // last byte is at offset 31 + expect(val.asUint()).toBe(42n); + }); + + it("returns zero for stack", async () => { + const state = createMachineState(executor); + expect(await state.stack.length).toBe(0n); + const val = await state.stack.peek({ depth: 0n }); + expect(val.asUint()).toBe(0n); + }); + + it("returns zero for memory", async () => { + const state = createMachineState(executor); + expect(await state.memory.length).toBe(0n); + const val = await state.memory.read({ + slice: { offset: 0n, length: 32n }, + }); + expect(val.asUint()).toBe(0n); + }); + + it("uses default context values", async () => { + const state = createMachineState(executor); + expect(await state.programCounter).toBe(0n); + expect(await state.opcode).toBe("STOP"); + expect(await state.traceIndex).toBe(0n); + }); + + it("accepts context overrides", async () => { + const state = createMachineState(executor, { + programCounter: 10n, + opcode: "SLOAD", + traceIndex: 5n, + }); + expect(await state.programCounter).toBe(10n); + expect(await state.opcode).toBe("SLOAD"); + expect(await state.traceIndex).toBe(5n); + }); + + it("reads code length", async () => { + const state = createMachineState(executor); + const len = await state.code.length; + // Deployed runtime is 6 bytes (storeValueCode) + expect(len).toBe(6n); + }); + + it("reads code bytes", async () => { + const state = createMachineState(executor); + const data = await state.code.read({ + slice: { offset: 0n, length: 1n }, + }); + // First byte of "602a60005500" is 0x60 (PUSH1) + expect(data.asUint()).toBe(0x60n); + }); + }); + + describe("with traceStep", () => { + it("reads stack from trace step", async () => { + const state = createMachineState(executor, { + traceStep: { + pc: 0, + opcode: "SSTORE", + stack: [100n, 200n, 300n], + }, + }); + + expect(await state.stack.length).toBe(3n); + + // depth 0 = top of stack = last element + const top = await state.stack.peek({ depth: 0n }); + expect(top.asUint()).toBe(300n); + + // depth 1 = second from top + const second = await state.stack.peek({ + depth: 1n, + }); + expect(second.asUint()).toBe(200n); + + // depth 2 = bottom + const bottom = await state.stack.peek({ + depth: 2n, + }); + expect(bottom.asUint()).toBe(100n); + }); + + it("returns zero for out-of-bounds depth", async () => { + const state = createMachineState(executor, { + traceStep: { + pc: 0, + opcode: "PUSH1", + stack: [42n], + }, + }); + + const val = await state.stack.peek({ depth: 5n }); + expect(val.asUint()).toBe(0n); + }); + + it("supports stack peek with slice", async () => { + const state = createMachineState(executor, { + traceStep: { + pc: 0, + opcode: "PUSH1", + stack: [0xdeadbeefn], + }, + }); + + // 0xdeadbeef padded to 32 bytes, last 4 bytes + const val = await state.stack.peek({ + depth: 0n, + slice: { offset: 28n, length: 4n }, + }); + expect(val.asUint()).toBe(0xdeadbeefn); + }); + + it("reads memory from trace step", async () => { + const mem = new Uint8Array(64); + mem[31] = 0xff; + mem[63] = 0xab; + + const state = createMachineState(executor, { + traceStep: { + pc: 0, + opcode: "MLOAD", + stack: [], + memory: mem, + }, + }); + + expect(await state.memory.length).toBe(64n); + + const word1 = await state.memory.read({ + slice: { offset: 0n, length: 32n }, + }); + expect(word1.asUint()).toBe(0xffn); + + const word2 = await state.memory.read({ + slice: { offset: 32n, length: 32n }, + }); + expect(word2.asUint()).toBe(0xabn); + }); + + it("derives pc/opcode from trace step", async () => { + const state = createMachineState(executor, { + traceStep: { + pc: 42, + opcode: "JUMPDEST", + stack: [], + }, + }); + + expect(await state.programCounter).toBe(42n); + expect(await state.opcode).toBe("JUMPDEST"); + }); + + it("allows overriding trace step context", async () => { + const state = createMachineState(executor, { + traceStep: { + pc: 42, + opcode: "JUMPDEST", + stack: [], + }, + programCounter: 99n, + opcode: "STOP", + }); + + expect(await state.programCounter).toBe(99n); + expect(await state.opcode).toBe("STOP"); + }); + }); +}); diff --git a/packages/evm/src/machine.ts b/packages/evm/src/machine.ts index b96aa99ad..6ffcd7055 100644 --- a/packages/evm/src/machine.ts +++ b/packages/evm/src/machine.ts @@ -8,14 +8,17 @@ import type { Machine } from "@ethdebug/pointers"; import { Data } from "@ethdebug/pointers"; import type { Executor } from "#executor"; +import type { TraceStep } from "#trace"; /** * Options for creating a Machine.State adapter. */ export interface MachineStateOptions { - /** Program counter for the current state */ + /** A captured trace step to read stack/memory from */ + traceStep?: TraceStep; + /** Program counter (overrides traceStep.pc if set) */ programCounter?: bigint; - /** Opcode at the current program counter */ + /** Opcode (overrides traceStep.opcode if set) */ opcode?: string; /** Trace index (step number) */ traceIndex?: bigint; @@ -24,42 +27,72 @@ export interface MachineStateOptions { /** * Create a Machine.State from an Executor. * - * This adapter allows using @ethdebug/pointers dereference() - * to evaluate pointers against an EVM executor's storage state. + * When a traceStep is provided, stack and memory reads + * use the captured step data. Without a traceStep, only + * storage is functional (end-state adapter). * - * Note: This creates an "end-state" adapter where only storage - * is fully implemented. Stack, memory, etc. return empty/zero - * values since we only have post-execution state access. - * - * @param executor - The EVM executor to read state from - * @param options - Optional state context (PC, opcode, trace index) - * @returns A Machine.State suitable for pointer dereferencing + * @param executor - EVM executor to read storage/code from + * @param options - Trace step and context overrides */ export function createMachineState( executor: Executor, options: MachineStateOptions = {}, ): Machine.State { - const { programCounter = 0n, opcode = "STOP", traceIndex = 0n } = options; + const { traceStep, traceIndex = 0n } = options; + + const programCounter = + options.programCounter ?? (traceStep ? BigInt(traceStep.pc) : 0n); + const opcode = options.opcode ?? (traceStep ? traceStep.opcode : "STOP"); return { - // Trace context traceIndex: Promise.resolve(traceIndex), programCounter: Promise.resolve(programCounter), opcode: Promise.resolve(opcode), - // Stack - not available in end-state stack: { - length: Promise.resolve(0n), - peek: async (): Promise => Data.zero(), + length: Promise.resolve(traceStep ? BigInt(traceStep.stack.length) : 0n), + async peek({ depth, slice }): Promise { + if (!traceStep) { + return Data.zero(); + } + + const { stack } = traceStep; + const index = stack.length - 1 - Number(depth); + if (index < 0 || index >= stack.length) { + return Data.zero(); + } + + const data = Data.fromUint(stack[index]).padUntilAtLeast(32); + + if (slice) { + const sliced = new Uint8Array(data).slice( + Number(slice.offset), + Number(slice.offset + slice.length), + ); + return Data.fromBytes(sliced); + } + + return data; + }, }, - // Memory - not available in end-state memory: { - length: Promise.resolve(0n), - read: async (): Promise => Data.zero(), + length: Promise.resolve( + traceStep?.memory ? BigInt(traceStep.memory.length) : 0n, + ), + async read({ slice }): Promise { + if (!traceStep?.memory) { + return Data.zero(); + } + + const sliced = traceStep.memory.slice( + Number(slice.offset), + Number(slice.offset + slice.length), + ); + return Data.fromBytes(sliced); + }, }, - // Storage - fully implemented via executor storage: { async read({ slot, slice }): Promise { const slotValue = slot.asUint(); @@ -79,25 +112,31 @@ export function createMachineState( }, }, - // Calldata - not available in end-state calldata: { length: Promise.resolve(0n), read: async (): Promise => Data.zero(), }, - // Returndata - not available in end-state returndata: { length: Promise.resolve(0n), read: async (): Promise => Data.zero(), }, - // Code - not available in end-state code: { - length: Promise.resolve(0n), - read: async (): Promise => Data.zero(), + length: (async () => { + const code = await executor.getCode(); + return BigInt(code.length); + })(), + async read({ slice }): Promise { + const code = await executor.getCode(); + const sliced = code.slice( + Number(slice.offset), + Number(slice.offset + slice.length), + ); + return Data.fromBytes(sliced); + }, }, - // Transient storage - not available in end-state transient: { read: async (): Promise => Data.zero(), }, diff --git a/packages/evm/src/trace.test.ts b/packages/evm/src/trace.test.ts new file mode 100644 index 000000000..4d7144de5 --- /dev/null +++ b/packages/evm/src/trace.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { Executor } from "#executor"; +import { createTraceCollector, createMachine } from "#trace"; + +// Constructor that deploys: PUSH1 0x2a PUSH1 0x00 SSTORE STOP +const constructorCode = "65602a600055006000526006601af3"; + +describe("createTraceCollector", () => { + it("collects trace steps during execution", async () => { + const executor = new Executor(); + await executor.deploy(constructorCode); + + const [handler, getTrace] = createTraceCollector(); + await executor.execute({}, handler); + const trace = getTrace(); + + expect(trace.steps.length).toBeGreaterThan(0); + + // First step should be at PC 0 + expect(trace.steps[0].pc).toBe(0); + expect(trace.steps[0].opcode).toBeDefined(); + expect(trace.steps[0].stack).toBeInstanceOf(Array); + }); + + it("captures memory when available", async () => { + const executor = new Executor(); + // Use bytecode that writes to memory: + // PUSH1 0x42 PUSH1 0x00 MSTORE STOP + // = 60 42 60 00 52 00 + const memCode = "604260005200"; + const result = await executor.executeCode(memCode); + expect(result.success).toBe(true); + + // Now deploy something and trace with memory + // Constructor: PUSH6 PUSH1 0 MSTORE + // PUSH1 6 PUSH1 0x1a RETURN + // where runtime = 604260005200 (MSTORE bytecode) + const memConstructor = "656042600052006000526006601af3"; + await executor.deploy(memConstructor); + + const [handler, getTrace] = createTraceCollector(); + await executor.execute({}, handler); + const trace = getTrace(); + + // The runtime does MSTORE, so memory should grow + const hasMemory = trace.steps.some((s) => s.memory && s.memory.length > 0); + expect(hasMemory).toBe(true); + }); + + it("captures gas remaining", async () => { + const executor = new Executor(); + await executor.deploy(constructorCode); + + const [handler, getTrace] = createTraceCollector(); + await executor.execute({}, handler); + const trace = getTrace(); + + expect(trace.steps[0].gasRemaining).toBeGreaterThan(0n); + }); + + it("returns independent copies", async () => { + const executor = new Executor(); + await executor.deploy(constructorCode); + + const [handler, getTrace] = createTraceCollector(); + await executor.execute({}, handler); + + const trace1 = getTrace(); + const trace2 = getTrace(); + expect(trace1.steps).toEqual(trace2.steps); + expect(trace1.steps).not.toBe(trace2.steps); + }); +}); + +describe("createMachine", () => { + let executor: Executor; + + beforeEach(async () => { + executor = new Executor(); + await executor.deploy(constructorCode); + }); + + it("returns a Machine with trace()", () => { + const machine = createMachine(executor); + expect(machine.trace).toBeDefined(); + }); + + it("yields Machine.State for each step", async () => { + const machine = createMachine(executor); + const states: unknown[] = []; + + for await (const state of machine.trace()) { + states.push(state); + } + + expect(states.length).toBeGreaterThan(0); + }); + + it("provides correct traceIndex", async () => { + const machine = createMachine(executor); + let index = 0n; + + for await (const state of machine.trace()) { + const traceIndex = await state.traceIndex; + expect(traceIndex).toBe(index); + index++; + } + }); + + it("provides program counter and opcode", async () => { + const machine = createMachine(executor); + let first = true; + + for await (const state of machine.trace()) { + if (first) { + const pc = await state.programCounter; + const opcode = await state.opcode; + expect(pc).toBe(0n); + expect(typeof opcode).toBe("string"); + expect(opcode.length).toBeGreaterThan(0); + first = false; + } + } + }); + + it("provides stack data at each step", async () => { + const machine = createMachine(executor); + let foundNonEmpty = false; + + for await (const state of machine.trace()) { + const len = await state.stack.length; + if (len > 0n) { + foundNonEmpty = true; + const top = await state.stack.peek({ + depth: 0n, + }); + expect(top.length).toBeGreaterThan(0); + } + } + + expect(foundNonEmpty).toBe(true); + }); + + it("provides storage data", async () => { + // Execute first to populate storage + await executor.execute(); + + const machine = createMachine(executor); + const { Data } = await import("@ethdebug/pointers"); + + for await (const state of machine.trace()) { + const val = await state.storage.read({ + slot: Data.fromUint(0n), + }); + // After the first execute, slot 0 = 42 + expect(val.asUint()).toBe(42n); + break; // just check first state + } + }); +}); diff --git a/packages/evm/src/trace.ts b/packages/evm/src/trace.ts index 2f9e3e389..13516839e 100644 --- a/packages/evm/src/trace.ts +++ b/packages/evm/src/trace.ts @@ -4,6 +4,11 @@ * Types for capturing and representing EVM execution traces. */ +import type { Machine } from "@ethdebug/pointers"; +import type { Executor } from "#executor"; +import type { ExecutionOptions } from "#executor"; +import { createMachineState } from "#machine"; + /** * A single step in an execution trace. */ @@ -34,10 +39,10 @@ export interface Trace { } /** - * Create a trace handler that collects steps into a Trace object. + * Create a trace handler that collects steps into a + * Trace object. * - * @returns A tuple of [handler, getTrace] where handler collects steps - * and getTrace returns the collected trace. + * @returns [handler, getTrace] tuple */ export function createTraceCollector(): [TraceHandler, () => Trace] { const steps: TraceStep[] = []; @@ -50,3 +55,40 @@ export function createTraceCollector(): [TraceHandler, () => Trace] { return [handler, getTrace]; } + +/** + * Create a Machine that traces execution and yields + * Machine.State for each step. + * + * @param executor - EVM executor to trace + * @param options - Execution options for the call + */ +export function createMachine( + executor: Executor, + options: ExecutionOptions = {}, +): Machine { + return { + trace(): AsyncIterable { + return traceExecution(executor, options); + }, + }; +} + +async function* traceExecution( + executor: Executor, + options: ExecutionOptions, +): AsyncGenerator { + const steps: TraceStep[] = []; + const handler: TraceHandler = (step) => { + steps.push(step); + }; + + await executor.execute(options, handler); + + for (let i = 0; i < steps.length; i++) { + yield createMachineState(executor, { + traceStep: steps[i], + traceIndex: BigInt(i), + }); + } +}