レッスン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 run で DOUBLE_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トレイトで状態供給。