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 + receipts root + timestamp + number + gas used + gas limit + ...) + Body (tx list + uncles) + Receipts (tx results).
- State root. The MPT root hash of every contract's Storage + every EOA's balance. 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 shallowly-confirmed block. PoS allows reorgs roughly within 2–3 blocks;
finalizedblocks (≈12 minutes) are irreversible. - Reorg behaviour. Undo old-chain txs → re-execute new-chain txs. Subscribers get a
ChainReorgedevent. The state DB must hold both views temporarily. - Three finality levels.
latest(unconfirmed) /safe(~32 blocks back, likely-final) /finalized(irreversible, ~12 minutes). 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. ExEx (Execution Extension) emits three variants (Committed / ChainReorged / Reverted); 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:
| Piece | What it contains | Hashing |
|---|---|---|
| Header | Metadata: parent hash, state root, tx root, receipts root, gas limit, timestamp, etc. | The block's "identity" hash is keccak256(header) |
| Body | The actual list of transactions, ordered | Tx root in header is the Merkle root of this list |
| Receipts | One receipt per transaction: status, gas used, logs/events emitted | Receipts root in header commits to this list |
When you hear "block hash 0x123...", that's keccak256 of the header — the body and receipts are committed to via the header 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 executedtransactions_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:
struct Receipt {
status: bool, // success / failed
cumulative_gas_used: u64,
logs: Vec<Log>,
bloom: BloomFilter, // log filter for fast searches
}
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 block's receipts root has a 256-byte bloom filter that summarizes all log addresses and topics in the block. Light clients and indexers use it to quickly skip blocks that don't mention an address they care about — without downloading the full receipts.
When Intermediate lesson 7 (MEV in practice) shows you ExEx code that filters logs by address, this bloom is what makes the pre-filter fast.
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 Intermediate lesson 4, that's exactly the verification path: replay each block's transactions, accumulate state changes, validate the resulting root.
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:
- New chain segment arrives, longer or with more attestations than current
- Walk back to the common ancestor (block 101 in the example)
- Unwind state changes from 102a, 103a (in reverse order)
- Apply state changes from 102b, 103b
- New canonical tip is 103b
In modern Ethereum (post-Merge PoS), reorgs deeper than 1-2 blocks are rare but happen. Validators reorganize around missed proposals or equivocations.
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. (Intermediate lesson 6 walks through this in detail.)
Why Reth's Staged Sync is symmetric
Every Reth Stage has execute (forward) and unwind (backward). The stages aren't designed for reorgs as a "special case" — reorgs are a normal mode of operation, modeled with the same trait. Going forward 1000 blocks: execute. Going back 3 blocks for a reorg: unwind. Same code path, opposite direction.
This is a design choice you'll appreciate in Intermediate lesson 4 (Staged Sync architecture).
Reading list
- Find a real block on Etherscan. Click "Click to see More" on the header to see all the fields. Find: parentHash, stateRoot, transactionsRoot, receiptsRoot, logsBloom.
- Open one of its transactions, look at the Logs tab. Each log has an Address, Topics, and Data — that's the structure above.
- For a sense of how reorgs look in production, search "reorg" on reth.rs blog or any Ethereum client release notes — operators care a lot about reorg-handling correctness.
What you should walk away with
- A block is header + body + receipts; 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) precisely because reorgs are normal, not exceptional.
When Intermediate lesson 4 walks through the Stage trait and lesson 6 walks through 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 (tx list) + Receipts. State root + receipts root are MPT roots in the header; validators verify independently.
- Reorgs swap out shallowly-confirmed blocks; PoS allows within ~2–3 blocks; finalized (~12 minutes) is irreversible. Reth ExEx emits three variants (Committed / ChainReorged / Reverted).
- Three finality levels (latest / safe / finalized). Indexers must handle reorgs. Next module: Rust for source-reading — the migration map from Solidity.