レッスン3 — ブロック・レシート・reorg
問い
tx の中身はこれまで見た。1 段上 = ブロック。ブロックは tx の列 + header + receipts + state root。reorg は信頼の浅いブロックが捨てられて別のチェーンに置き換わる現象、L1 同期コードの最大の複雑性源。
原理(最小モデル)
- ブロック構造. Header(親ハッシュ / state root / receipts root / timestamp / number / gas used / gas limit 等)+ Body(tx 列 + uncles)+ Receipts(tx 結果)。
- State root. 全コントラクトの Storage + EOA 残高の MPT ルートハッシュ、ブロック実行前後で変化、validator が独立に検証可能。
- Receipts root. 各 tx の結果(status / gas used / logs / events)の MPT ルート、tx の結果が証明可能。
- Reorg = 信頼の浅いブロックの差し替え. PoS では概ね 2-3 ブロック以内が reorg 可能、
finalizedブロックは reorg 不可(~12 ブロック後)。 - Reorg 時の挙動. Old chain の tx を巻き戻し → new chain の tx を再実行、subscriber には
ChainReorgedイベント、state DB は old/new 両方の view を持つ必要。 - Finality 3 段.
latest(未確認)/safe(~32 ブロック前、likely finalize)/finalized(不可逆、~12 分後)= 開発時にどの段で読むか。 - Block-level data 例.
block.number/block.timestamp/block.coinbase/block.basefee、tx で読める EVM 露出データ。 - Reth の reorg 対応. ExEx(Execution Extension)通知 で 3 バリアント(Committed / ChainReorged / Reverted)、indexer は 3 つ全部処理する必要。
具体例 + ステップで組み立てる
ブロック・レシート・reorg
ここまで 1 トランザクションずつ扱ってきました。チェーンは別のレベルで動きます: トランザクションの ブロック、何をしたかの レシート、そして時々起きる reorg (チェーンが直近の歴史を書き換えること)。
これは Reth の Staged Sync と ExEx の 中級レッスンが暗黙の前提とするレイヤー。
ブロック — 3 つの部分
各 Ethereum ブロックは概念的に 3 つの束:
| 部分 | 中身 | ハッシング |
|---|---|---|
| ヘッダー | メタデータ: 親ハッシュ、state root、tx root、receipts root、ガスリミット、タイムスタンプ等 | ブロックの「身元」ハッシュは keccak256(header) |
| ボディ | トランザクションの実リスト、順序付き | ヘッダーの tx root はこのリストの Merkle root |
| レシート | tx ごと 1 レシート: ステータス、使用ガス、発行ログ/イベント | ヘッダーの receipts root がこのリストにコミット |
「ブロックハッシュ 0x123...」と聞いたら、それはヘッダーの keccak256 — ボディとレシートはヘッダーを 経由して コミットされるが、ハッシュに直接は入らない。
なぜヘッダーに 3 つの root?
ヘッダーは 3 つの Merkle root を持つ: state、transactions、receipts。各 root はチェーンデータの異なる部分にコミット:
state_root: このブロックの tx 実行 後 のワールドステート MPT のルートtransactions_root: このブロックの全 tx の MPT のルート (「tx X はブロック N にあった」を証明できる)receipts_root: 全レシートの MPT のルート (「tx X がログ Y を発行した」を証明できる)
ライトクライアントはヘッダーだけを持ち、proof をたどればどれでも検証できます (Expert で扱います)。
レシートとログ — 監査証跡
各トランザクションは レシート を生成:
struct Receipt {
status: bool, // 成功 / 失敗
cumulative_gas_used: u64,
logs: Vec<Log>,
bloom: BloomFilter, // ログフィルタ、高速検索用
}
struct Log {
address: Address, // 発行コントラクト
topics: Vec<B256>, // 最大 4 つ、indexed
data: Bytes, // unindexed payload
}
重要なポイントが 2 つ:
1. ログが Solidity event をオフチェーンに届ける手段
Solidity で emit Transfer(from, to, amount) を書くと、EVM は LOG3 opcode を実行:
- topic[0] = keccak256("Transfer(address,address,uint256)") — イベントシグネチャ
- topic[1] = from (indexed)
- topic[2] = to (indexed)
- data = ABI エンコード済み amount (indexed でない)
ログがレシートに入る。インデクサ・MEV ボット・ExEx すべてがこれを消費する。
2. ブルームフィルタは「このブロックは X に触れたか?」の高速チェック
各ブロックの receipts root は 256 バイトのブルームフィルタ を持ち、ブロック内の全ログアドレスとトピックを要約。ライトクライアントとインデクサがこれを使って、自分が気にするアドレスを言及していないブロックを高速にスキップする — フルレシートをダウンロードせずに。
中級レッスン 7 (本番 MEV) で ExEx コードがアドレスでログをフィルタする時、このブルームが事前フィルタを高速にする。
ブロックは実際どう作られるか
典型的なブロックライフサイクル:
1. ブロックプロポーザ (validator) がスロット N で選ばれる
2. プロポーザが mempool (もしくはビルダー) から保留中 tx を集める
3. 各 tx について (順番に):
a. フレームを開いて EVM 実行
b. 成功ならワールドステート更新、失敗なら巻き戻し
c. レシート追記
4. root 計算: state_root, transactions_root, receipts_root
5. ログからブルームフィルタ計算
6. 全 root + parent_hash + timestamp + ... でヘッダー構築
7. 署名して伝播
このブロックを実行するフルノードは 同じ実行を 検証のために行う — ボディを取得、各 tx を実行、結果の state_root がヘッダーに一致するか確認。一致しなければブロックは却下。
中級レッスン 4 で Reth の ExecutionStage を読む時、それが正にこの検証パス: 各ブロックの tx を再生し、状態変更を蓄積、結果のルートを検証。
Reorg — チェーンが自分を書き換える
通常チェーンは線形に伸びる:
... → block 100 → block 101 → block 102 → block 103
しかし時々、2 人の validator が同じスロットでブロックを提案したり、ネットワーク分断が回復したりすると、正準チェーンが切り替わる。先端 (tip) の数ブロックが 巻き戻され、チェーンは別の経路で再び伸びていく:
┌─→ 102b → 103b (新しい正準)
... → 100 → 101 → 102a → 103a ──────┘
└────────── 巻き戻し (もう正準ではない)
これが reorg。ノードの視点から:
- 新チェーンセグメント到着、現在より長いか attestation が多い
- 共通祖先まで遡る (例だと block 101)
- 102a, 103a の状態変更を 巻き戻す (逆順で)
- 102b, 103b の状態変更を 適用
- 新正準 tip は 103b
モダン Ethereum (Merge 後 PoS) では 1-2 ブロックより深い reorg は稀 だが起きる。validator の proposal 欠落や equivocation を契機に再構成が走る。
オフチェーン消費者にとっての意味
「ブロック N コミット → 自分の DB に txs を書き込む」というインデクサを書いたとして、ブロック N が reorg されたら:
- DB に 正準チェーン上で起きなかった トランザクションの行が残る
- reorg 検知時にそれらの行を 削除する必要 がある
- そして新正準ブロック N の tx の行を再挿入
これが まさに ExEx に 3 通知タイプがある理由 — ChainCommitted、ChainReorged、ChainReverted。ChainCommitted だけ扱う naive インデクサは reorg のたびに導出状態を破壊する。(中級レッスン 6 で詳しく扱う。)
なぜ Reth の Staged Sync が対称なのか
Reth の各 Stage は execute (forward) と unwind (backward) を持つ。Stage は reorg を「特殊ケース」として設計されていない — reorg は 通常運用、同じトレイトでモデル化される。1000 ブロック前進: execute。reorg で 3 ブロック後退: unwind。同じコードパス、逆方向。
これは 中級レッスン 4 (Staged Sync アーキテクチャ) で評価できる設計判断。
読み物リスト
- Etherscan で実ブロックを探す。ヘッダーの「Click to see More」をクリックして全フィールドを見る。parentHash、stateRoot、transactionsRoot、receiptsRoot、logsBloom を見つける。
- そのトランザクションの 1 つを開いて Logs タブを見る。各ログが Address・Topics・Data を持つ — それが上の構造。
- 本番での reorg の感覚を得るには reth.rs ブログ や Ethereum クライアントのリリースノートで「reorg」を検索 — 運用者は reorg ハンドリングの正しさを大いに気にする。
このレッスンで持ち帰るもの
- ブロック = ヘッダー + ボディ + レシート。ヘッダーは 3 つの Merkle root (state・txs・receipts) を持つ。
- レシート が各 tx のステータス・ガス・ログを記録 (Solidity
eventは LOG opcode 経由でログになる)。 - Reorg は直近の歴史を書き換える。オフチェーン消費者は巻き戻しを明示的に扱う必要がある。
- Reth の Staged Sync は 対称 (execute / unwind) — それは reorg が例外ではなく通常運用だから。
中級レッスン 4 で Stage トレイトを読むときも、lesson 6 で ExEx 通知型を読むときも、モデルはすでに頭にある。あとは実装された Rust を読むだけでよい。
まとめ(3行)
- ブロック構造 = Header + Body(tx 列)+ Receipts、state root / receipts root が MPT ルートでブロックヘッダに、validator 独立検証。
- Reorg = 信頼の浅いブロックの差し替え、PoS で 2-3 ブロック以内、finalized 不可、Reth ExEx 通知 3 バリアント(Committed / ChainReorged / Reverted)。
- 3 段 finality(latest / safe / finalized)、indexer は reorg 対応必須、次モジュールで Solidity → Rust 移行マップに踏み込む。