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

レッスン8 — ドリル: フォークを出荷する

問い

3 行のメカニクスと 3 注意点は読んだ。自分で配線する — 別ウィンドウで cargo を立ち上げて、5 ドリル。何を観測するか?

原理(最小モデル)

  • セットアップ. git clone https://github.com/bluealloy/revm && cd revm && cargo build
  • 5 ドリル. ① 未割当バイト探索(レッスンを信用しない)、② 自前 Opcode 定義(DOUBLE_TOP = 1-in/1-out)、③ テーブルに配線、④ バイトコード実行、⑤ eprintln! で観察。
  • popn_top!([], op, ...) パターン.[] = pop なし、op だけがバインドされ現在スタックトップへの &mut、1-in/1-out/その場書き換え Opcode の表現。
  • DOUBLE_TOP 実装. *op = (*op).wrapping_mul(U256::from(2));、1 命令で「トップを 2 倍」。
  • テスト用バイトコード. PUSH1 0x05 DOUBLE_TOP STOP = 60 05 0C 00、5 push → 2 倍 → スタック 10 で終わる。
  • eprintln! 計装. 1 行のバイトコード 0x0C が 3 行 Rust 関数を起動する因果鎖を物理観察。

具体例

ドリル 2 — DOUBLE_TOP 実装:

const DOUBLE_TOP: u8 = 0x0C;  // 自分が選んだ未割当バイト

pub fn double_top<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
    popn_top!([], op, context.interpreter);
    *op = (*op).wrapping_mul(U256::from(2));
    Ok(())
}

ドリル 3 — テーブル配線:

let mut table = standard_table();
table[DOUBLE_TOP as usize] = Instruction::new(double_top);

ドリル 4 — バイトコード:

PUSH1 0x05  // 5 をスタックに push — バイト: 0x60 0x05
DOUBLE_TOP  // カスタム Opcode — バイト: 0x0C
STOP        // 0x00

16 進: 60 05 0C 00、最終スタック 10(= 5 × 2)。

ドリル 5 — 計装:

pub fn double_top<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
    popn_top!([], op, context.interpreter);
    let before = *op;
    *op = (*op).wrapping_mul(U256::from(2));
    eprintln!("DOUBLE_TOP: {:#x} -> {:#x}", before, *op);
    Ok(())
}

cargo runDOUBLE_TOP: 0x5 -> 0xa 1 行 = ディスパッチがあなたの Rust 関数に到達した瞬間の証跡。

失敗例(誤解)

popn_top!([op1], op2, ...)popn_top!([], op, ...) は同じ」— 間違い。空 [] = pop なし、op だけが現在スタックトップへの &mut = 1-in/1-out/その場書き換え Opcode の表現(add は 2-in/1-out)。マクロのアリティマッチャがここで効く。

「メインネットに出荷しても問題ない」— 致命的0x0C を含むブロックを go-ethereum が実行しようとした時点で state-root 検証失敗 → ピア切断、メインネットからフォーク。自前チェーン限定

cargo test で出力が見える」— 不十分-- --nocapture フラグが必要、cargo がデフォルトで eprintln! 出力を抑制する。

ステップで組み立てる

Step 1: 未割当バイト探索

crates/interpreter/src/instructions.rs でテーブル構築関数の代入左辺に 出現しない バイトを探す(0x0C-0x0F 等)。

Step 2: DOUBLE_TOP を定義

<IT: ITy, H: ?Sized> + popn_top!([], op, ...) + wrapping_mul(U256::from(2))

Step 3: テーブルに配線

standard_table() をコピー + table[DOUBLE_TOP as usize] = Instruction::new(double_top);

Step 4: テストバイトコード実行

60 05 0C 00 実行 → スタック 10 確認。

Step 5: eprintln! で計装

cargo run or cargo test -- --nocapture で因果鎖(バイト 0x0C → Rust 関数)を物理観察。

答え合わせ

  • popn_top!([], op, ...) のメカニクス: 空 [] = pop する値なしop だけが現在スタックトップへの &mut。1-in/1-out/その場書き換え Opcode(add の 2-in/1-out との違い)の表現方法。マクロのアリティマッチャが「同じマクロ、異なるスタックプロファイル、関数を二重に書く必要なし」を実現。
  • DOUBLE_TOP のスタックプロファイル: 1-in (現トップ) / 1-out (新トップ) / 副作用なし、その場書き換え。add (2-in/1-out)よりさらに単純、mul / sub 等の 2-in 二項より純粋。
  • メインネットに出荷した瞬間に壊れるもの: 0x0C を含む最初のブロックで他クライアントと state-root 不一致 → ピア切断。go-ethereum は 0x0C を INVALID として実行、あなたの Reth は swap として実行 → ブロックの state-root 検証失敗 → メインネットからフォーク。

合格基準

  • popn_top!([], op, ...)popn_top!([op1], op2, ...) の構造的違いを即答できる。
  • DOUBLE_TOP のスタックプロファイル(1-in/1-out/副作用なし)を即答できる。
  • メインネット出荷時に最初に壊れるもの(0x0C 含むブロックの state-root 不一致)を即答できる。
  • cargo test -- --nocapture フラグの役割を言える。
  • 5 ドリルを通じて「読んだ」から「やった」への遷移を実体験できる。

まとめ(3行)

  • 5 ドリル = 未割当バイト探索 + DOUBLE_TOP 実装(1-in/1-out)+ テーブル配線 + バイトコード 60 05 0C 00 実行 + eprintln! 計装。
  • popn_top!([], op, ...) 空ブラケット = pop なし、op だけが現スタックトップ &mut、1-in/1-out/その場書き換えの表現。
  • スタック 10 確認 → フォーク出荷完了、メインネット非互換チェーンを実行、コストを身をもって理解、次は Database トレイトで状態供給。