eevm logo

eevm

An Ethereum Virtual Machine written in Elixir.

CI License: MIT Elixir

eevm runs EVM bytecode, transactions, and blocks in pure Elixir, for every fork from Frontier to Osaka. It is its own implementation, not a wrapper around revm or evmone, and its cryptography is Elixir too, from Keccak-256 to BLS12-381 and KZG. It has no native code: no NIFs and no OTP :crypto.

Status: eevm passes the official Ethereum test suites, apart from the exceptions under Conformance. It is young and its API is not yet stable, so think twice before using it where consensus correctness matters.

Installation

Add eevm to your dependencies in mix.exs; it needs Elixir 1.18 and OTP 27 or later.

def deps do
  [{:eevm, "~> 0.1.0"}]
end

The changelog lists what each release adds and changes.

Quick start

Run bytecode

EEVM.run/2 runs code as a message call, without transaction rules. status is :success, :revert, or {:halt, reason}.

# PUSH1 42, PUSH1 0, MSTORE, PUSH1 32, PUSH1 0, RETURN
iex> {:ok, result} = EEVM.run(<<0x60, 42, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>)
iex> {result.status, :binary.decode_unsigned(result.return_data), result.gas_used}
{:success, 42, 18}

Pass tracer: true to get an EIP-3155 trace in result.trace.

Deploy and call a contract

EEVM.create/3 deploys a contract and EEVM.call/4 calls it by address, with no key, nonce, or fees. EEVM.ABI builds the calldata and decodes the return data.

# Runtime code: return the first argument plus 1. Initcode: copy the
# runtime code from its own tail and return it.
iex> runtime = <<0x60, 4, 0x35, 0x60, 1, 0x01, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>
iex> initcode = <<0x60, 14, 0x60, 12, 0x60, 0, 0x39, 0x60, 14, 0x60, 0, 0xF3>> <> runtime
iex> {:ok, deployed} = EEVM.create(EEVM.Database.InMemory.new(), initcode, caller: 0xA11CE)
iex> calldata = EEVM.ABI.encode_call("increment(uint256)", [41])
iex> {:ok, result} = EEVM.call(deployed.db, deployed.contract_address, calldata, caller: 0xB0B)
iex> EEVM.ABI.decode(["uint256"], result.return_data)
{:ok, [42]}

Neither call commits anything: pass result.db to the next call. EEVM.revert_reason/1 decodes a reverted call's Error(string), Panic(uint256), or custom error.

Execute a transaction

EEVM.transact/4 runs a signed transaction end to end: validation, gas, execution, refunds, and fees. A transaction that fails validation returns {:error, reason}; one that reverts or halts returns {:ok, result} with that status.

alias EEVM.{BlockEnv, Config, Database}
alias EEVM.Database.InMemory
alias EEVM.Transaction.{FeeMarket, Signer}

private_key = <<0x46::256>>
{:ok, sender} = Signer.address(private_key)
config = Config.new(:cancun, chain_id: 1)
db = InMemory.new(accounts: %{sender => %{balance: 10 ** 18, nonce: 0, code: <<>>}})
block = BlockEnv.new(number: 1, base_fee: 7, coinbase: 0xC0FFEE)

{:ok, tx} =
  %FeeMarket{max_priority_fee_per_gas: 1, max_fee_per_gas: 10, gas_limit: 21_000, to: 0xB0B, value: 1_000}
  |> Signer.sign(private_key, config)

{:ok, result} = EEVM.transact(tx, block, db, config: config)

result.receipt
#=> %EEVM.Block.Receipt{tx_type: :eip1559, status: 1, cumulative_gas_used: 21000, ...}
Database.get_balance(result.db, 0xB0B)
#=> 1000

The API documentation covers the rest: building and executing blocks with EEVM.Block, your own state backend with EEVM.Database, tracing, simulation from any address, and every error.

Hardforks and EIPs

Pick the rules with config: EEVM.Config.new(:prague, chain_id: 1); the default is :cancun. Every rule is gated on the EIP that introduced it, and EEVM.Config.enabled?/2 says whether an EIP is active.

Area Implemented
Transactions Legacy with EIP-155, EIP-2718 envelopes, EIP-2930 access lists, EIP-1559 fee market, EIP-4844 blobs, EIP-7702 set code, EIP-7623 calldata floor, EIP-7825 gas limit cap, EIP-7594 blob limit
Execution and gas Full opcode set through Osaka (including PUSH0, TLOAD/TSTORE, MCOPY, BLOBHASH, BLOBBASEFEE, CLZ), EIP-2929 warm/cold access, EIP-2200 and EIP-3529 SSTORE metering and refunds, EIP-161, EIP-170, EIP-3541, EIP-3651, EIP-3860, EIP-6780
Precompiles 0x01–0x11 and 0x100: ECRECOVER, SHA256, RIPEMD160, IDENTITY, MODEXP (EIP-198, EIP-2565; EIP-7823 and EIP-7883 from Osaka), alt_bn128 (EIP-196/197), BLAKE2F (EIP-152), KZG point evaluation (EIP-4844), BLS12-381 (EIP-2537), P256VERIFY (EIP-7951, from Osaka)
Blocks RLP block codec, building, header and body validation, EIP-7934 size limit, EIP-4895 withdrawals, EIP-4788 beacon roots, EIP-2935 block hashes, EIP-7685 requests (EIP-6110 deposits, EIP-7002 withdrawals, EIP-7251 consolidations), receipts, logs bloom, MPT state, transaction, and receipt roots
Tooling EIP-3155 tracing, disassembler, custom precompile registration

Osaka's execution-layer EIPs are implemented. The forks that only change blob parameters (EIP-7892) are not modelled.

Conformance

CI runs both official fixture sources on every push, in the Conformance jobs of ci.yml, and fails on any case that a known-failures list doesn't cover.

CI job Cases passed Known failures Stale Skipped fixtures Notes
EEST state tests 44,039 of 44,039 0 0 0
EEST blockchain tests 47,563 of 47,563 0 0 7 Too slow for the CI timeout (#224)
EEST Osaka state tests 19,517 of 19,517 0 0 0
EEST Osaka blockchain tests 21,644 of 21,644 0 0 0
ethereum/tests state tests 41,652 of 41,652 0 0 2 Too slow for the CI timeout (#201, #224)
ethereum/tests blockchain tests 633 of 1,168 3 532 0 Failures: the DAO fork (#183). Stale: Prague fixtures filled before the EIP-7002/EIP-7251 request contracts were required

Case counts come from CI for these pinned versions, and a skipped fixture's cases aren't counted. The other columns mirror the lists in test/fixtures/, and a test fails when they disagree, so the table can't drift from what CI enforces. Each job's log has the latest counts.

Contributing

Read AGENTS.md for the conventions and checks. Open tasks are in GitHub Issues. mix bench benchmarks the interpreter and precompiles; bench/README.md describes the workloads.

License

MIT