Skip to content
← Reading the Stack — Bridge to Intermediate
Block-level Ethereum
Lesson 4 of 10·CONTENT12 min25 XP
Course
Reading the Stack — Bridge to Intermediate
Lesson role
CONTENT
Sequence
4 / 10

Lesson 3 — Blocks, receipts, and reorgs

Question

You've seen tx internals. One layer up = blocks. A block is a sequence of txs + header + receipts + state root. Reorgs are how recently-mined blocks get discarded and replaced — the biggest source of complexity in L1 sync code.

Principle (minimum model)

  • Block structure. Header (parent hash + state root + transactions root + receipts root + logs bloom + timestamp + number + gas used + gas limit + ...) + Body (tx list + ommers, always empty since the Merge + withdrawals since Shanghai — alloy's BlockBody { transactions, ommers, withdrawals }). Receipts are produced by executing the body; the block only commits to them via receiptsRoot.
  • State root. The root of the world-state MPT: one leaf per account (nonce, balance, storage root, code hash); each contract's storage lives in its own trie whose root sits in the account. Changes before/after block execution; validators verify independently.
  • Receipts root. The MPT root of every tx's result (status / gas used / logs / events). Makes tx results provable.
  • Reorg = swap-out of a not-yet-finalized block. In post-Merge PoS, reorgs are almost always 1–2 blocks deep; finalized blocks (≈2 epochs, ~13 minutes behind head) can't be reverted without at least 1/3 of stake being slashed.
  • Reorg behaviour. Undo old-chain state changes → apply new-chain blocks. ExEx subscribers get ExExNotification::ChainReorged { old, new }. Reth keeps recent blocks (including side chains) in memory in the engine tree, so a tip reorg usually doesn't touch disk.
  • Three finality levels. latest (head, can reorg) / safe (latest justified checkpoint, typically ~1–2 epochs = 32–64 blocks back) / finalized (irreversible, ~2 epochs ≈ 13 minutes back). Pick the right level for the use case.
  • Block-level data exposed to txs. block.number / block.timestamp / block.coinbase / block.basefee — read by EVM during execution.
  • Reth's reorg handling. ExExNotification has three variants (ChainCommitted { new } / ChainReorged { old, new } / ChainReverted { old }, crates/exex/types/src/notification.rs); indexers must handle all three.

Worked example + steps

Blocks, receipts, and reorgs

You've worked with one transaction at a time. The chain operates at a different level: blocks of transactions, receipts of what they did, and the occasional reorg when the chain rewrites recent history.

This is the layer Intermediate lessons on Reth's Staged Sync and ExEx assume you understand.

A block — three pieces

Every Ethereum block is conceptually three things bundled together:

PieceWhat it containsHashing
HeaderMetadata: parent hash, state root, tx root, receipts root, gas limit, timestamp, etc.The block's "identity" hash is keccak256(header)
BodyThe actual list of transactions, orderedTx root in header is the Merkle root of this list
ReceiptsOne receipt per transaction: status, cumulative gas used, logs/events emitted. Not shipped with the block — every node recomputes them by executing the bodyReceipts root in header commits to this list

When you hear "block hash 0x123...", that's keccak256 of the RLP-encoded header — the body and receipts are committed to via the header's roots but aren't part of the hash directly.

Why three roots in the header?

The header carries three Merkle roots: state, transactions, receipts. Each root commits to a different piece of the chain's data:

  • state_root: the root of the world-state MPT after this block's transactions executed
  • transactions_root: the root of an MPT of all txs in this block (so you can prove "tx X was in block N")
  • receipts_root: the root of an MPT of all receipts (so you can prove "tx X emitted log Y")

A light client can verify any of these by holding only the header and walking a proof. (You'll see this in Expert.)

Receipts and logs — the audit trail

Each transaction produces a receipt:

// Reth's Receipt = alloy_consensus::EthereumReceipt (alloy-consensus 1.8.3)
struct EthereumReceipt {
    tx_type: TxType,
    success: bool,          // success / failed
    cumulative_gas_used: u64, // running total in the block, not this tx alone
    logs: Vec<Log>,
}
// The 256-byte bloom is computed from the logs (ReceiptWithBloom) when the
// receipt is encoded for receiptsRoot; it isn't a stored field here.

struct Log {
    address: Address,       // emitting contract
    topics: Vec<B256>,      // up to 4, indexed
    data: Bytes,            // unindexed payload
}

Two things matter:

1. Logs are how Solidity events reach off-chain

When you write emit Transfer(from, to, amount) in Solidity, the EVM executes the LOG3 opcode with:

  • topic[0] = keccak256("Transfer(address,address,uint256)") — the event signature
  • topic[1] = from (indexed param)
  • topic[2] = to (indexed param)
  • data = ABI-encoded amount (not indexed)

The log goes into the receipt. Indexers, MEV bots, and ExEx all consume these.

2. The bloom filter is a fast "did this block touch X?" check

Each receipt has a 2048-bit (256-byte) bloom filter over its log addresses and topics, and the block header's logsBloom is the OR of all of them. Light clients and RPC log filters use it to quickly skip blocks that can't mention an address they care about — without downloading the full receipts. (A bloom can give false positives, never false negatives.)

An ExEx already receives the executed receipts in its notification, so it filters logs directly; the bloom matters most for eth_getLogs-style scans over many blocks.

How a block actually gets built

A typical block lifecycle:

1. Block proposer (validator) gets selected for slot N
2. Proposer collects pending txs from mempool (or builders)
3. For each tx (in order):
   a. Open a frame, run the EVM
   b. Update world state on success, revert on failure
   c. Append receipt
4. Compute roots: state_root, transactions_root, receipts_root
5. Compute bloom filter from logs
6. Build header with all roots + parent_hash + timestamp + ...
7. Sign and propagate

The full node executing this block performs the same execution to verify — it gets the body, runs every tx, and checks that the resulting state_root matches the header. If it doesn't, the block is rejected.

When you read Reth's ExecutionStage in the Reth source-reading course's Staged Sync lessons, that's this verification path during sync: ExecutionStage replays each block's transactions and accumulates state changes, and the later MerkleStage checks the resulting state root. (At the live tip the engine tree does the same per block.)

Reorgs — the chain rewriting itself

Most of the time the chain extends linearly:

... → block 100 → block 101 → block 102 → block 103

But sometimes two validators propose blocks for the same slot, or a network partition heals, and the canonical chain switches. A few blocks at the tip get unwound, and the chain re-extends along a different path:

                                    ┌─→ 102b → 103b   (new canonical)
... → 100 → 101 → 102a → 103a ──────┘
                  └────────── unwound (no longer canonical)

This is a reorg. From the node's perspective:

  1. The consensus client's fork choice (LMD-GHOST, weighted by attestations) picks a new head and tells the execution client via engine_forkchoiceUpdated
  2. Walk back to the common ancestor (block 101 in the example)
  3. Unwind state changes from 102a, 103a (in reverse order)
  4. Apply state changes from 102b, 103b
  5. New canonical tip is 103b

In modern Ethereum (post-Merge PoS), reorgs deeper than 1-2 blocks are rare but happen. The typical case is a 1-block reorg of a block that was published too late to collect enough attestations.

What this means for off-chain consumers

If you wrote an indexer that listened to "block N committed, write txs to my DB," and then block N got reorged:

  • Your DB now has rows for transactions that never happened on the canonical chain
  • You need to delete those rows when the reorg is detected
  • Then re-insert rows for the new canonical block N's transactions

This is the reason ExEx has three notification types — ChainCommitted, ChainReorged, ChainReverted. A naive indexer that handles only ChainCommitted corrupts its derived state on every reorg. (The Reth source-reading course's ExEx lessons walk through this in detail.)

Why Reth's Staged Sync is symmetric

Every Reth Stage has execute (forward) and unwind (backward) on the same Stage trait (crates/stages/api/src/stage.rs). Rewinding is a normal mode of operation, not a special case: the pipeline (crates/stages/api/src/pipeline/mod.rs) calls every stage's unwind in reverse order to roll persisted data back to a target block — when its local head turns out to be on a different fork than the sync target ("detached head"), when a stage hits a bad block, or when asked explicitly (reth stage unwind).

At the live tip, Reth handles most reorgs one level up, in the engine tree: recent blocks are held in memory, on_new_head detects the fork and builds NewCanonicalChain::Reorg { new, old } (crates/engine/tree/src/tree/mod.rs), and only if old-chain blocks were already persisted does find_disk_reorg → remove_blocks remove them from disk.

This is a design choice you'll appreciate in the Reth source-reading course's Staged Sync lessons.

Reading list

  1. Dump a real block header: cast block latest --rpc-url <mainnet RPC> (or against a local anvil). Find: parentHash, stateRoot, transactionsRoot, receiptsRoot, logsBloom (also withdrawalsRoot, parentBeaconRoot, requestsHash on post-Prague blocks).
  2. Open one of its transactions, look at the Logs tab. Each log has an Address, Topics, and Data — that's the structure above.
  3. For a sense of how reorgs look in production, search "reorg" in Reth's release notes or any Ethereum client's release notes — operators care a lot about reorg-handling correctness.

What you should walk away with

  • A block is header + body; receipts come from executing it. The header carries three Merkle roots committing to state, txs, and receipts.
  • Receipts capture each tx's status, gas, and logs (Solidity events become logs via the LOG opcodes).
  • Reorgs rewrite recent history; off-chain consumers must handle the rewind explicitly.
  • Reth's Staged Sync is symmetric (execute / unwind) because rewinding is normal, not exceptional; tip reorgs are mostly handled in memory by the engine tree.

When the Reth source-reading course walks through the Stage trait and the ExEx notification types, you'll already have the model in your head. You'll just be reading the Rust that implements it.

Summary (3 lines)

  • Block = Header + Body (txs + withdrawals); receipts are derived by execution. State / transactions / receipts roots are MPT roots in the header; validators verify independently.
  • Reorgs swap out not-yet-finalized blocks (usually 1–2 deep); finalized (~13 minutes back) is irreversible. Reth ExEx emits three variants (ChainCommitted / ChainReorged / ChainReverted).
  • Three finality levels (latest / safe / finalized). Indexers must handle reorgs. Next module: Rust for source-reading — the migration map from Solidity.