From ed26cb6784b6dc02242cb572371f930cc218d7b1 Mon Sep 17 00:00:00 2001 From: mw2000 Date: Fri, 1 May 2026 23:56:32 -0700 Subject: [PATCH] =?UTF-8?q?chore(docs):=20trim=20moduledocs=20=E2=80=94=20?= =?UTF-8?q?drop=20EVM=20Concepts=20and=20Elixir=20Learning=20Notes=20secti?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "## EVM Concepts" sections were yellow-paper / EIP recaps duplicating public references; the "## Elixir Learning Notes" sections were Elixir tutorial content. Neither belongs in API documentation. Lead-in descriptions and API-relevant content (field tables, wire-encoding notes, examples) are preserved. Co-Authored-By: Claude Opus 4.7 --- lib/eevm/block/bloom.ex | 20 +---- lib/eevm/block/header.ex | 23 +----- lib/eevm/block/processor.ex | 37 +-------- lib/eevm/block/receipt.ex | 20 ++--- lib/eevm/block/result.ex | 18 +---- lib/eevm/block/withdrawal.ex | 17 +--- lib/eevm/context/block.ex | 17 +--- lib/eevm/context/contract.ex | 20 +---- lib/eevm/context/transaction.ex | 17 +--- lib/eevm/database.ex | 37 ++------- lib/eevm/database/in_memory.ex | 21 +---- lib/eevm/gas/intrinsic.ex | 26 ++----- lib/eevm/handler.ex | 16 +--- lib/eevm/hardfork_config.ex | 15 +--- lib/eevm/interpreter.ex | 41 +++------- .../interpreter/instructions/arithmetic.ex | 32 +------- lib/eevm/interpreter/instructions/bitwise.ex | 30 +------ .../interpreter/instructions/comparison.ex | 26 +------ .../interpreter/instructions/control_flow.ex | 42 ++-------- lib/eevm/interpreter/instructions/crypto.ex | 28 +------ lib/eevm/interpreter/instructions/helpers.ex | 35 ++------- lib/eevm/interpreter/instructions/logging.ex | 18 +---- lib/eevm/interpreter/journal.ex | 23 ++---- .../interpreter/machine_state/substate.ex | 6 +- lib/eevm/interpreter/memory.ex | 17 +--- lib/eevm/interpreter/stack.ex | 14 +--- lib/eevm/mpt/hex_prefix.ex | 19 +---- lib/eevm/mpt/trie.ex | 27 ++----- lib/eevm/precompiles/blake2f.ex | 21 +---- lib/eevm/precompiles/bn256.ex | 21 +---- lib/eevm/precompiles/ecrecover.ex | 32 +++----- lib/eevm/precompiles/identity.ex | 27 +------ lib/eevm/precompiles/kzg_point_eval.ex | 17 +--- lib/eevm/precompiles/modexp.ex | 28 +------ lib/eevm/precompiles/ripemd160.ex | 23 +----- lib/eevm/precompiles/sha256.ex | 23 ------ lib/eevm/storage.ex | 43 +--------- lib/eevm/system_contracts/beacon_roots.ex | 50 ++++-------- lib/eevm/system_contracts/block_hashes.ex | 78 ++++--------------- lib/eevm/transaction/intrinsic_gas.ex | 15 +--- lib/eevm/transaction/validator.ex | 13 +--- lib/eevm/transaction_result.ex | 33 ++------ lib/eevm/world_state.ex | 28 ++----- 43 files changed, 167 insertions(+), 947 deletions(-) diff --git a/lib/eevm/block/bloom.ex b/lib/eevm/block/bloom.ex index a219e4d..e14d15b 100644 --- a/lib/eevm/block/bloom.ex +++ b/lib/eevm/block/bloom.ex @@ -1,21 +1,9 @@ defmodule EEVM.Block.Bloom do @moduledoc """ - Ethereum logs bloom filter — 2048-bit probabilistic filter for log indexing. - - ## EVM Concepts - - Transaction receipts include a 256-byte (2048-bit) bloom filter that summarizes - all logs emitted during execution. Light clients use bloom filters to quickly - skip blocks/transactions that definitely don't contain events of interest. - - Each log's address and each indexed topic are added to the bloom using 3 hash - probes derived from Keccak-256. The algorithm is defined in Yellow Paper §4.3.1. - - ## Elixir Learning Notes - - - A bloom filter is represented as a fixed-size binary (`<<_::2048>>`). - - Bit-twiddling uses `Bitwise` operators (`band`, `bor`, `bsl`) on integers. - - We update a single byte inside a binary with pattern matching slices. + Ethereum logs bloom filter — 256-byte (2048-bit) probabilistic filter + summarising the addresses and indexed topics of every log in a receipt or + block. Each item is added via 3 hash probes derived from Keccak-256 + (Yellow Paper §4.3.1). The `data` field of a log is not included. """ import Bitwise diff --git a/lib/eevm/block/header.ex b/lib/eevm/block/header.ex index c2f2d65..739b7b8 100644 --- a/lib/eevm/block/header.ex +++ b/lib/eevm/block/header.ex @@ -1,17 +1,7 @@ defmodule EEVM.Block.Header do @moduledoc """ - Ethereum block header — the consensus-critical metadata for a block. - - ## EVM Concepts - - A block header is a thin descriptor of a block. It advertises the pre-state - parent pointers, the execution parameters (gas limit, base fee, randomness), - and the post-execution commitments (state / receipts / transactions roots - plus the aggregated logs bloom). - - The execution client is responsible for checking that the post-execution - commitments it computes match the ones advertised here. This struct is a - pure data carrier — validation lives in `EEVM.Block.Processor`. + Ethereum block header — the consensus-critical metadata for a block. A pure + data carrier; validation lives in `EEVM.Block.Processor`. ### Fields @@ -29,15 +19,6 @@ defmodule EEVM.Block.Header do | `receipts_root` | MPT root of `{rlp(index) => rlp(receipt)}` | | `transactions_root` | MPT root of `{rlp(index) => rlp(transaction)}` | | `logs_bloom` | 256-byte bloom summarising every log in the block | - - ## Elixir Learning Notes - - - This module defines only a struct plus tiny constructors. The field types - deliberately mirror `EEVM.Context.Block` where they overlap so callers can - move values between the "what the network says" layer (`Header`) and the - "what opcodes see" layer (`Context.Block`) without impedance. - - `new/0` and `new/1` follow the same zero-arg / keyword-override idiom used - by the rest of the codebase. """ @type t :: %__MODULE__{ diff --git a/lib/eevm/block/processor.ex b/lib/eevm/block/processor.ex index 77e61c0..61c755d 100644 --- a/lib/eevm/block/processor.ex +++ b/lib/eevm/block/processor.ex @@ -4,33 +4,14 @@ defmodule EEVM.Block.Processor do beacon-chain withdrawals, then the commitments a consensus-level verifier will check. - ## EVM Concepts - - A block is not just a bag of transactions. The execution client must: - - 1. Fire any *system calls* the active hardfork mandates before user code - runs. Post-Cancun this is EIP-4788 (beacon roots) and post-Prague this - is EIP-2935 (historical block hashes). - 2. Reject the block outright if the transactions, taken together, declare - more gas than the header allows. - 3. Execute each transaction against the evolving state, recording a receipt - whose `cumulative_gas_used` is the running total across the block. - 4. Produce the three MPT roots — `state_root`, `transactions_root`, - `receipts_root` — plus the aggregated `logs_bloom` and the block - `gas_used` figure. These are what external verifiers compare against the - header. - If any transaction fails (the injected executor returns an `{:error, _}`) the processor halts and reports the failing index. The post-state database is left at whatever the last *successful* transaction produced — this processor does not attempt optimistic rollback. - ## Design: dependency injection + ## Options - The issues that own the real transaction executor (#83) and the real - system-call hooks (#81 EIP-2935 / #82 EIP-4788) are being developed on - parallel branches and are not yet merged. To keep this PR independently - mergeable, we do not `alias` those modules here — callers pass them in: + Callers inject the executor and any per-block hooks: - `:tx_executor` — `(tx, %Context.Block{}, %Database{}) -> {:ok, tx_result} | {:error, reason}`. Each call receives the *current* @@ -50,20 +31,6 @@ defmodule EEVM.Block.Processor do reflects them. Defaults to `[]`. - `:hardfork` — optional atom, purely informational today; the real executor will consume it once wired in. - - Once #83 / #81 / #82 land, a follow-up PR will set sensible defaults for - `:tx_executor` and supply the real system-call hooks; until then callers - inject their own. - - ## Elixir Learning Notes - - - The transaction fold uses `Enum.reduce_while/3` so we can bail out - early on the first executor error without recursing manually. - - The receipts / transactions trie roots use `EEVM.MPT.Trie.root_hash/1` - (non-secure — keys are RLP-encoded indices, not hashes), matching the - Yellow Paper's specification for block-level tries. - - The logs-bloom aggregation reuses `EEVM.Block.Bloom.merge/2`: a 256-byte - bitwise OR already optimised in the bloom module. """ alias EEVM.Block.{Bloom, Header, Receipt, Result, Withdrawal} diff --git a/lib/eevm/block/receipt.ex b/lib/eevm/block/receipt.ex index f1efc87..f3930ed 100644 --- a/lib/eevm/block/receipt.ex +++ b/lib/eevm/block/receipt.ex @@ -1,23 +1,17 @@ defmodule EEVM.Block.Receipt do @moduledoc """ - Per-transaction receipt emitted after execution. + Per-transaction receipt emitted after execution (Byzantium-and-later layout). - ## EVM Concepts + ### Fields - A receipt is the execution-layer's summary of what happened when a transaction - ran. It does not carry the full transaction payload — instead it records the - post-conditions an observer needs to validate state transitions: - - - `status` — `1` if the top-level call completed, `0` if it reverted. The - Byzantium hardfork replaced the pre-image `state_root` field with this - one-byte status code; this module follows that modern layout. + - `status` — `1` if the top-level call completed, `0` if it reverted. - `cumulative_gas_used` — total gas consumed in the block up to and including - this transaction. Per the Yellow Paper, receipts are ordered and the - `cumulative_gas_used` of the last receipt equals the block's `gas_used`. + this transaction. 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 `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 + - `logs_bloom` — the 2048-bit bloom filter over this receipt's logs + (`EEVM.Block.Bloom.from_logs(logs)`); stored pre-computed so block-level aggregation is a fast bytewise OR. ## Wire encoding (EIP-2718) diff --git a/lib/eevm/block/result.ex b/lib/eevm/block/result.ex index 92283fd..6375041 100644 --- a/lib/eevm/block/result.ex +++ b/lib/eevm/block/result.ex @@ -1,13 +1,9 @@ defmodule EEVM.Block.Result do @moduledoc """ The outcome of executing a block — the post-state database plus every - commitment derived from it. + commitment derived from it. Returned by `EEVM.Block.Processor.process_block/4`. - ## EVM Concepts - - `EEVM.Block.Processor.process_block/4` returns one of these on success. The - fields line up with the header commitments a verifier would check against - the advertised `EEVM.Block.Header`: + ### Fields - `post_state_db` — the `EEVM.Database` after system calls and every transaction have been applied in order. @@ -17,14 +13,8 @@ defmodule EEVM.Block.Result do - `receipts_root` — MPT root of `{rlp(index) => rlp(receipt)}`. - `transactions_root` — MPT root of `{rlp(index) => rlp(transaction)}`. - `logs_bloom` — bitwise OR of every receipt's logs bloom. - - `gas_used` — total gas consumed across all transactions (matches - the final receipt's `cumulative_gas_used`, `0` when the block is empty). - - ## Elixir Learning Notes - - - This struct is intentionally a value type: the processor builds it once - at the end of `process_block/4` and hands it back unchanged. No helper - mutators live here. + - `gas_used` — total gas consumed across all transactions (matches the + final receipt's `cumulative_gas_used`, `0` when the block is empty). """ alias EEVM.Block.{Bloom, Receipt} diff --git a/lib/eevm/block/withdrawal.ex b/lib/eevm/block/withdrawal.ex index 497fc4e..71a59f6 100644 --- a/lib/eevm/block/withdrawal.ex +++ b/lib/eevm/block/withdrawal.ex @@ -3,20 +3,9 @@ defmodule EEVM.Block.Withdrawal do EIP-4895 beacon-chain withdrawal: a credit applied to an execution-layer account at the start of a block. - ## EVM Concepts - - Validators on the beacon chain accumulate rewards (and partial-stake exits) - that periodically need to land on the execution layer as plain balance - increases. The consensus layer hands the execution client a flat list of - these credits per block. They are not transactions: there is no signature, - no gas, no nonce, and no possibility of failure. The execution client - simply iterates the list and adds each `amount` to `address`. - - Per EIP-4895 the wire `amount` is denominated in *gwei*; the EVM works in - wei everywhere else, so the actual balance bump is `amount * 1_000_000_000`. - - Withdrawals are folded into block processing alongside transactions: the - state root the header advertises is post-withdrawals. + The wire `amount` is denominated in gwei; the EVM works in wei, so the + balance bump is `amount * 1_000_000_000`. Withdrawals are applied between + the transaction fold and the state-root commitment in `Block.Processor`. """ @type t :: %__MODULE__{ diff --git a/lib/eevm/context/block.ex b/lib/eevm/context/block.ex index 209b2dd..1351d8d 100644 --- a/lib/eevm/context/block.ex +++ b/lib/eevm/context/block.ex @@ -1,13 +1,7 @@ defmodule EEVM.Context.Block do @moduledoc """ - Block-level context — information about the block being executed. - - ## EVM Concepts - - The EVM has access to the block it's executing within. Smart contracts - use this to read timestamps, block numbers, and randomness (PREVRANDAO). - - In revm, this is the `Block` trait — the second of three context layers. + Block-level context — information about the block being executed, exposed + to the EVM via environment opcodes. ### Fields @@ -35,13 +29,6 @@ defmodule EEVM.Context.Block do Before The Merge, opcode 0x44 was DIFFICULTY. Post-Merge, the same opcode returns the RANDAO mix from the Beacon Chain — a source of on-chain randomness. - - ## Elixir Learning Notes - - - The `hashes` field is a Map — Elixir maps provide O(log n) lookup. - - `hash/2` validates that the requested block is within the last 256 blocks, - matching the EVM spec's constraint. - - `max/2` is a Kernel function — we use it to avoid negative numbers. """ @type t :: %__MODULE__{ diff --git a/lib/eevm/context/contract.ex b/lib/eevm/context/contract.ex index c537584..08e108c 100644 --- a/lib/eevm/context/contract.ex +++ b/lib/eevm/context/contract.ex @@ -1,14 +1,7 @@ defmodule EEVM.Context.Contract do @moduledoc """ Contract/message-level context — the current call frame's environment. - - ## EVM Concepts - - Each call frame (triggered by CALL, DELEGATECALL, etc.) has its own message - context. This is the innermost of the three context layers and changes with - every nested call. - - In revm, this maps to the `Host` trait's per-frame data. + Each CALL / DELEGATECALL / STATICCALL / CREATE frame has its own. ### Fields @@ -27,16 +20,7 @@ defmodule EEVM.Context.Contract do - `origin` (ORIGIN/tx.origin, in `EEVM.Context.Transaction`) = the EOA that signed the transaction. Never changes. - Example: EOA → Contract A → Contract B - - In Contract B: `caller` = A, `origin` = EOA - - ## Elixir Learning Notes - - - `calldata` is a raw binary (`<<>>`). Elixir's binary pattern matching - makes zero-copy slicing trivial via `binary_part/3`. - - `calldata_load/2` implements EVM spec behavior: reads 32 bytes from an - offset, right-padding with zeros if calldata is shorter than offset+32. - - `balances` is a simplified model — a real EVM would query a state database. + Example: EOA -> Contract A -> Contract B; in B, `caller = A`, `origin = EOA`. """ @type t :: %__MODULE__{ diff --git a/lib/eevm/context/transaction.ex b/lib/eevm/context/transaction.ex index fcff58a..424f747 100644 --- a/lib/eevm/context/transaction.ex +++ b/lib/eevm/context/transaction.ex @@ -1,14 +1,7 @@ defmodule EEVM.Context.Transaction do @moduledoc """ - Transaction-level context — information about the original transaction. - - ## EVM Concepts - - Every EVM execution is triggered by a transaction. The transaction context - captures who initiated the transaction and the gas economics they agreed to. - - In a production EVM (like revm), this is the `Transaction` trait — one of - three context layers that the VM reads from via environment opcodes. + Transaction-level context — information about the original transaction + exposed to the EVM via environment opcodes. ### Fields @@ -23,12 +16,6 @@ defmodule EEVM.Context.Transaction do `origin` is always the EOA that signed the transaction — it never changes, even through nested contract calls. `caller` (in `EEVM.Context.Contract`) is the *direct* caller of the current frame and changes with each CALL. - - ## Elixir Learning Notes - - - This is a simple struct with `struct!/2` for construction with validation. - - `new/0` and `new/1` pattern shows how to provide both zero-arg and keyword - constructors idiomatically. """ @type t :: %__MODULE__{ diff --git a/lib/eevm/database.ex b/lib/eevm/database.ex index 4acbe9f..8335031 100644 --- a/lib/eevm/database.ex +++ b/lib/eevm/database.ex @@ -1,23 +1,9 @@ defmodule EEVM.Database do @moduledoc """ - Behaviour defining the external state interface for the EVM. - - ## EVM Concepts - - The EVM requires access to two categories of persistent state during execution: - - - **Account state** — balances, nonces, and bytecode for all addresses (the - "world state" in Yellow Paper terminology). - - **Contract storage** — per-contract key-value mappings of 256-bit slots. - - This behaviour abstracts both behind a single interface, allowing different - backends (in-memory maps, Merkle-backed stores, databases) to be plugged in - without changing opcode implementations. - - The default implementation is `EEVM.Database.InMemory`, which wraps the - existing `EEVM.WorldState` and `EEVM.Storage` modules. - - ## Design + Behaviour defining the external state interface for the EVM — account + state (balances, nonces, code) plus per-contract storage — abstracted + behind a single interface so different backends (in-memory, Merkle-backed, + disk) can be plugged in without changing opcode implementations. A database value is a `%EEVM.Database{}` struct containing: @@ -25,19 +11,8 @@ defmodule EEVM.Database do - `state` — the implementation-specific state (opaque to callers) All public functions in this module dispatch to the `impl` module, passing - and receiving the opaque `state`. This keeps opcode modules decoupled from - any particular storage backend. - - ## Elixir Learning Notes - - - **Behaviours** define a contract (set of callbacks) that implementing modules - must fulfill — similar to interfaces in OOP languages. - - The `%Database{}` struct acts as a "type-erased wrapper": callers interact - with it through this module's public API, while the actual work is done by - whatever module sits behind `impl`. - - This pattern avoids Protocols (which dispatch on the first argument's struct - type) because we want a single struct type (`%Database{}`) that can wrap - any backend. + and receiving the opaque `state`. The default implementation is + `EEVM.Database.InMemory`. """ @type account :: %{ diff --git a/lib/eevm/database/in_memory.ex b/lib/eevm/database/in_memory.ex index 13182cb..8e961cd 100644 --- a/lib/eevm/database/in_memory.ex +++ b/lib/eevm/database/in_memory.ex @@ -1,29 +1,14 @@ defmodule EEVM.Database.InMemory do @moduledoc """ - In-memory database implementation using plain maps. - - ## EVM Concepts - - This is the default database backend for eevm. It stores all account state - (balances, nonces, code) and contract storage in Elixir maps — the same - data structures used by `EEVM.WorldState` and `EEVM.Storage` before the - Database abstraction was introduced. + In-memory `EEVM.Database` backend backed by plain maps. The state is a map with two keys: - `:accounts` — `%{address => account_map}` (balances, nonces, code) - `:storage` — `%{address => %{slot => value}}` (per-contract storage) - This implementation is suitable for testing, single-transaction execution, - and learning. For production use with persistent state, implement the - `EEVM.Database` behaviour with a disk-backed store. - - ## Elixir Learning Notes - - - `@behaviour EEVM.Database` tells the compiler to verify that this module - implements all required callbacks. Missing callbacks produce warnings. - - The state is a plain map (not a struct) to keep it lightweight and - easy to inspect in tests. + Suitable for testing and single-transaction execution. For persistent + state, implement the `EEVM.Database` behaviour against a disk-backed store. """ @behaviour EEVM.Database diff --git a/lib/eevm/gas/intrinsic.ex b/lib/eevm/gas/intrinsic.ex index 67b2d3a..59642f2 100644 --- a/lib/eevm/gas/intrinsic.ex +++ b/lib/eevm/gas/intrinsic.ex @@ -2,28 +2,12 @@ defmodule EEVM.Gas.Intrinsic do @moduledoc """ Intrinsic gas costs for transactions — costs that apply before execution begins. - ## EVM Concepts + Two main components: - Every transaction pays a base intrinsic cost *before* the EVM starts executing - its bytecode. The two main components are: - - - **Base cost**: 21,000 gas for a regular transaction (not used here directly). - - **Calldata cost**: per-byte fee on the transaction's `data` field, - differentiated by whether each byte is zero or non-zero. - - ### EIP-2028 (Istanbul hardfork) - - EIP-2028 reduced the cost of non-zero calldata bytes from **68 gas → 16 gas**, - making data-heavy transactions (rollup calldata, token transfers) significantly - cheaper. Zero bytes remained at 4 gas — their lower cost reflects that they - compress well and impose less load on the network. - - ## Elixir Learning Notes - - - The `for <>` comprehension iterates a binary byte-by-byte - without converting it to a list — efficient and idiomatic for binary data. - - `reduce: 0` seeds the accumulator and returns the final accumulated value. - - Module attributes (`@tx_data_zero_gas`) make constants auditable in one place. + - **Base cost**: 21,000 gas for a regular transaction. + - **Calldata cost**: per-byte fee on the transaction's `data` field — + 4 gas per zero byte, 16 gas per non-zero byte (post-Istanbul / EIP-2028; + pre-Istanbul, non-zero bytes cost 68 gas). """ @tx_base_gas 21_000 diff --git a/lib/eevm/handler.ex b/lib/eevm/handler.ex index 6748526..1c15e08 100644 --- a/lib/eevm/handler.ex +++ b/lib/eevm/handler.ex @@ -2,12 +2,8 @@ defmodule EEVM.Handler do @moduledoc """ End-to-end transaction handler. - ## EVM Concepts - - A signed Ethereum transaction is not a single operation — it is a sequence - of well-defined steps the client must perform against current world state. - This module orchestrates those steps, deferring each phase to a sibling - module so the layering stays inspectable: + Orchestrates the per-transaction pipeline, deferring each phase to a + sibling module so the layering stays inspectable: - `EEVM.Handler.Validation` — decode → recover sender → validate - `EEVM.Handler.PreExecution` — charge upfront → intrinsic gas → bump nonce @@ -17,14 +13,6 @@ defmodule EEVM.Handler do This split mirrors revm's `handler/` crate (`validation.rs`, `pre_execution.rs`, `execution.rs`, `post_execution.rs`) and keeps each phase independently testable. - - ## Elixir Learning Notes - - - The pipeline is a `with` chain: each step returns `{:ok, …}` or - `{:error, reason}`, and the chain short-circuits on the first failure. - - The sender is represented two ways: as a 20-byte binary (from the wire - and from `Signer.recover_sender/2`) and as an integer (inside - `EEVM.Context.Transaction` and `EEVM.Database`'s key space). """ alias EEVM.{Config, Database, TransactionResult} diff --git a/lib/eevm/hardfork_config.ex b/lib/eevm/hardfork_config.ex index c065dff..19bae40 100644 --- a/lib/eevm/hardfork_config.ex +++ b/lib/eevm/hardfork_config.ex @@ -2,12 +2,8 @@ defmodule EEVM.HardforkConfig do @moduledoc """ Hardfork configuration controlling which EIP rules are active during execution. - ## EVM Concepts - - Ethereum upgrades via *hardforks* — coordinated rule changes activated at a - specific block number (or, post-Merge, timestamp). Each hardfork is a superset - of all previous rules plus its own additions. The `spec_id` atom identifies the - target hardfork, and `enabled?/2` answers whether a given EIP is active. + The `spec_id` atom identifies the target hardfork, and `enabled?/2` answers + whether a given EIP is active under that spec. Hardfork timeline (oldest → newest): @@ -26,13 +22,6 @@ defmodule EEVM.HardforkConfig do | `:shanghai` | EIP-3651 (COINBASE pre-warm), EIP-3855 (PUSH0), EIP-3860 (initcode limit) | | `:cancun` | EIP-1153 (TLOAD/TSTORE), EIP-4844 (blobs), EIP-5656 (MCOPY), EIP-6780 (SELFDESTRUCT) | | `:prague` | EIP-2537 (BLS12-381 precompiles), EIP-7623 (calldata floor), EIP-7702 (set-code) | - - ## Elixir Learning Notes - - - We represent ordering as a map from atom to integer. This lets us compare - hardforks with simple integer arithmetic rather than a cumbersome `cond` chain. - - `@spec_order` is a module attribute — a compile-time constant. Using it as the - default argument in `enabled?/2`'s guard keeps the function head readable. """ @type spec_id :: diff --git a/lib/eevm/interpreter.ex b/lib/eevm/interpreter.ex index 720cb07..2495612 100644 --- a/lib/eevm/interpreter.ex +++ b/lib/eevm/interpreter.ex @@ -2,38 +2,19 @@ defmodule EEVM.Interpreter do @moduledoc """ The EVM interpreter — fetches, decodes, and dispatches opcodes in a loop. - This is the core opcode-execution engine. It is invoked by the - `EEVM.Handler` (the transaction-level pipeline) and by system - contracts that need to evaluate bytecode against an existing world state. - - ## EVM Concepts - - The executor implements the EVM's fetch-decode-execute cycle: - - 1. **Fetch**: Read one byte from bytecode at the current program counter. - 2. **Decode**: Look up the base gas cost and identify which opcode module - handles this byte. - 3. **Execute**: Deduct base gas, delegate to the opcode module, and loop. - - Execution ends when: - - A terminating opcode is reached (STOP, RETURN, REVERT, INVALID). - - The program counter advances past the end of the bytecode (implicit STOP). - - Gas runs out before the opcode can execute. - - INVALID (0xFE) is special-cased: its "base" gas cost is set to the entire + This is the core opcode-execution engine. It is invoked by `EEVM.Handler` + (the transaction-level pipeline) and by system contracts that need to + evaluate bytecode against an existing world state. + + Execution ends when a terminating opcode is reached (STOP, RETURN, REVERT, + INVALID), the program counter advances past the end of the bytecode + (implicit STOP), or gas runs out before the opcode can execute. INVALID + (0xFE) is special-cased: its "base" gas cost is set to the entire remaining gas, draining it completely before the opcode runs. - ## Elixir Learning Notes - - - `run_loop/1` is tail-recursive — Elixir optimizes tail calls, so the loop - runs in constant stack space even for long-running contracts. - - The three `run_loop/1` clauses use pattern matching on `status` to cleanly - separate running, halted, and out-of-gas states without any conditionals. - - Opcode dispatch is data-driven: `EEVM.Interpreter.Instructions.Registry` - maps each opcode to its implementing module, and `execute_opcode/2` is a - single registry lookup rather than a hand-maintained case table. - - Separation of concerns: the executor only orchestrates; opcode modules own - their semantics. + Opcode dispatch is data-driven via + `EEVM.Interpreter.Instructions.Registry`; opcode modules own their + semantics, and the interpreter only orchestrates. """ alias EEVM.Database diff --git a/lib/eevm/interpreter/instructions/arithmetic.ex b/lib/eevm/interpreter/instructions/arithmetic.ex index c66b5ce..47ead1d 100644 --- a/lib/eevm/interpreter/instructions/arithmetic.ex +++ b/lib/eevm/interpreter/instructions/arithmetic.ex @@ -1,35 +1,5 @@ defmodule EEVM.Interpreter.Instructions.Arithmetic do - @moduledoc """ - EVM arithmetic opcodes: ADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND. - - ## EVM Concepts - - All arithmetic operates on unsigned 256-bit integers (uint256). Overflow wraps - silently modulo 2^256 — there are no exceptions or traps. This means, for - example, that `max_uint256 + 1 == 0`. - - Key rules: - - 1. **Wrapping arithmetic** — ADD, MUL, SUB all wrap mod 2^256. We enforce - this with a bitwise AND mask after each operation. - 2. **Division by zero returns 0** — unlike most languages, DIV, SDIV, MOD, - and SMOD all return 0 when the divisor is zero. No exception is raised. - 3. **Signed operations use two's complement** — SDIV and SMOD interpret - operands as signed 256-bit integers before dividing. - 4. **ADDMOD and MULMOD use unlimited precision** — they compute `(a + b) mod n` - and `(a * b) mod n` without the intermediate result wrapping at 2^256. - Elixir's arbitrary-precision integers make this free. - 5. **EXP has dynamic gas** — cost grows with the byte length of the exponent. - - ## Elixir Learning Notes - - - Elixir integers are arbitrary-precision, so `a * b` never overflows. We - apply a `band(..., @max_uint256)` mask to truncate to 256 bits explicitly. - - Each opcode clause is a separate `execute/2` head matched by the opcode byte. - This is idiomatic pattern matching on function arguments. - - `with` chains short-circuit on `{:error, reason}` — each line binds a - variable only if the previous step succeeded. - """ + @moduledoc "EVM arithmetic opcodes: ADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND." import Bitwise alias EEVM.Gas.Dynamic diff --git a/lib/eevm/interpreter/instructions/bitwise.ex b/lib/eevm/interpreter/instructions/bitwise.ex index da2b15c..9b3e49b 100644 --- a/lib/eevm/interpreter/instructions/bitwise.ex +++ b/lib/eevm/interpreter/instructions/bitwise.ex @@ -1,33 +1,5 @@ defmodule EEVM.Interpreter.Instructions.Bitwise do - @moduledoc """ - EVM bitwise opcodes: AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR. - - ## EVM Concepts - - Bitwise opcodes operate on the full 256-bit integer values on the stack. - There are no partial-width operations; every operand is treated as a 256-bit - number. - - Key behaviors: - - 1. **NOT is bitwise complement** — it flips all 256 bits. Equivalent to XOR - with `max_uint256` (all bits set). - 2. **BYTE extracts a single byte** — byte index 0 is the most significant byte - (big-endian). If the index is >= 32, the result is 0. - 3. **SHL and SHR are logical shifts** — zeros fill in from the empty side. - Added in Constantinople via EIP-145. Shifting by >= 256 always returns 0. - 4. **SAR is arithmetic right shift** — it preserves the sign bit, filling the - top bits with 1s when shifting a negative value. Added in Constantinople. - - ## Elixir Learning Notes - - - `import Bitwise` brings in `band/2`, `bor/2`, `bxor/2`, `bnot/1`, `<<<`, - and `>>>` as local functions and operators. - - Elixir's integers are arbitrary-precision, so left shifts don't saturate. - We mask with `@max_uint256` after left shifts to stay within 256 bits. - - SAR converts to signed via `Helpers.to_signed/1`, shifts with `>>>` (which - fills with 1s for negative numbers in Elixir), then converts back to uint256. - """ + @moduledoc "EVM bitwise opcodes: AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR." import Bitwise alias EEVM.Interpreter.Instructions.Helpers diff --git a/lib/eevm/interpreter/instructions/comparison.ex b/lib/eevm/interpreter/instructions/comparison.ex index 26809a0..44a4397 100644 --- a/lib/eevm/interpreter/instructions/comparison.ex +++ b/lib/eevm/interpreter/instructions/comparison.ex @@ -1,29 +1,5 @@ defmodule EEVM.Interpreter.Instructions.Comparison do - @moduledoc """ - EVM comparison opcodes: LT, GT, SLT, SGT, EQ, ISZERO. - - ## EVM Concepts - - Comparison opcodes consume their operands and push a boolean result as either - 1 (true) or 0 (false). The EVM has no native boolean type. - - Key behaviors: - - 1. **Unsigned comparisons** — LT, GT, and EQ treat both operands as uint256. - `1 < 2` is true; `max_uint256 < 0` is false. - 2. **Signed comparisons** — SLT and SGT interpret operands as two's complement - signed integers. The high bit is the sign bit, so `max_uint256` is -1. - 3. **ISZERO is unary** — it pops one value and pushes 1 if it is zero, 0 - otherwise. It's also used to negate a boolean (ISZERO of a comparison result). - - ## Elixir Learning Notes - - - Passing comparison functions as arguments (`&Kernel.= 0x60 and op <= 0x7F`) match all PUSH opcodes in - a single clause, avoiding 32 separate function heads. - - Binary pattern matching (`MachineState.read_code/3`) extracts raw bytes - from the bytecode binary with no intermediate parsing step. - - DUP depth is derived by subtracting the base opcode (`op - 0x80`), and - SWAP depth adds 1 (`op - 0x90 + 1`) to include the top-of-stack position. + Covers: + + - **Branching**: JUMP (0x56), JUMPI (0x57) — destination must land on a + JUMPDEST (0x5B) byte. + - **PUSH**: PUSH0 (0x5F, EIP-3855), PUSH1-PUSH32 (0x60-0x7F). + - **DUP**: DUP1-DUP16 (0x80-0x8F). + - **SWAP**: SWAP1-SWAP16 (0x90-0x9F). + - **PC** (0x58) and **JUMPDEST** (0x5B). """ alias EEVM.HardforkConfig diff --git a/lib/eevm/interpreter/instructions/crypto.ex b/lib/eevm/interpreter/instructions/crypto.ex index ec7217c..6cc5355 100644 --- a/lib/eevm/interpreter/instructions/crypto.ex +++ b/lib/eevm/interpreter/instructions/crypto.ex @@ -1,31 +1,5 @@ defmodule EEVM.Interpreter.Instructions.Crypto do - @moduledoc """ - EVM cryptographic opcodes: KECCAK256. - - ## EVM Concepts - - KECCAK256 (opcode 0x20, formerly called SHA3 before standardization) hashes a - region of memory and pushes the 256-bit hash onto the stack. - - It's one of the most frequently used opcodes in real contracts: - - - **Solidity mappings** — `mapping(key => value)` computes storage slots as - `keccak256(abi.encode(key, slot_index))`. - - **Event signatures** — `Transfer(address,address,uint256)` is identified by - the first 4 bytes of `keccak256("Transfer(address,address,uint256)")`. - - **CREATE2 addresses** — deterministic contract addresses use keccak256. - - Gas cost: 30 (static) + 6 per 32-byte word of input (dynamic). - Memory expansion cost applies if the input range extends beyond current memory. - - ## Elixir Learning Notes - - - We use the `ExKeccak` NIF (native implemented function) for hashing. - NIFs call into compiled C code, making them much faster than a pure Elixir - implementation for cryptographic operations. - - The binary pattern `<>` decodes the raw 32-byte - hash binary into a single 256-bit unsigned integer in one step. - """ + @moduledoc "EVM cryptographic opcode: KECCAK256 (0x20)." alias EEVM.Gas.Dynamic alias EEVM.Gas.Memory, as: GasMemory alias EEVM.Interpreter.{MachineState, Memory, Stack} diff --git a/lib/eevm/interpreter/instructions/helpers.ex b/lib/eevm/interpreter/instructions/helpers.ex index 3b9233a..83ea81f 100644 --- a/lib/eevm/interpreter/instructions/helpers.ex +++ b/lib/eevm/interpreter/instructions/helpers.ex @@ -2,33 +2,14 @@ defmodule EEVM.Interpreter.Instructions.Helpers do @moduledoc """ Shared utilities for opcode implementations. - ## EVM Concepts - - Many opcode categories (arithmetic, comparison, bitwise) share the same - structural patterns: pop operands, compute, push result, advance PC. Pulling - those patterns into helpers keeps each opcode module focused on what makes it - unique and avoids duplicating error-handling boilerplate. - - Key responsibilities: - - 1. **Signed arithmetic** — the EVM stack stores everything as uint256, but - several opcodes (SLT, SGT, SDIV, SMOD, SAR) interpret values as signed. - Two's complement conversion is the bridge between the two representations. - 2. **Modular exponentiation** — the EXP opcode raises a base to an exponent - mod 2^256. A naive loop would be impossibly slow for large exponents, so - we use square-and-multiply (binary exponentiation). - 3. **Jump validation** — JUMP and JUMPI must land on a JUMPDEST (0x5B) byte. - Any other destination is invalid and halts execution. - - ## Elixir Learning Notes - - - These helpers are `def` (public) so any opcode module can call them. - Each module keeps its own private helpers private (`defp`) to avoid - polluting the shared namespace. - - `with` chains let us sequence fallible stack operations cleanly. If any - step returns `{:error, reason}`, execution jumps to the `else` clause. - - Pattern matching in `to_signed/1` guards (`when value >= @sign_bit`) - avoids a conditional branch — the correct clause is selected at call time. + Provides: + + - Stack-pop / push helpers for the common pop-pop-compute-push shape used + by arithmetic, comparison, and bitwise opcodes. + - Two's-complement conversion (`to_signed/1`, `to_unsigned/1`) bridging + the uint256 stack with signed opcodes (SLT, SGT, SDIV, SMOD, SAR). + - `mod_pow/3`, square-and-multiply modular exponentiation used by EXP. + - `valid_jumpdest?/2` for JUMP / JUMPI destination validation. """ import Bitwise diff --git a/lib/eevm/interpreter/instructions/logging.ex b/lib/eevm/interpreter/instructions/logging.ex index 832417f..2b395cf 100644 --- a/lib/eevm/interpreter/instructions/logging.ex +++ b/lib/eevm/interpreter/instructions/logging.ex @@ -1,21 +1,5 @@ defmodule EEVM.Interpreter.Instructions.Logging do - @moduledoc """ - EVM logging opcodes — LOG0 through LOG4. - - ## EVM Concepts - - - LOG instructions emit event logs that are stored in transaction receipts. - - Each log contains the emitting contract address, a data payload from memory, - and 0–4 indexed topic values from the stack. - - Topics are 256-bit values used for efficient off-chain filtering (e.g. event signatures). - - Gas cost: 375 base + 375 per topic + 8 per byte of data + memory expansion. - - ## Elixir Learning Notes - - - We use pattern matching on the topic count (0–4) to dispatch LOG variants. - - Logs accumulate in `state.substate.logs` as a list of maps, preserving insertion order. - - `binary_part/3` extracts a slice from a binary — used to read log data from memory bytes. - """ + @moduledoc "EVM logging opcodes — LOG0 through LOG4." alias EEVM.Gas.Dynamic alias EEVM.Gas.Memory, as: GasMemory diff --git a/lib/eevm/interpreter/journal.ex b/lib/eevm/interpreter/journal.ex index 9fd7601..8f32c2c 100644 --- a/lib/eevm/interpreter/journal.ex +++ b/lib/eevm/interpreter/journal.ex @@ -2,16 +2,12 @@ defmodule EEVM.Interpreter.Journal do @moduledoc """ Snapshot/commit semantics for nested EVM call frames. - ## EVM Concepts + When a child frame halts normally (`:stopped`), its mutations to the + parent's revertible fields are promoted; on any other halt they are + discarded and the parent observes the pre-call state. - When a child frame runs (CALL, DELEGATECALL, STATICCALL, CREATE, CREATE2), - its mutations to world state become permanent only if the child halts - normally (`:stopped`). On revert, out-of-gas, invalid, or any other abnormal - halt, those mutations must be discarded — the parent must observe the world - state it had before the child was spawned. - - Concretely, the `db` field and the entire `substate` struct on - `EEVM.Interpreter.MachineState` participate in this revert behaviour: + Revertible fields (the `db` plus all of `substate`) participate in the + merge: - `db` — the unified world database (accounts + storage) - `substate.logs` — emitted log entries (parent's first, then child's) @@ -24,15 +20,6 @@ defmodule EEVM.Interpreter.Journal do monotonically and are always preserved across child boundaries regardless of the child's outcome. - ## Why a dedicated module - - Before this module existed, every call/create site re-implemented the - conditional "if child succeeded, take child's six fields, else keep - parent's" with six per-field local variables and six explicit `Map.put/3` - calls. Centralising the merge means: (1) a future revertible field is - added in one place, not six; (2) call sites read as "merge child result" - rather than as a hand-wired six-field reconciliation. - ## What this module does NOT cover - Stack and memory: those are local to a frame and never propagate child diff --git a/lib/eevm/interpreter/machine_state/substate.ex b/lib/eevm/interpreter/machine_state/substate.ex index db8cbdf..fbf8acb 100644 --- a/lib/eevm/interpreter/machine_state/substate.ex +++ b/lib/eevm/interpreter/machine_state/substate.ex @@ -2,11 +2,7 @@ defmodule EEVM.Interpreter.MachineState.Substate do @moduledoc """ Transaction-scoped accumulators that survive nested call frames. - ## EVM Concepts - - The Yellow Paper calls this "substate" — fields that accumulate across - successful call frames within a single transaction and are discarded - on revert. Concretely: + ### Fields - `touched_addresses` — EIP-161 cleanup set; empty accounts touched during the transaction are deleted on commit. diff --git a/lib/eevm/interpreter/memory.ex b/lib/eevm/interpreter/memory.ex index e0e15f6..fd03e2d 100644 --- a/lib/eevm/interpreter/memory.ex +++ b/lib/eevm/interpreter/memory.ex @@ -2,20 +2,9 @@ defmodule EEVM.Interpreter.Memory do @moduledoc """ EVM memory — a byte-addressable, dynamically expanding linear memory. - ## EVM Concepts - - - Memory is a word-addressed byte array that expands in 32-byte chunks. - - Reading/writing beyond current size auto-expands (zero-filled). - - Gas cost increases quadratically with memory size (see `EEVM.Gas.Memory`). - - ## Elixir Learning Notes - - - We use a `Map` for sparse storage — only written bytes are stored. - This is more memory-efficient than a huge binary for a learning impl. - - `Map.get(map, key, default)` returns a default if the key is missing — - perfect for zero-filled memory semantics. - - Binaries (`<<>>`) are Elixir's way of handling raw bytes. We use them - for reading contiguous chunks. + Memory expands in 32-byte chunks. Reads or writes beyond the current size + auto-expand and are zero-filled. Gas cost increases quadratically with + memory size (see `EEVM.Gas.Memory`). """ @type t :: %__MODULE__{ diff --git a/lib/eevm/interpreter/stack.ex b/lib/eevm/interpreter/stack.ex index 587f280..f8d1b86 100644 --- a/lib/eevm/interpreter/stack.ex +++ b/lib/eevm/interpreter/stack.ex @@ -1,17 +1,5 @@ defmodule EEVM.Interpreter.Stack do - @moduledoc """ - The EVM stack — a LIFO data structure with a max depth of 1024. - - ## Elixir Learning Notes - - - We use a simple list as the underlying data structure. In Elixir, lists are - linked lists, so prepending (push) is O(1) and popping the head is O(1). - - The `@max_depth` is a module attribute — Elixir's version of a constant. - - Pattern matching in function heads (`[top | rest]`) is idiomatic Elixir — - we destructure data right where we receive it. - - We return tagged tuples like `{:ok, value}` and `{:error, reason}` which is - the Elixir convention for fallible operations. - """ + @moduledoc "The EVM stack — a LIFO data structure with a max depth of 1024." @max_depth 1024 diff --git a/lib/eevm/mpt/hex_prefix.ex b/lib/eevm/mpt/hex_prefix.ex index d740c37..736b3a6 100644 --- a/lib/eevm/mpt/hex_prefix.ex +++ b/lib/eevm/mpt/hex_prefix.ex @@ -2,28 +2,15 @@ defmodule EEVM.MPT.HexPrefix do @moduledoc """ Hex-prefix (aka compact) encoding used by Ethereum's Merkle Patricia Trie. - ## EVM Concepts - - Trie leaf and extension nodes do not store nibble paths directly. Instead, they - store a compact byte representation that combines: - - - node kind bit (`leaf` vs `extension`) - - path parity bit (`odd` vs `even` nibble length) - - the path nibbles themselves - - The first nibble contains the flags: + Trie leaf and extension nodes encode their nibble path as a compact byte + string. The first nibble carries node kind and parity flags: - `0x0` = extension, even path length - `0x1` = extension, odd path length - `0x2` = leaf, even path length - `0x3` = leaf, odd path length - ## Elixir Learning Notes - - - We keep paths as nibble lists (`[0..15]`) for clarity while building trie - nodes. - - Encoding/decoding uses small pure helper functions and binary pattern - matching, so the module stays deterministic and side-effect free. + Paths are represented as nibble lists (`[0..15]`). """ @typedoc "Nibble values used in hex-prefix paths." diff --git a/lib/eevm/mpt/trie.ex b/lib/eevm/mpt/trie.ex index e8d1944..9426f8f 100644 --- a/lib/eevm/mpt/trie.ex +++ b/lib/eevm/mpt/trie.ex @@ -2,28 +2,13 @@ defmodule EEVM.MPT.Trie do @moduledoc """ Ethereum Merkle Patricia Trie (MPT) root computation. - ## EVM Concepts + The trie is path-compressed (nibble-by-nibble keys, hex-prefix compact + encoding for leaves and extensions) and content-addressed: each node is + RLP-encoded, and a child reference is either the raw RLP payload (when + `byte_size(RLP(node)) < 32`) or `keccak256(RLP(node))` otherwise. - Ethereum stores account state and contract storage in a Merkle Patricia Trie. - This trie is path-compressed and content-addressed: - - - keys are traversed nibble-by-nibble (4-bit steps) - - leaves and extensions use hex-prefix compact encoding - - each node is RLP-encoded - - child references follow the 32-byte rule: - - `RLP(node) < 32` bytes => embed inline in parent - - `RLP(node) >= 32` bytes => embed `keccak256(RLP(node))` - - This module implements the recursive `patricialize` strategy from Ethereum's - execution specs, producing roots compatible with geth/revm for the same inputs. - - ## Elixir Learning Notes - - - The implementation is fully functional (no processes, no ETS, no mutable - node database). - - We represent trie nodes with tagged tuples and recursively encode bottom-up. - - Inline-child handling uses manual RLP list payload assembly so pre-encoded - child bytes are embedded as raw RLP elements (not double-encoded). + Implements the recursive `patricialize` strategy from Ethereum's execution + specs, producing roots compatible with geth/revm for the same inputs. """ alias EEVM.MPT.HexPrefix diff --git a/lib/eevm/precompiles/blake2f.ex b/lib/eevm/precompiles/blake2f.ex index 2d1a42c..01a1a61 100644 --- a/lib/eevm/precompiles/blake2f.ex +++ b/lib/eevm/precompiles/blake2f.ex @@ -2,16 +2,7 @@ defmodule EEVM.Precompiles.Blake2F do @moduledoc """ Blake2f precompile — address `0x09` (EIP-152). - ## EVM Concepts - - EIP-152 adds the Blake2b compression function `F` as an EVM precompile so - contracts can verify data structures from ecosystems that rely on BLAKE2, - especially **Zcash block headers** and **Equihash proofs**, without expensive - in-EVM emulation. - - This also benefits interoperability with chains and protocols that use - Blake2-based primitives, including **Filecoin proof systems**, where on-chain - verification needs the exact compression round function. + Implements the Blake2b compression function `F`. ### Gas schedule @@ -29,16 +20,6 @@ defmodule EEVM.Precompiles.Blake2F do - byte `212` — `f` final block flag (`0` or `1` only) Output is 64 bytes: the 8 updated state words in little-endian. - - ## Elixir Learning Notes - - - Cryptographic compression can be implemented in pure Elixir with - `import Bitwise` plus explicit 64-bit masking (`0xFFFFFFFFFFFFFFFF`) after - additions to preserve modulo `2^64` behavior. - - Binary pattern matching with little-endian fields (`<>`) is - ideal for parsing EIP-152 state/message/counter words directly from input. - - Tuple-based state management (`put_elem/3`, `elem/2`) is a practical fit for - fixed-size vectors like Blake2b's 16-word working vector. """ @behaviour EEVM.Precompile diff --git a/lib/eevm/precompiles/bn256.ex b/lib/eevm/precompiles/bn256.ex index e99309d..2bbefea 100644 --- a/lib/eevm/precompiles/bn256.ex +++ b/lib/eevm/precompiles/bn256.ex @@ -1,14 +1,7 @@ defmodule EEVM.Precompiles.BN256 do @moduledoc """ - BN256 (alt_bn128) curve precompiles — addresses `0x06`, `0x07`, `0x08`. - - ## EVM Concepts - - The alt_bn128 (also called BN254) is a pairing-friendly elliptic curve - introduced into the EVM by EIP-196 and EIP-197. These precompiles enable - efficient on-chain verification of **zero-knowledge proofs** (notably Groth16 - and PLONK), which underpin zkRollups like zkSync and Polygon zkEVM, as well as - privacy protocols and decentralized identity systems. + BN256 (alt_bn128) curve precompiles — addresses `0x06`, `0x07`, `0x08` + (EIP-196, EIP-197). Three operations are exposed: @@ -37,16 +30,6 @@ defmodule EEVM.Precompiles.BN256 do Input length must be divisible by 192. The identity/infinity point is encoded as `(0, 0)`. - - ## Elixir Learning Notes - - - The `bn` hex package provides pure-Elixir BN128 field arithmetic - (`BN.FQ`, `BN.FQ2`) and curve operations (`BN.BN128Arithmetic`, - `BN.Pairing`), so no NIF compilation is required. - - Points are represented as `{BN.FQ.t(), BN.FQ.t()}` tuples for G1 and - `{BN.FQP.t(), BN.FQP.t()}` tuples for G2. - - `:binary.decode_unsigned/1` converts big-endian binaries to Elixir's - arbitrary-precision integers for coordinate parsing. """ alias BN.{BN128Arithmetic, FQ, FQ2, Pairing} diff --git a/lib/eevm/precompiles/ecrecover.ex b/lib/eevm/precompiles/ecrecover.ex index c7c050d..483f931 100644 --- a/lib/eevm/precompiles/ecrecover.ex +++ b/lib/eevm/precompiles/ecrecover.ex @@ -2,37 +2,27 @@ defmodule EEVM.Precompiles.ECRecover do @moduledoc """ ECRecover precompile — address `0x01`. - ## EVM Concepts - - `ecrecover` is the canonical on-chain signature verification primitive. It takes - a message hash and ECDSA signature tuple `(v, r, s)`, recovers the signer public - key, and converts it into an Ethereum address. Solidity exposes this directly via - `ecrecover(...)`, and higher-level standards like **EIP-712 typed data** and - permit-style token approvals depend on it heavily. - - The precompile does not return a boolean validity flag. Instead, callers infer - validity from output shape: successful recovery returns an address; invalid - signature data returns empty bytes. + Takes a message hash and ECDSA signature tuple `(v, r, s)`, recovers the + signer public key, and converts it into an Ethereum address. Callers infer + validity from output shape: successful recovery returns an address; + invalid signature data returns empty bytes. ### Gas schedule (Yellow Paper §E.1) Flat `3000` gas, independent of input size. + ### Input format + + Parsed as a normalized 128-byte frame `hash(32) || v(32) || r(32) || s(32)`; + shorter input is right-padded with zeros. + ### Output format On success, returns a 32-byte word containing the recovered 20-byte address left-padded with 12 zero bytes. - On failure (invalid `v/r/s`, invalid signature, or recovery failure), returns an - empty binary `<<>>` while still consuming the fixed 3000 gas. - - ## Elixir Learning Notes - - - Erlang's `:crypto` supports secp256k1 primitives but does not provide a direct - `ec_recover` operation; this module uses `ExSecp256k1.recover_compact/3`. - - The input layout is parsed with binary pattern matching over a normalized - 128-byte frame: `hash(32) || v(32) || r(32) || s(32)`. - - Ethereum addresses are derived as `keccak256(pubkey_without_0x04)[12..31]`. + On failure (invalid `v/r/s`, invalid signature, or recovery failure), + returns an empty binary `<<>>` while still consuming the fixed 3000 gas. """ @behaviour EEVM.Precompile diff --git a/lib/eevm/precompiles/identity.ex b/lib/eevm/precompiles/identity.ex index 978fe79..64de5db 100644 --- a/lib/eevm/precompiles/identity.ex +++ b/lib/eevm/precompiles/identity.ex @@ -1,22 +1,6 @@ defmodule EEVM.Precompiles.Identity do @moduledoc """ - Identity precompile — address `0x04`. - - ## EVM Concepts - - The identity precompile is the simplest of the built-in contracts: it returns - its input bytes unchanged. Despite that triviality, it is one of the most - frequently called precompiles on mainnet because **Solidity uses it to copy - memory regions via an inline `CALL`**. The `memcpy` assembly pattern in many - contracts looks like: - - ``` - assembly { pop(call(gas(), 0x04, 0, src, len, dst, len)) } - ``` - - This works because CALL writes the return data directly into the specified - output memory range, giving Solidity a cheap, predictable copy primitive that - does not require a custom opcode. + Identity precompile — address `0x04`. Returns its input bytes unchanged. ### Gas schedule (Yellow Paper §E.2) @@ -26,15 +10,6 @@ defmodule EEVM.Precompiles.Identity do | Per word | 3 (one word = 32 bytes, rounded up) | An empty input costs the 15-gas base with no word component. - - ## Elixir Learning Notes - - - `div(n + 31, 32)` is the idiomatic ceiling-division-by-32 pattern. Adding - `(divisor - 1)` before integer-dividing avoids a conditional while keeping - the expression branchless. - - The function returns a tagged tuple — `{:ok, output, gas_used}` on success - and `{:error, :out_of_gas}` on failure — following the same convention used - throughout the opcode modules. """ @behaviour EEVM.Precompile diff --git a/lib/eevm/precompiles/kzg_point_eval.ex b/lib/eevm/precompiles/kzg_point_eval.ex index 5fe4d1b..06b3735 100644 --- a/lib/eevm/precompiles/kzg_point_eval.ex +++ b/lib/eevm/precompiles/kzg_point_eval.ex @@ -2,14 +2,7 @@ defmodule EEVM.Precompiles.KZGPointEval do @moduledoc """ KZG point evaluation precompile — address `0x0A` (EIP-4844). - ## EVM Concepts - - EIP-4844 (proto-danksharding) introduces **blob transactions** where large data - blobs are carried outside normal calldata and represented on-chain by compact - **KZG commitments**. Contracts do not process full blobs directly; they verify - evaluation claims against commitments. - - This precompile performs that evaluation check flow: + The precompile flow: 1. Validate the `versioned_hash` derived from the commitment. 2. Validate field element bounds for the evaluation point `z` and value `y`. @@ -34,14 +27,6 @@ defmodule EEVM.Precompiles.KZGPointEval do - `FIELD_ELEMENTS_PER_BLOB` (`4096`) - `BLS_MODULUS` - ## Elixir Learning Notes - - - Binary pattern matching (`<<...>>`) gives direct fixed-offset parsing with no - manual indexing. - - `:binary.decode_unsigned/1` converts 32-byte big-endian field elements to - arbitrary-precision Elixir integers for range checks. - - `:crypto.hash(:sha256, commitment)` computes the versioned hash preimage. - ### Verification implementation note Full KZG proof verification requires trusted setup parameters and a native diff --git a/lib/eevm/precompiles/modexp.ex b/lib/eevm/precompiles/modexp.ex index 2b15229..2b3cdb0 100644 --- a/lib/eevm/precompiles/modexp.ex +++ b/lib/eevm/precompiles/modexp.ex @@ -1,24 +1,11 @@ defmodule EEVM.Precompiles.ModExp do @moduledoc """ - Modular exponentiation precompile — address `0x05`. + Modular exponentiation precompile — address `0x05` (EIP-198). - ## EVM Concepts - - ModExp computes `base^exp mod modulus` over arbitrarily large integers. It was - introduced by EIP-198 so contracts can perform heavy number-theoretic - operations without reimplementing bignum arithmetic in EVM bytecode. - - This precompile is a core primitive for: - - - **RSA signature verification**, where exponentiation modulo a large integer - is the fundamental operation. - - **Zero-knowledge proofs**, whose verifier circuits frequently require - modular exponentiation over large fields. + Computes `base^exp mod modulus` over arbitrarily large integers. ### Gas schedule (EIP-2565) - The gas charge uses the EIP-2565 repricing formula: - `gas = max(200, floor(multiplication_complexity * iteration_count / 3))` where: @@ -35,8 +22,6 @@ defmodule EEVM.Precompiles.ModExp do ### Input format - Input is ABI-like (but not standard Solidity ABI): - - bytes `0..31`: `base_length` (big-endian uint256) - bytes `32..63`: `exp_length` (big-endian uint256) - bytes `64..95`: `mod_length` (big-endian uint256) @@ -48,15 +33,6 @@ defmodule EEVM.Precompiles.ModExp do Output is the big-endian value of `base^exp mod modulus`, left-padded with zeros to exactly `mod_length` bytes. If `modulus == 0` or `mod_length == 0`, the precompile returns `mod_length` zero bytes. - - ## Elixir Learning Notes - - - `:crypto.mod_pow(base, exp, modulus)` delegates modular exponentiation to - Erlang/OTP's `:crypto` NIF, which uses optimized native big-integer code. - - `<>` is binary pattern matching for parsing - big-endian uint256 fields directly from a binary. - - Elixir integers are arbitrary precision, so converting between bytes and - integers with `:binary.decode_unsigned/1` works for very large values. """ @behaviour EEVM.Precompile diff --git a/lib/eevm/precompiles/ripemd160.ex b/lib/eevm/precompiles/ripemd160.ex index ca39d81..47866d0 100644 --- a/lib/eevm/precompiles/ripemd160.ex +++ b/lib/eevm/precompiles/ripemd160.ex @@ -2,21 +2,11 @@ defmodule EEVM.Precompiles.RIPEMD160 do @moduledoc """ RIPEMD-160 precompile — address `0x03`. - ## EVM Concepts - - RIPEMD-160 is a 160-bit (20-byte) cryptographic hash function developed in - Europe as an alternative to SHA-1. Its primary use in Ethereum is - **Bitcoin address derivation**: a Bitcoin address is `RIPEMD160(SHA256(pubkey))`, - so on-chain Bitcoin light-client contracts need this precompile to reconstruct - and verify addresses without trusting off-chain computation. - ### Output format (Yellow Paper §E.2) Even though the digest is only 20 bytes, the precompile **always returns 32 bytes**: the 20-byte hash is **right-aligned** within the 32-byte word, - with 12 zero bytes prepended on the left. This matches the EVM's natural - word size and lets callers use the result directly as a `bytes32` or `address` - value without extra masking. + with 12 zero bytes prepended on the left. ``` [ 0x00 × 12 | RIPEMD160(input) × 20 ] @@ -29,17 +19,6 @@ defmodule EEVM.Precompiles.RIPEMD160 do |-----------|------| | Base | 600 | | Per word | 120 (one word = 32 bytes, rounded up) | - - The higher base cost reflects the fact that RIPEMD-160 is slower than both - Keccak-256 and SHA-256 in typical hardware implementations. - - ## Elixir Learning Notes - - - `:crypto.hash(:ripemd160, data)` produces a 20-byte binary. Erlang's - `:crypto` NIF supports RIPEMD-160 via OpenSSL. - - `<<0::96, hash::binary>>` is binary construction syntax. `0::96` emits - 96 zero bits (= 12 zero bytes), and `hash::binary` appends the raw hash - bytes, yielding the required 32-byte output in one expression. """ @behaviour EEVM.Precompile diff --git a/lib/eevm/precompiles/sha256.ex b/lib/eevm/precompiles/sha256.ex index 856be32..ad3cd8e 100644 --- a/lib/eevm/precompiles/sha256.ex +++ b/lib/eevm/precompiles/sha256.ex @@ -2,21 +2,6 @@ defmodule EEVM.Precompiles.SHA256 do @moduledoc """ SHA-256 precompile — address `0x02`. - ## EVM Concepts - - SHA-256 is the second built-in contract and the only precompile that uses a - different hash function than the EVM's native Keccak-256. It is included - primarily for **Bitcoin interoperability**: Bitcoin addresses and block headers - are SHA-256 based, so on-chain verification of Bitcoin proofs requires access - to the function. - - SHA-256 also appears in: - - - **EIP-197 (BN256)** pairing checks, whose inputs are sometimes hashed with - SHA-256 before being passed on-chain. - - **Beacon chain / consensus layer** — the Ethereum consensus layer uses - SHA-256 (not Keccak) for its Merkle tree. - ### Gas schedule (Yellow Paper §E.2) | Component | Cost | @@ -27,14 +12,6 @@ defmodule EEVM.Precompiles.SHA256 do ### Output format Always 32 bytes — the raw SHA-256 digest. - - ## Elixir Learning Notes - - - `:crypto.hash(:sha256, data)` calls into Erlang's OpenSSL-backed `:crypto` - NIF (native implemented function). It is significantly faster than a pure - Elixir implementation and requires no extra dependencies. - - The atom `:sha256` maps to OpenSSL's `EVP_sha256` digest under the hood; - Erlang exposes the full OpenSSL digest catalogue this way. """ @behaviour EEVM.Precompile diff --git a/lib/eevm/storage.ex b/lib/eevm/storage.ex index 02186a1..0fc6d70 100644 --- a/lib/eevm/storage.ex +++ b/lib/eevm/storage.ex @@ -1,46 +1,9 @@ defmodule EEVM.Storage do @moduledoc """ - Persistent key-value storage for the EVM. + Persistent key-value storage for a single contract account: a mapping from + 256-bit slot keys to 256-bit values. - ## EVM Concepts - - Storage is the EVM's persistent state — it survives across transactions and - blocks. Each contract (account) has its own isolated storage, a mapping from - 256-bit keys ("slots") to 256-bit values. - - This is what makes Ethereum a *stateful* computer. When a Solidity contract - declares `uint256 public counter`, that variable lives in storage slot 0. - `SSTORE` writes to it, `SLOAD` reads from it. - - ### Storage vs Memory - - | Property | Storage | Memory | - |----------|---------|--------| - | Lifetime | Permanent (on-chain) | Cleared after each call | - | Cost | Very expensive (20,000 gas to write) | Cheap (3 gas + expansion) | - | Size | 2^256 slots per account | Grows dynamically | - | Access | Key-value (slot → value) | Byte-addressable (offset → byte) | - - ### Gas Costs (Simplified) - - In a production EVM, storage gas depends on cold/warm access (EIP-2929) and - whether you're writing to a fresh vs dirty slot (EIP-2200). We use simplified - flat costs for learning: - - - `SLOAD`: 200 gas (warm access cost) - - `SSTORE`: 20,000 gas (fresh write cost) - - The full EIP-2929 + EIP-2200 model tracks an "access set" per transaction and - distinguishes between original, current, and new values to compute refunds. - - ## Elixir Learning Notes - - - We use a plain `Map` as the underlying data structure. Elixir maps have - O(log n) access which is fine for our learning purposes. - - Uninitialized slots return 0 — we use `Map.get/3` with a default value - rather than checking for key existence. - - The module is purely functional: `store/3` returns a new storage, it doesn't - mutate the old one. This is a core Elixir/functional programming pattern. + Uninitialized slots return `0`. All values are masked to 256 bits on write. """ @type t :: %__MODULE__{ diff --git a/lib/eevm/system_contracts/beacon_roots.ex b/lib/eevm/system_contracts/beacon_roots.ex index 0829a68..baca6e2 100644 --- a/lib/eevm/system_contracts/beacon_roots.ex +++ b/lib/eevm/system_contracts/beacon_roots.ex @@ -1,21 +1,14 @@ defmodule EEVM.SystemContracts.BeaconRoots do @moduledoc """ - EIP-4788 beacon block root contract. + EIP-4788 beacon block root contract (activated in Cancun). - ## EVM Concepts - - Before Cancun the consensus-layer (CL) beacon block root was invisible to - the EVM. EIP-4788 bridges that gap with an ordinary contract deployed at a - well-known address: - - 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02 - - At the start of every block the execution client performs a *system call* - into this contract with the parent beacon-block root as calldata, signed as - coming from the SYSTEM_ADDRESS (`0xff..fe`). The contract keys the root by - the current block timestamp and stashes it in a ring buffer of - `HISTORY_BUFFER_LENGTH = 8191` slots. User contracts later `CALL` the same - address with a 32-byte timestamp to read back the root. + Installs the canonical EIP-4788 deployed bytecode at + `0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02` and drives it through the + normal executor. At the start of every block the execution layer performs + a system call (caller = SYSTEM_ADDRESS `0xff..fe`) with the parent + beacon-block root as calldata; the contract keys it by `block.timestamp` + in a ring buffer of `HISTORY_BUFFER_LENGTH = 8191` slots. User contracts + read past roots back via a plain `CALL` with a 32-byte timestamp. Storage layout (per the EIP): @@ -23,31 +16,16 @@ defmodule EEVM.SystemContracts.BeaconRoots do - `storage[(timestamp % 8191) + 8191]` = beacon_root On read, the contract verifies the timestamp slot matches the queried - timestamp (so collisions from the ring buffer return an empty result - instead of a stale root). - - ## Design + timestamp (so ring-buffer collisions return empty rather than a stale root). - We do not invent a custom precompile. Instead we install the exact deployed - bytecode specified by the EIP at the canonical address and drive it through - the normal executor — the same code path any CALL would take. This keeps - behavior faithful to mainnet and means any future changes to CALL semantics - come along for free. + ## API - `install/1` — place the deployed bytecode into a `Database`. - `commit/3` — perform the block-start system call that stores a root. - - `lookup/2` — read a stored root directly from storage (convenience). - - `commit/3` returns `{:ok, updated_db}` on success and `{:error, reason, db}` - on an execution failure; the caller decides what to do about it. - - ## Elixir Learning Notes - - - The deployed bytecode lives as a compile-time constant via `@deployed_code` - so there is no per-call decoding cost. - - `install/1` and `commit/3` both work on plain `Database` values — no - `MachineState` is exposed to callers — keeping the integration surface - small for higher layers that aren't yet aware of full EVM state. + Returns `{:ok, db}` on success and `{:error, reason, db}` on execution + failure. + - `lookup/2` — read a stored root directly from storage (`{:ok, root}` + when the timestamp slot matches, `:not_found` otherwise). """ alias EEVM.Context.{Block, Contract, Transaction} diff --git a/lib/eevm/system_contracts/block_hashes.ex b/lib/eevm/system_contracts/block_hashes.ex index 64ca9c5..b1b7ea4 100644 --- a/lib/eevm/system_contracts/block_hashes.ex +++ b/lib/eevm/system_contracts/block_hashes.ex @@ -1,71 +1,27 @@ defmodule EEVM.SystemContracts.BlockHashes do @moduledoc """ - EIP-2935 historical block hashes contract. + EIP-2935 historical block hashes contract (activated in Prague). - ## EVM Concepts + Installs the canonical EIP-2935 deployed bytecode at + `0x0000F90827F1C53a10cb7A02335B175320002935` and drives it through the + normal executor. At the start of every block the execution layer performs + a system call (caller = SYSTEM_ADDRESS `0xff..fe`) with the parent block + hash as 32-byte calldata; the contract stashes the hash in a ring buffer + of `HISTORY_SERVE_WINDOW = 8191` slots keyed by the parent block number. + User contracts read past hashes back via a plain `CALL` with a 32-byte + block number. - The `BLOCKHASH` opcode can only address the previous 256 ancestors of the - current block. That 256-block window is a protocol-imposed limit that keeps - clients from having to retain arbitrarily-deep history in order to execute - contracts. EIP-2935 extends the reachable history to the last 8191 blocks - without touching the opcode itself — instead, an ordinary contract deployed - at - - 0x0000F90827F1C53a10cb7A02335B175320002935 - - exposes the hashes through a plain `CALL`. The `BLOCKHASH` opcode's rules - are unchanged; dapps that want deeper history do so explicitly by CALLing - this address. - - At the start of every block the execution client performs a *system call* - into the contract with the parent block hash as 32-byte calldata, signed as - coming from the SYSTEM_ADDRESS (`0xff..fe`). The contract notices the - system caller and stashes the hash in a ring buffer slot keyed by the - parent block number: - - storage[(block.number - 1) % 8191] = parent_block_hash - - User contracts later `CALL` the same address with a 32-byte block number - as calldata. The bytecode validates that the calldata is exactly 32 bytes - and that the requested block is strictly in the past - (`requested < block.number`); otherwise it reverts. When the upper-bound - check passes the contract returns `storage[requested mod 8191]`. The - EIP-canonical bytecode does *not* additionally enforce a lower bound — a - request that ranges further back than 8191 blocks will simply return - whatever stale entry currently sits in that ring-buffer slot. - - Activated in Prague. - - ## Design - - We do not invent a custom precompile. Instead we install the exact deployed - bytecode specified by EIP-2935 at the canonical address and drive it - through the normal executor — the same code path any user CALL takes. This - keeps behavior faithful to mainnet and means any future changes to CALL - semantics come along for free. + ## API - `install/1` — place the deployed bytecode into a `Database`. - `commit/3` — perform the block-start system call that stores a hash. - - `lookup/2` — read a stored hash directly from storage (convenience). - - `commit/3` takes the *current* block (whose `number` is, say, N) plus the - hash of its parent (block N-1). The contract does the `N - 1` subtraction - internally before writing to `storage[(N - 1) mod 8191]`, so the caller - never has to know about the ring-buffer offset. - - `commit/3` returns `{:ok, updated_db}` on success and `{:error, reason, db}` - on an execution failure; the caller decides what to do about it. - - ## Elixir Learning Notes - - - The deployed bytecode lives as a compile-time constant via `@deployed_code` - so there is no per-call decoding cost. - - `install/1` and `commit/3` both work on plain `Database` values — no - `MachineState` is exposed to callers — keeping the integration surface - small for higher layers that aren't yet aware of full EVM state. - - `lookup/2` reads storage directly: a slot that has never been written - returns `:not_found`, distinguishable from a genuinely-stored zero only - if the caller knows the slot was written. + Takes the *current* block (number N) plus the hash of its parent + (block N-1); the contract does the `N - 1` subtraction internally + before writing to `storage[(N - 1) mod 8191]`. Returns `{:ok, db}` + on success and `{:error, reason, db}` on execution failure. + - `lookup/2` — read a stored hash directly from storage. A slot that + has never been written returns `:not_found` (distinguishable from a + genuinely-stored zero only if the caller knows the slot was written). """ alias EEVM.Context.{Block, Contract, Transaction} diff --git a/lib/eevm/transaction/intrinsic_gas.ex b/lib/eevm/transaction/intrinsic_gas.ex index a3ed1b4..2319c33 100644 --- a/lib/eevm/transaction/intrinsic_gas.ex +++ b/lib/eevm/transaction/intrinsic_gas.ex @@ -2,18 +2,9 @@ defmodule EEVM.Transaction.IntrinsicGas do @moduledoc """ Intrinsic transaction gas calculation. - ## EVM Concepts - - Before EVM bytecode starts executing, every transaction pays an upfront - intrinsic gas cost. This covers the base transaction envelope plus static - costs derived from calldata, contract creation, and access-list entries. - - This module implements the cost components needed by transaction validation. - - ## Elixir Learning Notes - - - `Enum.reduce/3` is used to accumulate byte and access-list costs. - - We compute `ceil(n / 32)` with integer arithmetic as `div(n + 31, 32)`. + Covers the base transaction envelope plus static costs derived from + calldata, contract creation, and access-list entries — the upfront gas + charged before EVM execution begins. """ alias EEVM.Context.Transaction diff --git a/lib/eevm/transaction/validator.ex b/lib/eevm/transaction/validator.ex index 8d98e07..b4b1737 100644 --- a/lib/eevm/transaction/validator.ex +++ b/lib/eevm/transaction/validator.ex @@ -2,17 +2,8 @@ defmodule EEVM.Transaction.Validator do @moduledoc """ Transaction pre-execution validation. - ## EVM Concepts - - Before running bytecode, Ethereum clients validate transaction envelope and - sender constraints against current state and block context. This module - performs those checks in deterministic order and returns the first failure. - - ## Elixir Learning Notes - - - Validation is modeled as a pipeline of `with` steps returning `:ok` or - `{:error, reason}`. - - Small private helpers keep each rule isolated and testable. + Performs envelope and sender checks in deterministic order against current + state and block context, returning `:ok` or the first `{:error, reason}`. """ alias EEVM.Context.{Block, Transaction} diff --git a/lib/eevm/transaction_result.ex b/lib/eevm/transaction_result.ex index 61fcf54..0f1c90c 100644 --- a/lib/eevm/transaction_result.ex +++ b/lib/eevm/transaction_result.ex @@ -1,23 +1,13 @@ defmodule EEVM.TransactionResult do @moduledoc """ - Result of running a single transaction through the end-to-end execution pipeline. + Result of running a single transaction through the end-to-end execution + pipeline. - ## EVM Concepts - - Every Ethereum transaction that passes validation produces three things: - - - A **receipt** — status flag, cumulative gas used, logs, and logs bloom — - which is what nodes hash into the block's receipts trie. See Yellow Paper - §4.3.1. - - A **post-state database** — the world state after the transaction's side - effects (balance moves, storage writes, nonce bumps, code deployment). - - **Return data / contract address** — the raw output of the top-level call, - or the newly deployed address for a CREATE transaction (`to == nil`). - - This struct bundles those outputs for consumers (test harnesses, block - executors, RPC layers). Transactions that fail validation *before* execution - never produce a `TransactionResult` — the pipeline returns `{:error, reason}` - in that case and the caller is expected not to apply any state changes. + Bundles the outputs a caller needs after `EEVM.Handler.execute/4` returns: + the receipt (status, cumulative gas, logs, logs bloom), the post-state + database, and the top-level return data / newly created contract address. + Transactions that fail pre-execution validation never produce a + `TransactionResult` — the pipeline returns `{:error, reason}` in that case. ### `status` values @@ -28,15 +18,6 @@ defmodule EEVM.TransactionResult do - `:failed_validation` — used internally by helpers that need to describe a pre-execution failure in the same shape; the pipeline itself returns `{:error, reason}` for these cases rather than a struct. - - ## Elixir Learning Notes - - - Using an atom tag (`:success | :reverted | :failed_validation`) for the - result status lets consumers pattern match on success vs. failure without - poking at numeric receipt flags. - - The `receipt` field is a plain map (not a nested struct) because the - Ethereum receipt shape is small and stable — a map keeps construction - cheap and keeps serializers flexible. """ alias EEVM.Block.Bloom diff --git a/lib/eevm/world_state.ex b/lib/eevm/world_state.ex index b82f195..9e933a1 100644 --- a/lib/eevm/world_state.ex +++ b/lib/eevm/world_state.ex @@ -1,29 +1,11 @@ defmodule EEVM.WorldState do @moduledoc """ - Minimal world/account state used for external account lookups. + Minimal world/account state used for external account lookups + (`EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`). - ## EVM Concepts - - The EVM distinguishes between the current execution frame and global account state. - `EEVM.Interpreter.MachineState` stores frame-local execution data (pc, stack, memory), while - this module stores account-level data used by external inspection opcodes: - - - `EXTCODESIZE` (0x3B) - - `EXTCODECOPY` (0x3C) - - `EXTCODEHASH` (0x3F) - - The state is intentionally minimal for this learning implementation. Each account - can include `:balance`, `:nonce`, `:code`, and `:storage`. Missing accounts behave - like non-existent EVM accounts: no code and zero balance. - - ## Elixir Learning Notes - - - The world state is represented as a struct wrapping a map keyed by numeric - addresses. - - Optional map keys let account records stay lightweight while preserving a - clear shape via typespecs. - - `Map.get/3` gives spec-friendly defaults (`<<>>` for code, `0` for balance) - without introducing special sentinel values. + Each account can include `:balance`, `:nonce`, `:code`, and `:storage`. + Missing accounts behave like non-existent EVM accounts: no code and + zero balance. """ alias EEVM.Storage