FABRKNT
Inside Revm — EVM エンジンを読む
Revmの心臓部
レッスン 5 / 17·CONTENT12 分25 XP
コース
Inside Revm — EVM エンジンを読む
レッスンの役割
CONTENT
順序
5 / 17

レッスン4 — ドリル: インタープリターのソースが読める証明

問い

add とマクロは読んだ。手を引かれずに同じファイルの残りを読める証明 — 別ウィンドウで cargo を開いた状態で 4 ドリル、どれも「読むだけ」ではなく やる + 観測したことを書き留める。何を観察するか?

原理(最小モデル)

  • セットアップ. git clone https://github.com/bluealloy/revm && cd revm && cargo build
  • 4 ドリル.mul を読み add と比較、② exp の動的ガス課金、③ わざとコンセンサスを壊す(wrapping_addsaturating_add)、④ add を計装してデータ流を観察。
  • addmul の構造同一の根拠. 両方とも 2-stack-in / 1-stack-out / 固定ガス / 副作用なし = 同じ制御フロー形にコンパイル、違うのは OPwrapping_add vs wrapping_mul)とガス料金(現行両方 3)のみ。
  • exp が本体課金の理由. コストが指数バイト単位(実行時値)に依存 → ディスパッチが前払いできない → 本体で gas! 呼び出し。同パターン: sha3 / mload / call 系。
  • コンセンサスを壊す実証. wrapping_addsaturating_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 × 数百万ノード × 数年)をターミナルに圧縮。

失敗例(誤解)

addmul が同形なのは『両方算術』だから」— 間違い。根本理由 = メカニクス的な同一プロファイル(2-in / 1-out / 固定ガス / 副作用なし)、このプロファイル合致全 Opcode が同じ制御フロー形にコンパイル。

exp は数学が複雑だから本体課金」— 不十分。根本理由 = コストがオペランド依存(指数バイト単位)→ ディスパッチが前払い不能 → 本体で gas!sha3 / mload / call も同じ理屈。

saturating_add 置換ぐらいでメインネットフォークはしない」— 致命的1 tx 分の数値不一致 = state-root 不一致 = ブロック検証失敗 = ピアから切断。コンセンサスはライブラリ関数 1 つ分の距離で失われる。

ステップで組み立てる

Step 1: muladd と比較

構造同一の根拠 = 2-in/1-out/固定ガス/副作用なしプロファイル。

Step 2: exp の動的ガス課金

オペランド依存コスト → 本体内 gas!、同パターンを sha3 / mload / call で。

Step 3: わざとコンセンサスを壊す

wrapping_addsaturating_add → test 数値不一致観察 → 戻す。

Step 4: eprintln! で計装

cargo test -- --nocapture で ADD 実行回数を数える、データ流を物理観察。

答え合わせ

  • addmul 同形の根拠: 両方 2-stack-in / 1-stack-out / 固定ガス / 副作用なし プロファイル → popn_top!([a], b, ctx.interpreter); *b = a.OP(*b); Ok(()) の同じ制御フロー形。違うのは OPwrapping_add vs wrapping_mul)とガス料金(現行両方 3)のみ。
  • exp が本体課金の理由: コストが 指数オペランドのバイト単位(実行時値)に依存 → ディスパッチが前払いできない → 本体内オペランド検査後に gas! 呼び出し。一般化: コストがオペランドに形作られる全 Opcode(sha3 / mload / call)は本体内で課金。
  • コンセンサスを壊す変更: wrapping_addsaturating_add、最初のオーバーフロー tx で他クライアントと state-root 不一致 → メインネットフォーク。テスト失敗モード = panic ではなく 数値不一致(特定の test ケースで期待値と実値が違う)= コンセンサス契約の実装ミスを CI が捕まえる。

合格基準

  • addmul の構造同一の根拠(2-in/1-out/固定ガス/副作用なし プロファイル)を即答できる。
  • exp が本体課金の理由(オペランド依存コスト)を即答 + 同パターン Opcode 3 つ(sha3 / mload / call)を言える。
  • wrapping_addsaturating_add がコンセンサスを壊す瞬間を即答できる。
  • cargo test -- --nocapture で計装出力を読む手順を言える。

まとめ(3行)

  • 4 ドリル = mul 構造比較 + exp 動的ガス + コンセンサスをわざと壊す + eprintln! 計装、どれも「やる + 観測したことを書き留める」。
  • addmul 同形は機構的(2-in/1-out/固定ガス/副作用なしプロファイル)、exp 本体課金はオペランド依存コスト(同パターン: sha3 / mload / call)。
  • wrapping_addsaturating_add でテスト数値不一致 → コンセンサスはライブラリ関数 1 つ分の距離で失われる、肌で感じてから戻す。