Skip to content
Open
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
20 changes: 4 additions & 16 deletions lib/eevm/block/bloom.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
23 changes: 2 additions & 21 deletions lib/eevm/block/header.ex
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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__{
Expand Down
37 changes: 2 additions & 35 deletions lib/eevm/block/processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand All @@ -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}
Expand Down
20 changes: 7 additions & 13 deletions lib/eevm/block/receipt.ex
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
18 changes: 4 additions & 14 deletions lib/eevm/block/result.ex
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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}
Expand Down
17 changes: 3 additions & 14 deletions lib/eevm/block/withdrawal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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__{
Expand Down
17 changes: 2 additions & 15 deletions lib/eevm/context/block.ex
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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__{
Expand Down
20 changes: 2 additions & 18 deletions lib/eevm/context/contract.ex
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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__{
Expand Down
17 changes: 2 additions & 15 deletions lib/eevm/context/transaction.ex
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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__{
Expand Down
37 changes: 6 additions & 31 deletions lib/eevm/database.ex
Original file line number Diff line number Diff line change
@@ -1,43 +1,18 @@
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:

- `impl` — the module implementing `@behaviour EEVM.Database`
- `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 :: %{
Expand Down
21 changes: 3 additions & 18 deletions lib/eevm/database/in_memory.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading