レッスン11 — Differential fuzzing と execution-spec-tests
問い
フォークを ship した。カスタム precompile、独自 payload builder、いじったガススケジュール。ユニットテストは通っている、バニラ Reth diff テストも通る。だが、誰もテストを書かなかったパスに潜むバグはどう見つけるか?
原理(最小モデル)
- 2 方向の自動圧力. EEST(execution-spec-tests)が 仕様に対する準拠 を構造的に保証 + Differential fuzzing が 誰も探さなかったバグ を浮かび上がらせる。
- EEST はフォーク CI の毎 push. 昨日からの fail デルタが非ゼロなら build break。Triage: 意図的(document)/ 非意図的(バグ)。
- Differential fuzz 4 リファレンス. vanilla Revm(Revm セマンティクス)/ Geth
debug_traceTransaction(mainnet コンセンサス)/ Erigon(系譜逸脱検知)/ 形式仕様(Python EELS)。 - 24-48 時間走らせる + 縮約. Foundry スタイルの shrinking で 50-200 byte の最小再現に → 人間が repro を読んで根本原因特定。
- 両規律は補完であり代替ではない. EEST = 仕様明示範囲、fuzz = 仕様未定義パス含む。両方が production L1 チーム(Hyperliquid / Tempo / Berachain)の標準。
- Fault injection(chaos の前駆体). 入力ではなく 環境(DB read 失敗、ネットワーク分断、部分書き込み、OOM)を fuzz → 「書き込み中 crash でチェーン壊れる」クラス。
具体例
EEST を自分のフォークに走らせる:
# spec-tests フレームワークを clone
git clone https://github.com/ethereum/execution-spec-tests
cd execution-spec-tests
uv sync
# フォークの spec-test runner バイナリをビルド(各フォークが自前同梱; revm は `revme`)
cargo build --release -p revme
# スイートをバイナリに対して実行
uv run consume direct \
--bin /path/to/your/fork/target/release/revme \
-- ./tests/cancun/ # または任意のサブセット
Differential fuzz パターン:
ランダム tx → [自前フォーク] → state_root_A
↓
[リファレンス実装] → state_root_B
assert(state_root_A == state_root_B)
Fuzz harness 例:
// tests/differential_fuzz.rs
use libafl::prelude::*; // または proptest、arbitrary、独自 harness
fn fuzz_target(input: &[u8]) -> Result<()> {
let tx = arbitrary_tx_from_bytes(input)?;
let pre = arbitrary_pre_state_from_bytes(input)?;
let your_root = your_fork_execute(pre.clone(), tx.clone())?.state_root;
let ref_root = reference_execute(pre, tx)?.state_root;
if your_root != ref_root {
return Err(format!("DIFFERENTIAL: your={your_root}, ref={ref_root}").into());
}
Ok(())
}
3 ツール役割表:
| ツール | 捕まえる | 捕まえない |
|---|---|---|
| EEST | 仕様が明示的カバーするケースの回帰 | 仕様未定義挙動のバグ、フォーク固有エッジケース |
| Differential fuzzing | リファレンスからの乖離(仕様未定義パス含む) | リファレンスとフォーク両方間違っている仕様違反 |
| 両方併用 | コンセンサスクリティカルバグの広範囲 | 仕様黙 + リファレンスなし + fuzzer 未到達の稀バグ |
失敗例(誤解)
「EEST が通れば fuzz 不要」— 間違い。EEST は仕様明示範囲だけ。仕様が黙っているパス(カスタム precompile の境界条件、新 opcode 組み合わせ)は fuzz が浮かび上がらせる。
「Differential fuzz は vanilla Revm に対してだけ」— 不十分。Revm vs Geth の対比で Revm 系全体のバグ(系譜共有バグ)を炙れる。Erigon や EELS(Python 仕様)との対比で更に層を厚く。
「fuzz 24 時間で出ないなら問題なし」— 間違い。Fuzzer の入力空間カバレッジ依存。crash 0 = バグなしではない。フォーク誕生から半年が hit 率最高期 = この期間は重点的に。
🛑 予測。 vanilla Revm は何年も fuzz されている、なぜフォークで fuzz が特に有用?(答え: バニラ EVM のバグはほぼ新機能の未テスト組み合わせに残る。あなたのフォークは 新機能そのもの → fuzz hit 率が最も高い時期。同じ予算でも、vanilla を 1 ヶ月 fuzz する vs フォーク半年目を 1 週間 fuzz する → 後者の発見密度が桁違いに高い。)
ステップで組み立てる
Step 1: EEST を 1 回走らせる
uv run consume direct --bin revme -- ./tests/cancun/ → pass/fail/skip 数 → 昨日との delta → 非ゼロは build break。
Step 2: 簡単な fuzz harness を書く
proptest か libafl で「ランダム tx + pre-state → 両実装で実行 → state_root assert」の 50 行。1 時間走らせて乖離をログ。
Step 3: 4 リファレンスから選ぶ
- vanilla Revm: Revm 系譜のバグ検知
- Geth: mainnet コンセンサス検知
- Erigon: 系譜逸脱検知
- 形式仕様(Python EELS): 仕様直接対比
Step 4: 縮約規律
shrinking で 50-200 byte の最小 repro に → 人間が読んで根本原因特定。24-48 時間 fuzz + shrink + 人間 triage が標準。
Step 5: フォーク CI マトリクス
毎 push: EEST サブセット(5 分以内)+ 短い fuzz(5 分)+ diff against vanilla Reth(1000 ブロック)。週次: 24h fuzz + 全 EEST + マルチリファレンス diff。
Step 6: Fault injection で chaos の前段
入力ではなく環境 fuzz: kill -9 実行中プロセス、MDBX random page 破壊、ネットワーク分断、OOM 状況。Reth 自身の CI はここまでやらない、production フォークチームが追加。
答え合わせ
- EEST と fuzz の冗長性ではない理由: EEST は仕様に対してチェック、fuzz は別実装に対してチェック — 仕様が制約しないパスも含めて。仕様が「opcode X の入力 A での挙動は未定義」だと EEST はテストを書けない。fuzz は無作為に A を生成して別実装と比較する → 仕様未定義領域で乖離があれば検知。
- fuzz hit 率が「フォーク誕生から半年」で最高な理由: 新機能は未テスト組み合わせの空間が広い + 設計者の頭の中だけのテスト依存 → 想像外の入力で破綻する。時間が経つほどユーザが trigger する → 公開バグになる → 残りは fuzz が掘る相手が少なくなる。
- Fault injection が fuzzing と区別される領域: fuzzing = 入力空間探索(「どの tx で壊れるか」)、fault injection = 環境探索(「どの障害で安全性プロパティが破られるか」)。「書き込み中に crash してチェーンが壊れた」クラスは fuzzing には現れない(入力は正常)+ 環境が異常。
合格基準
- EEST と differential fuzzing が補完である理由を 1 文で言える。
- 4 リファレンス実装の選び分けを言える。
- 簡単な fuzz harness(
proptestで 50 行)を書ける。 - フォーク CI マトリクス(毎 push / 週次)を設計できる。
- Fault injection が fuzz と違う領域を捕まえる理由を言える。
まとめ(3行)
- EEST(仕様準拠)+ Differential fuzzing(リファレンス対比)が production フォークの正しさ保証 2 本柱、両方が毎 CI 必須。
- Fuzz は 24-48h 走らせて shrink、フォーク誕生から半年が hit 率最高、4 リファレンスで層を厚く。
- Fault injection(環境 fuzz)が chaos engineering の前段、「正常入力 + 環境異常」クラスのバグを捕まえる。