レッスン4 — ドリル: インタープリターのソースが読める証明
問い
add とマクロは読んだ。手を引かれずに同じファイルの残りを読める証明 — 別ウィンドウで cargo を開いた状態で 4 ドリル、どれも「読むだけ」ではなく やる + 観測したことを書き留める。何を観察するか?
原理(最小モデル)
- セットアップ.
git clone https://github.com/bluealloy/revm && cd revm && cargo build。 - 4 ドリル. ①
mulを読みaddと比較、②expの動的ガス課金、③ わざとコンセンサスを壊す(wrapping_add→saturating_add)、④addを計装してデータ流を観察。 addとmulの構造同一の根拠. 両方とも 2-stack-in / 1-stack-out / 固定ガス / 副作用なし = 同じ制御フロー形にコンパイル、違うのはOP(wrapping_addvswrapping_mul)とガス料金(現行両方 3)のみ。expが本体課金の理由. コストが指数バイト単位(実行時値)に依存 → ディスパッチが前払いできない → 本体でgas!呼び出し。同パターン:sha3/mload/call系。- コンセンサスを壊す実証.
wrapping_addをsaturating_addに変更 →cargo test -p revm-interpreterで数値不一致 → メインネットフォーク。 eprintln!で計装.cargo test -- --nocaptureで出力可視化、ADD 実行回数を数える、データ流を物理的に観察。
具体例
ドリル 3 — コンセンサスを壊す:
# 1. crates/interpreter/src/instructions/arithmetic.rs の add で
# wrapping_add を saturating_add に変更、保存
# 2. cargo test -p revm-interpreter
# → テスト失敗、数値不一致(panic ではない)
# 3. git checkout crates/interpreter/src/instructions/arithmetic.rs
# 4. cargo test -p revm-interpreter で再びパス確認
要点 = ライブラリ関数 1 つを書き換えた瞬間、クライアントは 0xFFF...FF + 1 の結果について世界中の他 Ethereum クライアントと不一致 = ADD オーバーフローする最初の tx でメインネットからフォーク。
ドリル 4 — 計装:
pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
popn_top!([op1], op2, context.interpreter);
eprintln!("ADD: {:#x} + {:#x} = ?", op1, *op2); // ← 追加
*op2 = op1.wrapping_add(*op2);
eprintln!("ADD result: {:#x}", *op2); // ← 追加
Ok(())
}
cargo test -p revm-interpreter -- --nocapture で出力。テストスイート全体で ADD 実行回数を数える = production の現実(mainnet 毎 tx × 数百万ノード × 数年)をターミナルに圧縮。
失敗例(誤解)
「add と mul が同形なのは『両方算術』だから」— 間違い。根本理由 = メカニクス的な同一プロファイル(2-in / 1-out / 固定ガス / 副作用なし)、このプロファイル合致全 Opcode が同じ制御フロー形にコンパイル。
「exp は数学が複雑だから本体課金」— 不十分。根本理由 = コストがオペランド依存(指数バイト単位)→ ディスパッチが前払い不能 → 本体で gas!。sha3 / mload / call も同じ理屈。
「saturating_add 置換ぐらいでメインネットフォークはしない」— 致命的。1 tx 分の数値不一致 = state-root 不一致 = ブロック検証失敗 = ピアから切断。コンセンサスはライブラリ関数 1 つ分の距離で失われる。
ステップで組み立てる
Step 1: mul を add と比較
構造同一の根拠 = 2-in/1-out/固定ガス/副作用なしプロファイル。
Step 2: exp の動的ガス課金
オペランド依存コスト → 本体内 gas!、同パターンを sha3 / mload / call で。
Step 3: わざとコンセンサスを壊す
wrapping_add → saturating_add → test 数値不一致観察 → 戻す。
Step 4: eprintln! で計装
cargo test -- --nocapture で ADD 実行回数を数える、データ流を物理観察。
答え合わせ
addとmul同形の根拠: 両方 2-stack-in / 1-stack-out / 固定ガス / 副作用なし プロファイル →popn_top!([a], b, ctx.interpreter); *b = a.OP(*b); Ok(())の同じ制御フロー形。違うのはOP(wrapping_addvswrapping_mul)とガス料金(現行両方 3)のみ。expが本体課金の理由: コストが 指数オペランドのバイト単位(実行時値)に依存 → ディスパッチが前払いできない → 本体内オペランド検査後にgas!呼び出し。一般化: コストがオペランドに形作られる全 Opcode(sha3/mload/call)は本体内で課金。- コンセンサスを壊す変更:
wrapping_add→saturating_add、最初のオーバーフロー tx で他クライアントと state-root 不一致 → メインネットフォーク。テスト失敗モード = panic ではなく 数値不一致(特定の test ケースで期待値と実値が違う)= コンセンサス契約の実装ミスを CI が捕まえる。
合格基準
addとmulの構造同一の根拠(2-in/1-out/固定ガス/副作用なし プロファイル)を即答できる。expが本体課金の理由(オペランド依存コスト)を即答 + 同パターン Opcode 3 つ(sha3/mload/call)を言える。wrapping_add→saturating_addがコンセンサスを壊す瞬間を即答できる。cargo test -- --nocaptureで計装出力を読む手順を言える。
まとめ(3行)
- 4 ドリル =
mul構造比較 +exp動的ガス + コンセンサスをわざと壊す +eprintln!計装、どれも「やる + 観測したことを書き留める」。 addとmul同形は機構的(2-in/1-out/固定ガス/副作用なしプロファイル)、exp本体課金はオペランド依存コスト(同パターン:sha3/mload/call)。wrapping_add→saturating_addでテスト数値不一致 → コンセンサスはライブラリ関数 1 つ分の距離で失われる、肌で感じてから戻す。