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
43 changes: 42 additions & 1 deletion .github/workflows/elixir.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# separate terms of service, privacy policy, and support
# documentation.

name: Elixir CI
name: CI

on:
push:
Expand Down Expand Up @@ -82,3 +82,44 @@ jobs:
run: mix deps.get
- name: Run tests
run: mix test

docs:
name: Docs
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.18.3'
otp-version: '27.0'
- name: Restore dependencies cache
uses: actions/cache@v4
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Restore build cache
uses: actions/cache@v4
with:
path: |
_build
priv/native
native/eevm_bls/target
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock', 'native/**/Cargo.lock', 'native/**/Cargo.toml', 'native/**/src/**') }}
restore-keys: ${{ runner.os }}-build-
- name: Install dependencies
run: mix deps.get
- name: Compile project
run: mix compile
- name: Generate docs (fail on ExDoc warnings)
run: |
mix docs 2>&1 | tee docs.log
# Only fail on warnings emitted by ExDoc itself (after "Generating docs..."),
# not on compile warnings from deps that may surface during a fresh build.
if awk '/Generating docs\.\.\./{flag=1; next} flag' docs.log \
| grep -E "^[[:space:]]+warning:"; then
echo "::error::ExDoc emitted warnings — see log above"
exit 1
fi
66 changes: 66 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: GitHub Pages

on:
push:
branches: [ "main" ]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Build docs
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.18.3'
otp-version: '27.0'
- name: Restore dependencies cache
uses: actions/cache@v4
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Restore build cache
uses: actions/cache@v4
with:
path: |
_build
priv/native
native/eevm_bls/target
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock', 'native/**/Cargo.lock', 'native/**/Cargo.toml', 'native/**/src/**') }}
restore-keys: ${{ runner.os }}-build-
- name: Install dependencies
run: mix deps.get
- name: Generate docs
run: mix docs
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: doc

deploy:
name: Deploy to Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
38 changes: 16 additions & 22 deletions lib/eevm.ex
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
defmodule EEVM do
@moduledoc """
EEVM — An Ethereum Virtual Machine implementation in Elixir.
EEVM — Ethereum Virtual Machine implementation in Elixir.

This is a learning project that implements the core EVM execution engine.
It supports basic arithmetic, stack manipulation, memory operations,
and control flow opcodes.
Hardfork-aware execution engine covering Frontier through Prague: full
opcode set, all precompiles (including BLS12-381 via NIF), transaction
validation and processing, MPT-backed state root, and a harness for the
official `ethereum/tests` `GeneralStateTests` suite.

## Quick Start

# Execute raw bytecode: PUSH1 2, PUSH1 3, ADD, STOP
iex> result = EEVM.execute(<<0x60, 2, 0x60, 3, 0x01, 0x00>>)
iex> result.status
:stopped
iex> EEVM.stack_values(result)
[5]

## Architecture

- `EEVM.Interpreter.Stack` — LIFO stack (max 1024, uint256 values)
- `EEVM.Interpreter.Memory` — Byte-addressable linear memory
- `EEVM.Interpreter.MachineState` — Combined execution state
- `EEVM.Interpreter.Instructions.Registry` — Opcode definitions and metadata
- `EEVM.Interpreter` — The fetch-decode-execute loop

## Elixir Concepts Demonstrated

- **Structs & Maps** — Data structures with compile-time field checks
- **Pattern Matching** — Multi-clause functions, destructuring
- **Tagged Tuples** — `{:ok, val}` / `{:error, reason}` error handling
- **Recursion** — Tail-recursive execution loop (no mutable state)
- **Bitwise Operations** — Working with 256-bit integers
- **Module Attributes** — Compile-time constants
- **Typespecs** — `@spec` annotations for documentation and Dialyzer
## Module Map

- `EEVM.Interpreter` — fetch-decode-execute loop and call frames
- `EEVM.Interpreter.MachineState` — frame-local state (pc, stack, memory, gas)
- `EEVM.Precompiles` — registered precompiles (`0x01`–`0x11`)
- `EEVM.Transaction` — envelope decoding, validation, signing, intrinsic gas
- `EEVM.Block.Processor` — end-to-end transaction-to-receipt pipeline
- `EEVM.Database`, `EEVM.WorldState`, `EEVM.Storage` — account and storage state
- `EEVM.MPT.Trie` + `EEVM.StateRoot` — Merkle-Patricia trie and state-root hash
- `EEVM.HardforkConfig` — per-hardfork EIP activation flags
- `EEVM.SystemContracts` — EIP-2935 (block hashes) and EIP-4788 (beacon roots)
"""

alias EEVM.{Interpreter, Tracer}
Expand Down
2 changes: 1 addition & 1 deletion lib/eevm/block/receipt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule EEVM.Block.Receipt do
this transaction. Per the Yellow Paper, receipts are ordered and the
`cumulative_gas_used` of the last receipt equals the block's `gas_used`.
- `logs` — every log entry emitted by this transaction, in emission order.
Each log is a map matching `EEVM.Block.Bloom.log_entry/0`.
Each log is a map matching `t:EEVM.Block.Bloom.log_entry/0`.
- `logs_bloom` — the 2048-bit bloom filter over this receipt's logs, i.e.
`EEVM.Block.Bloom.from_logs(logs)`. We store it pre-computed so block-level
aggregation is a fast bytewise OR.
Expand Down
58 changes: 53 additions & 5 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
defmodule EEVM.MixProject do
use Mix.Project

@version "0.1.0"
@source_url "https://github.com/mw2000/eevm"

def project do
[
app: :eevm,
version: "0.1.0",
name: "eevm",
version: @version,
elixir: "~> 1.18",
description: "Ethereum Virtual Machine implementation in Elixir.",
source_url: @source_url,
homepage_url: @source_url,
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_paths(Mix.env()),
deps: deps(),
dialyzer: [
plt_add_deps: :app_tree,
plt_add_apps: [:ex_unit, :mix],
ignore_warnings: "dialyzer_ignore.exs",
list_unused_filters: true
],
aliases: aliases()
aliases: aliases(),
docs: docs()
]
end

def cli do
[
preferred_envs: [
dialyzer: :dev,
docs: :dev,
"hex.publish": :dev
]
]
end

Expand All @@ -24,18 +43,20 @@ defmodule EEVM.MixProject do
]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

defp deps do
[
{:ex_rlp, "~> 0.6.0"},
# Keccak-256 hash (Ethereum uses Keccak, not SHA3-256)
{:ex_keccak, "~> 0.7"},
{:ex_secp256k1, "~> 0.7"},
{:jason, "~> 1.4"},
# BN128 (alt_bn128) elliptic curve operations for EVM precompiles 0x06-0x08
{:bn, "~> 0.2.2"},
{:rustler, "~> 0.36", runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev], runtime: false}
{:dialyxir, "~> 1.4", only: [:dev], runtime: false},
{:ex_doc, "~> 0.34", only: :dev, runtime: false}
]
end

Expand All @@ -50,4 +71,31 @@ defmodule EEVM.MixProject do
]
]
end

defp docs do
[
main: "EEVM",
source_url: @source_url,
source_ref: "v#{@version}",
extras: ["README.md", "LICENSE"],
groups_for_modules: [
Interpreter: [~r/^EEVM\.Interpreter($|\.)/],
Instructions: [~r/^EEVM\.Interpreter\.Instructions/],
Precompiles: [~r/^EEVM\.Precompile($|s)/],
Block: [~r/^EEVM\.Block($|\.)/],
Transaction: [~r/^EEVM\.Transaction($|\.)/],
Context: [~r/^EEVM\.Context($|\.)/],
"System Contracts": [~r/^EEVM\.SystemContracts($|\.)/],
"State & Storage": [
EEVM.Database,
EEVM.Database.InMemory,
EEVM.Storage,
EEVM.WorldState,
EEVM.StateRoot,
~r/^EEVM\.MPT($|\.)/
],
Gas: [~r/^EEVM\.Gas($|\.)/, EEVM.HardforkConfig]
]
]
end
end
6 changes: 6 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
"credo": {:hex, :credo, "1.7.17", "f92b6aa5b26301eaa5a35e4d48ebf5aa1e7094ac00ae38f87086c562caf8a22f", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1eb5645c835f0b6c9b5410f94b5a185057bcf6d62a9c2b476da971cde8749645"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
"ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"},
"ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"},
"ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"},
"ex_secp256k1": {:hex, :ex_secp256k1, "0.8.0", "aade42e790638de82a2b951a83f55b9cc6545b8c105b20a0112ff6b78e1800cd", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "87257ef7110c45ac396a110eb93023371db96b6f65ed0676046a11e866d1e3a0"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"rustler": {:hex, :rustler, "0.37.3", "5f4e6634d43b26f0a69834dd1d3ed4e1710b022a053bf4a670220c9540c92602", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a6872c6f53dcf00486d1e7f9e046e20e01bf1654bdacc4193016c2e8002b32a2"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"},
}
4 changes: 0 additions & 4 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
"test/support/**/*.ex"
|> Path.wildcard()
|> Enum.each(&Code.require_file/1)

ExUnit.start()
Loading