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";constprogram=Effect.gen(function*(){// Create empty blockchainconstchainId=U64.constant(1n);constblockchain=BlockChain.empty(chainId);// Generate accountsconstalicePrivateKey=getRandomPrivateKey();constalice=getAddressFromPrivateKey(alicePrivateKey);constbob=newAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");// Fund Alice with 10 ETHyield*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 BobconstunsignedTx=LegacyTransaction.make({nonce: U256.constant(0n),gasPrice: Uint.constant(10n*10n**9n),// 10 gweigas: Uint.constant(21_000n),to: bob,value: U256.constant(1n*10n**18n),// 1 ETHdata: Bytes.empty,});// Sign transactionconstsignature=signTransaction({transaction: unsignedTx,privateKey: alicePrivateKey});constsignedTx: LegacyTransaction={ ...unsignedTx, ...signature};// Create block environmentconstblockEnv=BlockEnvironment.make({
chainId,state: blockchain.state,blockGasLimit: Uint.constant(30_000_000n),blockHashes: [],coinbase: newAddress("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 transactionconstblockOutput=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 forkEffect.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