0xjojoex/evm.zig

Fast and composable EVM execution engine in Zig

★ 3Forks 0ZigGitHub ↗Compare

README

evmz

A fast, composable EVM execution engine in Zig. One exact Ethereum specification — dispatch table, gas schedule, precompiles, transaction rules, and block hooks — is a compile-time value, resolved into one concrete VM with no runtime fork selection. The same block state transition function runs natively and as an RV64 guest on SP1, ZisK, and OpenVM.

Status

evmz is pre-release. The version is 0.0.0, the public API may change between commits, and the project has not been audited. main tracks the latest supported execution-spec prereleases. EEST conformance resolves fixture releases through execution-specs itself; release evidence records the exact resolved corpus and index hash.

Implemented:

  • Ethereum execution through the latest supported Glamsterdam prerelease, whose execution-layer fork is Amsterdam
  • Stateless block validation from execution witnesses
  • Native, SP1, ZisK, and OpenVM execution of the stateless validator

Experimental:

  • Block-access-list validation and BAL-driven parallel verification
  • Deeper transaction and block programmability
  • A native evmz C API

Install

zig fetch --save git+https://github.com/0xjojoex/evm.zig

The four core modules are evmz, rlp, mpt, and ssz; import the ones you need.

// build.zig
const evmz = b.dependency("evmz", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("evmz", evmz.module("evmz"));
exe.root_module.addImport("rlp", evmz.module("rlp")); // also: mpt, ssz

rlp, mpt, and ssz are self-contained pure-Zig codecs. To use them without configuring the EVM core — and without fetching any C dependency — pass .core = false to b.dependency. For a zkVM guest, pass .profile = .zkvm.

This follows main; pin a commit for reproducible builds. Each release supports one Zig minor, and every module ships at the package version — see the release policy.

evmz Zig
main 0.16.x

Quick start

const std = @import("std");
const evmz = @import("evmz");

// Use your own StateReader in production.
var memory = evmz.state.MemoryStore.init(allocator);
defer memory.deinit();

var vm = evmz.Evm.init(allocator, .{
    .state = .{ .reader = memory.reader() },
});
defer vm.deinit();

const execution = switch (try vm.transact(.{
    .env = .{ .gas_limit = 100_000 },
    .tx = .{
        .sender = evmz.addr(0xaaaa),
        .to = evmz.addr(0xbbbb),
        .gas_limit = 100_000,
    },
})) {
    .executed => |value| value,
    .rejected => return error.TransactionRejected,
};
defer execution.discardIfCurrent();

const result = execution.result();
std.debug.print("status: {any}, gas: {}\n", .{
    result.status,
    result.gas.used,
});

// Accept the provisional branch. Without this, the deferred discard rolls back.
execution.retain();

retain() accepts the execution into the VM's pending branch. Advanced block-level code can consume vm.executor.acceptedChanges() and persist it. See examples/basic.zig for runnable transaction execution and provisional storage-change inspection.

Tools

zig build cli-build installs zig-out/bin/evmz. Run evmz --help or evmz help <command> to see its commands and options. zig build run -- <args> runs the same binary. The former separate tool executables are replaced by subcommands:

  • evmz t8n accepts alloc/env/transaction inputs and writes transition results.
  • evmz statetest executes a General State Test vector and writes its observed root and optional opcode trace.
  • evmz blocktest imports encoded blocks and writes actual acceptance, committed state, and optional opcode traces.
  • evmz debug inspects controlled execution; add -x for a single command.

Execution layers

evmz exposes the execution stack as separate reusable surfaces:

Surface Responsibility
Interpreter EVM bytecode and call-frame execution
Transaction program Envelope validation, fees, nonce, execution, and settlement
Block program Ordered transaction execution and block-level rules
Stateless validator Witness validation and post-state and receipts roots
Guest The stateless validator compiled for ZisK, SP1, or OpenVM

Exact specifications

Evm is the latest exact Ethereum VM. Bind another specification at compile time:

const LatestEvm = evmz.Evm;
const CancunEvm = evmz.Vm(evmz.eth.cancun);

Extend the exact base you mean and compile it into a concrete VM:

const my_cancun = evmz.eth.cancun.extend(.{
    .transaction = .{
        .max_initcode_size = 0x10000,
    },
    .settlement = .{
        .gas_refund_cap_divisor = 4,
    },
});
const MyEvm = evmz.Vm(my_cancun);

Spec.extend patches parameters and semantic functions. Complete instruction, precompile, transaction, or block bindings can also be replaced. See examples/custom_fork/ and examples/op/.

Stateless validation and zkVM guests

evmz.stateless validates a block from an execution witness and returns its post-state and receipts roots:

const Validator = evmz.stateless.Exact(.amsterdam);
const result = try Validator.validate(allocator, input);

The same validator is compiled natively and as an RV64 ELF under guest/. SP1, ZisK, and OpenVM share one accelerator ABI and persistent host protocol while retaining backend-specific runtimes and metrics.

Run the full test suite through the zkVM adapters on the host, with no RV64 toolchain or vendor library required:

zig build test-evmz-zkvm

ZisK requires its exact-commit provider archive. SP1 and OpenVM build their ERE-backed providers from the pinned repo manifests:

zig build guest-zisk -Dguest-payload=stateless-ere -Doptimize=ReleaseFast \
    -Dziskos-staticlib=/path/to/libziskos_staticlib.a

zig build guest-sp1 -Dguest-payload=stateless-ere -Doptimize=ReleaseFast

zig build guest-openvm -Dguest-payload=stateless-ere -Doptimize=ReleaseFast

The matching guest-zisk-run and guest-sp1-run steps execute the built ELF on the backend emulator; pass -Dguest-input=<file> for the stateless input and -Dguest-output=<file> to capture public output. ZisK runs use ziskemu from PATH unless -Dziskemu points elsewhere; the SP1 run step builds its host driver with Cargo on demand.

See guest/README.md for provider setup, heap and RAM sizing, schema pinning, and proof-readiness checks.

Performance

Representative Apple M1 Max VM-loop results from the fixed-Osaka ReleaseFast snapshot measured on 2026-08-13; lower is better:

Fixture evmz evmone-base revm-int
Arithmetic loop 0.104 ms 0.109 ms 0.501 ms
Storage SSTORE loop 0.162 ms 0.854 ms 0.875 ms
ERC20 transfer 3.761 ms 6.247 ms 6.084 ms
Snailtracer 19.965 ms 59.804 ms 37.990 ms

Both full snapshots, fixtures, methodology, and reproduction commands are in bench/README.md.

The evmz approach

evmz doesn't ship one general interpreter with runtime switches. Everything you'd normally toggle at runtime — fork rules, tracing, single-stepping, custom opcodes — is a compile-time decision, and each combination compiles into its own exact machine from one semantic foundation.

The interpreter is where the principle pays off most visibly. One set of handler semantics compiles into different execution models for different purposes:

  • Default — pure tail dispatch: each handler charges gas, executes, and tail-calls its successor. No central dispatch loop, no per-op capability checks.
  • Trace — a separate build whose dispatch table interleaves trace hooks with the same handlers. Observability is a different binary path, not a flag the default build tests per instruction.
  • Step — the continuation flips from chaining to yielding after each instruction, producing a genuine single-step interpreter for the debugger with behavior identical to the default build.

Around the interpreter sits a zero-alloc, pooled executor: frames, stacks, messages, and IO buffers live in preallocated slots (optionally hard-bounded for embedded/zkVM targets), and the state journal is cheap enough that the full executor benches within noise of the raw interpreter.

Hard-bounding the pools yields yet another purpose-built machine — the zkVM guest, where every dead branch and allocator call would be a proven, costed cycle.

Each purpose is its own compilation, and binaries carry exactly the machines they were built for. For an execution engine — where you know at ship time what you need

Packages

These standalone Zig libraries under pkg/ can be fetched independently:

  • pkg/rlp — strict RLP encoding and decoding
  • pkg/mpt — stateless MPT proofs, authenticated catalogs, and sparse/fixed-key updates
  • pkg/ssz — comptime-typed SSZ encoding and decoding

Scope

evmz is an execution engine, not a client. It includes the EVM, Ethereum block state transition, stateless witness validation, and the trie work needed to produce post-state and receipts roots.

Networking, block sync, consensus-layer fork choice, RPC, persistent storage, proof generation, and prover orchestration remain the caller's responsibility. Supply state through StateReader or an execution witness.

Contributing

zig build test                       # native unit tests
zig build test-evmz-zkvm             # zkVM adapter semantics on the host
zig build ci -j2                     # complete deterministic local CI
zig build eest-test                  # EEST lane
zig build tidy                       # dead and unexercised declarations
zig build debug -- 6001600201        # interactive bytecode debugger

tidy reports review candidates and never edits source; pass --strict to promote advisory findings.

See the release policy for the single-root package version, tag, and changelog conventions.

License

Licensed under either of:

at your option.

Bundled third-party components retain their own licenses: blst (Apache-2.0) and mcl (BSD-3-Clause). Distributions including them should reproduce the applicable license and NOTICE files.

Contributors

0xjojoex

Issues