diff --git a/.github/workflows/elixir.yml b/.github/workflows/ci.yml similarity index 62% rename from .github/workflows/elixir.yml rename to .github/workflows/ci.yml index 74c3970..f3ecb7c 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ # separate terms of service, privacy policy, and support # documentation. -name: Elixir CI +name: CI on: push: @@ -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 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..e35bc39 --- /dev/null +++ b/.github/workflows/pages.yml @@ -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 diff --git a/lib/eevm.ex b/lib/eevm.ex index efa5d93..eabfbb9 100644 --- a/lib/eevm.ex +++ b/lib/eevm.ex @@ -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} diff --git a/lib/eevm/block/receipt.ex b/lib/eevm/block/receipt.ex index efb0a81..f1efc87 100644 --- a/lib/eevm/block/receipt.ex +++ b/lib/eevm/block/receipt.ex @@ -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. diff --git a/mix.exs b/mix.exs index fcc235c..748a2e3 100644 --- a/mix.exs +++ b/mix.exs @@ -1,12 +1,20 @@ 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, @@ -14,7 +22,18 @@ defmodule EEVM.MixProject do 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 @@ -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 @@ -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 diff --git a/mix.lock b/mix.lock index b15658f..3f7fdbd 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, } diff --git a/test/test_helper.exs b/test/test_helper.exs index 7ee6d35..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,5 +1 @@ -"test/support/**/*.ex" -|> Path.wildcard() -|> Enum.each(&Code.require_file/1) - ExUnit.start()