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
134 changes: 134 additions & 0 deletions packages/evm/src/executor.test.ts
Original file line number Diff line number Diff line change
@@ -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 <runtime> 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);
});
});
});
32 changes: 17 additions & 15 deletions packages/evm/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};

Expand Down
4 changes: 2 additions & 2 deletions packages/evm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading
Loading