An Ethereum Virtual Machine (EVM) implementation in TypeScript using the Effect library, with a focus on debuggability.
This implementation is designed for building developer tooling that requires deep introspection into EVM execution — such as debuggers, tracers, profilers, and testing frameworks. The Effect-TS foundation provides structured concurrency, typed errors, and built-in tracing capabilities that make it straightforward to observe and analyze every step of execution.
Primary packages:
| Package | Role |
|---|---|
@evm-effect/evm |
EVM interpreter, block/transaction processing, state, precompiles, and tracing. See packages/evm/README.md. |
@evm-effect/solc |
Compile Solidity via Effect (Node or Next.js worker). See packages/solc/README.md. |
Other packages in this repo (@evm-effect/ethereum-types, @evm-effect/crypto, @evm-effect/rlp, and internal packages) are published as needed for those two; you typically install evm and/or solc and let the package manager resolve the rest.
The documentation site lives in apps/docs — a Next.js app using
Fumadocs and Tailwind CSS. Its API reference is generated
from the TSDoc comments of every published package with
TypeDoc, so it always matches the source.
pnpm docs:dev # build the packages, generate the API reference, serve on :3000
pnpm docs:build # production build
pnpm docs:api # regenerate content/docs/api onlyGuides are written in apps/docs/content/docs. The api/ section underneath it
is generated and git-ignored; regenerate it with pnpm docs:api after changing
a package's doc comments. TypeDoc settings shared by every package live in
typedoc.base.json, with per-package entry points in each
packages/*/typedoc.json.
This implementation is extensively tested against the official Ethereum Execution Specs state tests and blockchain tests, from the tests-bal@v7.1.1 fixture release. Both suites run on every push and pull request via .github/workflows/evm-tests.yml:
| Suite | Tests | Result | Runtime |
|---|---|---|---|
| State tests | 49,931 | ✅ all passing | 2h 44m |
| Blockchain tests | 59,722 | ✅ all passing | 4h 36m |
| Total | 109,653 | ✅ 0 failures |
Numbers from the latest run on main (commit 381b062).
What isn't covered by those runs
The fixture release contains 220,583 test cases in total; 148,067 of them are in the two formats this runner consumes (state_test and blockchain_test). Of those, 109,653 execute — the rest are skipped by the fork allowlist in test/cli.ts:
| Skipped | Tests | Why |
|---|---|---|
| Amsterdam | 35,480 | Fork still in progress |
Petersburg (ConstantinopleFix) |
2,561 | Not yet in the enabled fork set |
Transition forks (ShanghaiToCancunAtTime15k, …) |
373 | Not yet in the enabled fork set |
The remaining 72,516 fixtures are in formats the runner does not consume: blockchain_test_engine (72,333), transaction_test (165), and blockchain_test_sync (18).
cd packages/evm
pnpm run fixtures # download the execution-spec fixtures (requires uv)
pnpm tsx ./test/cli.ts --format=state_test
pnpm tsx ./test/cli.ts --format=blockchain_testThe full suite takes several hours, so narrow it down with --filter (comma-separated substrings matched against the test id, fixture hash, fork, and format) and --limit:
pnpm tsx ./test/cli.ts --format=state_test --filter=eip7702 --limit=100Add --trace to emit EIP-3155 traces per test case, or --emit-report to write a pass/fail report file.
All released Ethereum forks are supported:
| Fork | Status |
|---|---|
| Frontier | ✅ |
| Homestead | ✅ |
| Tangerine Whistle | ✅ |
| Spurious Dragon | ✅ |
| Byzantium | ✅ |
| Constantinople | ✅ |
| Petersburg | ✅ |
| Istanbul | ✅ |
| Muir Glacier | ✅ |
| Berlin | ✅ |
| London | ✅ |
| Arrow Glacier | ✅ |
| Gray Glacier | ✅ |
| Paris (The Merge) | ✅ |
| Shanghai | ✅ |
| Cancun | ✅ |
| Prague | ✅ |
| Osaka | ✅ |
| Amsterdam (unreleased) | 🚧 In Progress |
This monorepo contains the following packages:
| Package | Description |
|---|---|
@evm-effect/evm |
Core EVM implementation with interpreter, block/transaction processing, state management, precompiles, and EIP-3155 tracing |
@evm-effect/ethereum-types |
Core Ethereum types (Address, Bytes, U256, etc.) |
@evm-effect/rlp |
RLP encoding and decoding |
@evm-effect/crypto |
Cryptographic primitives (keccak256, sha256, transaction signing) |
@evm-effect/solc |
Solidity compiler wrapper with typed schemas |
@evm-effect/shared |
Shared utilities (HashMap, HashSet) |
@evm-effect/examples |
Usage examples (contract deployment, transactions) |
import { Console, Effect } from "effect";
import { getRandomPrivateKey } from "@evm-effect/crypto/getRandomPrivateKey";
import { getAddressFromPrivateKey, signTransaction } from "@evm-effect/crypto/transactions";
import { Address, Bytes, Bytes32, U64, U256, Uint } from "@evm-effect/ethereum-types";
import {
Account, applyBody, BlockChain, BlockEnvironment,
Fork, LegacyTransaction, State
} from "@evm-effect/evm";
const program = Effect.gen(function* () {
// Create empty blockchain
const chainId = U64.constant(1n);
const blockchain = BlockChain.empty(chainId);
// Generate accounts
const alicePrivateKey = getRandomPrivateKey();
const alice = getAddressFromPrivateKey(alicePrivateKey);
const bob = new Address("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
// Fund Alice with 10 ETH
yield* State.setAccount(blockchain.state, alice, Account.make({
nonce: Uint.constant(0n),
balance: U256.constant(10n * 10n ** 18n),
code: Bytes.empty,
}));
// Create transaction: Alice sends 1 ETH to Bob
const unsignedTx = LegacyTransaction.make({
nonce: U256.constant(0n),
gasPrice: Uint.constant(10n * 10n ** 9n), // 10 gwei
gas: Uint.constant(21_000n),
to: bob,
value: U256.constant(1n * 10n ** 18n), // 1 ETH
data: Bytes.empty,
});
// Sign transaction
const signature = signTransaction({ transaction: unsignedTx, privateKey: alicePrivateKey });
const signedTx: LegacyTransaction = { ...unsignedTx, ...signature };
// Create block environment
const blockEnv = BlockEnvironment.make({
chainId,
state: blockchain.state,
blockGasLimit: Uint.constant(30_000_000n),
blockHashes: [],
coinbase: new Address("0xcccccccccccccccccccccccccccccccccccccccc"),
number: Uint.constant(1n),
baseFeePerGas: Uint.constant(10n * 10n ** 9n),
time: U256.constant(BigInt(Math.floor(Date.now() / 1000))),
prevRandao: Bytes32.zero,
difficulty: Uint.constant(0n),
excessBlobGas: U64.constant(0n),
parentBeaconBlockRoot: Bytes32.zero,
});
// Execute block with transaction
const blockOutput = yield* applyBody(blockEnv, [signedTx], [], []);
yield* Console.log(`Gas used: ${blockOutput.blockGasUsed.value}`);
yield* Console.log(`Bob balance: ${State.getAccount(blockchain.state, bob).balance.value / 10n ** 18n} ETH`);
});
// Run with London fork
Effect.runPromise(program.pipe(Effect.provide(Fork.london())));
// Output:
// Gas used: 21000
// Bob balance: 1 ETHnpm add @evm-effect/evm effect @effect/platform @effect/platform-nodePre-1.0 releases may still change APIs; pin versions in production as you would for any 0.x dependency.
From source (contributors or local development):
git clone https://github.com/julia-script/evm-effect.git
cd evm-effect
bun install
bun run buildMIT