レッスン19 — Custom ChainSpec(fork / genesis / precompile schedule)
問い
mainnet では検証が通るブロックが、あなたの chain では reject される。同じブロック、同じクライアントバイナリ、同じ Revm — なのに結果が違う。ChainSpec のどこかが「この高さでは、ここのルールが違う」と言っているから — 何が入っているか?
原理(最小モデル)
ChainSpec= chain のプロトコル定義. Chain ID / Hardfork activation / Base fee params / Genesis / Precompile schedule / レガシー params。- 6 カテゴリ. Chain ID(EIP-155 replay protection)+ Hardfork activation(block-height / timestamp スイッチ)+ Base fee(EIP-1559 elasticity / change denominator)+ Genesis(初期 allocation / state root / gas limit)+ Precompile schedule(各 fork でアクティブな precompile アドレス)+ レガシー(block gas limit / DAO fork / mining difficulty)。
- 拡張 ChainSpec. L2 chain は base
ChainSpecをラップし chain 固有 fork(Bedrock / Canyon / Ecotone / Fjord)+ 独自 precompile schedule を追加。 - Hardfork enum がプロトコル史. 声に出して読むのが chain 理解の最速ルート。
- Precompile activation は ChainSpec に住む. EVM config 単独ではなく ChainSpec → activation 自体がコンセンサスルール。
- L2 chainspec の追加項目. L1 chain ID / L1 block oracle / Sequencer address / Withdrawal config。
具体例
ChainSpec 6 カテゴリ:
| カテゴリ | 何を制御 |
|---|---|
| Chain ID | EIP-155 replay protection キー |
| Hardfork activation | Protocol upgrade を切り替える block-height / timestamp スイッチ |
| Base fee params | EIP-1559 elasticity、change denominator |
| Genesis | 初期 allocation、state root、gas limit |
| Precompile schedule | 各 fork でアクティブな precompile アドレス |
| その他レガシー | Block gas limit、DAO fork、mining difficulty(legacy) |
OP Hardfork enum 例:
pub enum OptimismHardfork {
Bedrock,
Regolith,
Canyon,
Ecotone,
Fjord,
Granite,
Holocene,
// ...
}
Precompile schedule の対応:
Fork F において、アドレス A は ネイティブ関数 impl I にマップされる
実体は chain の EVM config crate、activation 判定は ChainSpec(activation 自体がコンセンサスルール)。
Genesis 出荷物:
- Genesis JSON ファイル(allocation / gas limit / 初期 difficulty/seal)
- chainspec crate 内
GenesisRust struct(JSON からロード可能) - 計算済み genesis state root — 全ノード合意が必要
L2 chainspec 追加項目:
- L1 chain ID — L2 がアンカーされている先(cross-domain message verification)
- L1 block oracle address on L2 — 現 L1 block hash を記録するコントラクト
- Sequencer address — sequencer 署名つき batch の検証用
- Withdrawal config — L2 → L1 withdrawal の時間遅延
失敗例(誤解)
「Precompile activation は EVM config 単独で十分」— 間違い。Block N でどの precompile がアクティブかを 2 ノードが食い違って判定 → 一方が「成功」もう一方が「revert」→ stateRoot 乖離 → コンセンサス分裂。activation はコンセンサスルール → ChainSpec に置く(EVM config はその指示を実行するだけ)。
「Genesis state root はどうでもいい」— 間違い。コード上の root と実ネットワークの root が食い違えば、全ノードが block 1 で食い違う → chain が起動しない / 1 ブロックで分岐。
「ChainSpec は単なる config ファイル」— 半分間違い。6 カテゴリすべてがコンセンサスクリティカル。chain ID 違い = replay 攻撃、hardfork 高さ違い = 分裂、precompile schedule 違い = stateRoot 乖離。「config」より「プロトコル定義」が正確。
🛑 予測。 "ChainSpec" は config 風に聞こえるが何が入っているか カテゴリを 5 つ 予想する。5 つに届かない / 全部 gas 関連 → consensus rule の範囲を過小評価。(答え: Chain ID / Hardfork activation / Base fee params / Genesis / Precompile schedule / レガシー params。6 カテゴリすべて consensus rule、ChainSpec 1 つ間違えると chain が分裂する。)
ステップで組み立てる
Step 1: 6 カテゴリ即答
Chain ID / Hardfork / Base fee / Genesis / Precompile schedule / レガシー。
Step 2: Hardfork enum を音読
OptimismHardfork::Bedrock / Regolith / Canyon / Ecotone / Fjord / ... を声に出す → chain のプロトコル史。
Step 3: 「fork F は block H、timestamp T でアクティブか?」関数
OP chainspec の is_fork_active_at_block / is_fork_active_at_timestamp を辿る。
Step 4: OP mainnet と Base の Bedrock activation block
両方が異なる block を持つ → OP mainnet は本番、Base は別。同じ OptimismHardfork::Bedrock でも chain ごとに高さが違う。
Step 5: L2 chainspec の追加 4 項目
L1 chain ID / L1 block oracle / Sequencer address / Withdrawal config。
答え合わせ
- Precompile activation を ChainSpec に置く理由: 2 ノードが block N で精コンパイル set を食い違うと → 一方が呼び出し成功、もう一方が「アドレスは empty」と revert → stateRoot 乖離 → 分裂。activation はコンセンサスルール → 全ノード合意必要 → ChainSpec(プロトコル定義)に住む。
- Genesis state root のコード ↔ 実ネットワーク食い違いの結末: 新ノード起動時、ChainSpec の genesis root を「正しい root」として使う → 実ネットワークのノードが違う root を持つ → block 1 をネットワークから受信 → 自分の post-state root と食い違う → block 1 を invalid と判定 → ネットワークから孤立。
- L1 chainspec と L2 chainspec の追加 4 項目の存在理由: L2 は親 chain(L1)にアンカーされる → ① L1 chain ID(どの L1)+ ② L1 block oracle(L1 状態へのアクセス)+ ③ Sequencer address(sequencer 認証)+ ④ Withdrawal config(cross-layer 時間遅延)。L1 自体はアンカー先がない → 不要。
合格基準
- 6 カテゴリを即答できる。
- Hardfork enum を音読 → activation table が読める。
- Precompile activation が ChainSpec に住む理由を説明できる。
- Genesis state root の重要性を言える。
- L2 chainspec の追加 4 項目を即答できる。
まとめ(3行)
- ChainSpec = 6 カテゴリ(Chain ID / Hardfork activation / Base fee / Genesis / Precompile schedule / レガシー)、すべて consensus-critical。
- Hardfork enum はプロトコル史、activation table は「fork F が block H、timestamp T でアクティブか」を答える関数。
- L2 chainspec は L1 chain ID / L1 block oracle / Sequencer / Withdrawal の 4 項目を追加 — L1 は自分でアンカーされないので不要。