FABRKNT
Reth Expert — 本番エンジニアリング
本番エンジニアリング
レッスン 10 / 25·CONTENT15 分35 XP
コース
Reth Expert — 本番エンジニアリング
レッスンの役割
CONTENT
順序
10 / 25

レッスン9 — zkEVM with Revm

問い

Linea、zkSync、Scroll、Polygon zkEVM — どの本番 zkEVM ロールアップも同じ主張: 「verifier は我々を信用しない、verifier は 250 バイトの証明を検査するだけ」。証明を生成するのが プローバ で、動かすのは geth でも nethermind でもなく RevmzkVM 内で使うのに Revm が選ばれる理由は?

原理(最小モデル)

  • Revm を RISC-V にコンパイル → zkVM 内で実行 → zkVM が正しい実行の証明を出す. ホスト(preflight: witness 収集)→ guest(Revm 実行 + journal commit)→ プローバ(STARK / SNARK)→ 検証者コントラクト。
  • Revm が zkVM に適する 3 性質. モジュラー(Database trait で witness / oracle パターンが綺麗)+ 決定論的(同入力同出力)+ CPU で速い → サイクル数少 → 証明サイズ小。
  • Witness パターン. プローバ内では「ディスクから状態を読む」不可 → 証明前に witness(ブロックが触ったすべての値)を組み立て → in-zkVM Database 実装が witness から読む。witness にない値を読むと証明失敗。
  • env::commit_slice で公開出力. 「証明を持つ誰でも検証できる事実」を journal に。コミットメント(ブロックハッシュ + 番号 + stateRoot)+ アプリ固有データ。
  • 汎用 zkVM vs 専用 zkEVM. Risc0 / SP1 = 柔軟(任意 Rust)+ 遅い(数秒-数分 / ブロック GPU)/ 専用(Linea、Scroll)= 速い(1 秒未満)+ 不柔軟(全部自作)。

具体例

プルービングスタック:

flowchart TB
    subgraph Host
        RPC[Ethereum RPC] --> Pre[preflight: witness 収集]
    end
    Pre -->|Input: header + witness + call| Guest
    subgraph Guest [zkVM guest]
        Verify[stateRoot に対して witness 検証]
        Verify --> RevmRun[Revm が EVM call を実行]
        RevmRun --> Journal[block hash + 結果を commit]
    end
    Guest --> Prover[証明システム<br/>STARK / SNARK]
    Prover --> Proof[Proof + Journal]
    Proof --> Verifier[オンチェーン verifier コントラクト]

本物の guest コード(boundless-xyz/steel/examples/erc20-counterguest/src/main.rs):

use alloy_primitives::U256;
use erc20_counter_core::{IERC20, Input, Journal};
use risc0_steel::{Contract, ethereum::EthChainSpec};
use risc0_zkvm::guest::env;

fn main() {
    // guest 環境から入力を読む
    let input: Input = env::read();

    // chain ID から chain spec を導出
    let chain_spec = EthChainSpec::from_chain_id(input.chain_id).unwrap();

    // 入力を EvmEnv に変換。入力ヘッダーの state root と一致するか検証
    let env = input.evm_input.into_env(chain_spec);

    // view call 実行;sol! マクロが生成した型で結果が返る
    let call = IERC20::balanceOfCall {
        account: input.account,
    };
    let returns = Contract::new(input.erc20_contract, &env)
        .call_builder(&call)
        .call();

    // 指定アカウントが少なくとも1トークン保有している
    assert!(returns >= U256::from(1));

    // view_call_env 導出に使ったブロックハッシュと番号を journal にコミット
    let journal = Journal {
        commitment: env.into_commitment(),
        contract: input.erc20_contract,
    };
    env::commit_slice(&journal.abi_encode());
}

ホスト側(preflight):

let input = builder.preflight(&provider, contract, &call).await?;
let env = ExecutorEnv::builder().write(&input)?.build()?;
let receipt = default_prover().prove(env, ERC20_COUNTER_GUEST_ELF)?;

Witness DB パターン:

struct WitnessDB {
    accounts: HashMap<Address, AccountInfo>,
    storage: HashMap<(Address, U256), U256>,
    // ...
}

impl Database for WitnessDB {
    fn basic(&mut self, addr: Address) -> ... {
        Ok(self.accounts.get(&addr).cloned())
    }
    // ...
}

性能比較:

システム証明時間(1 ブロック)ハードウェア
Risc0数秒〜数分GPU
SP1数秒GPU + 再帰
専用 zkEVM (Linea, Scroll)1 秒未満 / ブロック専用インフラ

失敗例(誤解)

「Go 製 geth を zkVM 用にコンパイルできる」— 動かない。GC + goroutine + 動的ディスパッチ + 巨大バイナリで zkVM が爆発(サイクル数膨大)。Revm が選ばれる構造的理由: no_std 可能 + Database trait で IO 抽象 + 非決定性なし + 静的ディスパッチ中心。

「プローバが state について嘘をつける」— 間違いevm_input.into_env(chain_spec)witness をヘッダの stateRoot に対して検証 → 1 バイト改竄で失敗。プローバが嘘をつくと証明が出ない。

🛑 予測。 攻撃者が、コールが必要とする 1 つのストレージスロット以外はすべて正しい state を持つ witness を含む Input を提出。guest のどこで abort?(答え: Contract::new(...).call_builder(&call).call() 実行中、Revm がその欠落スロットを read しようとした瞬間に WitnessDB の HashMap::getNoneDatabase::storage が「未含有」エラー → call panic → 証明生成失敗。プローバから見える失敗は「guest プログラムが panic で終了」、receipt 生成不可。)

ステップで組み立てる

Step 1: スタック 4 層を即答

Host(preflight) → Guest(Revm + commitment) → Prover(STARK/SNARK) → Verifier コントラクト。

Step 2: Revm が zkVM に適する 3 性質

モジュラー / 決定論的 / CPU で速い → サイクル数少 → 証明サイズ小。

Step 3: Witness パターン

ホストが preflight で witness 収集 → Input にして guest に渡す → guest 内の WitnessDB(Database 実装)が witness から read。witness にない read は証明失敗 = セキュリティモデルの一部。

Step 4: env::commit_slice の意味

公開出力 = 証明を持つ誰でも検証できる事実。Journal に block hash + stateRoot + アプリ固有データを入れる → 検証者は journal + proof で「block N でコントラクト X のユーザ Y が ≥ 1 トークン保有」を知る。

Step 5: 汎用 vs 専用

Risc0 / SP1 = 柔軟性のためにプローバ速度を諦める(任意 Rust 証明可)/ 専用(Linea / Scroll)= ブロック当たり桁違いに速いが全部自作。Risc0 を選ぶ理由は「証明したいロジックが Solidity 以外でも書ける」「Ethereum 以外の chain も対応」。

答え合わせ

  • 「1 バイトでも間違えば失敗」のメカニズム: into_env が witness(trie ノード列)を MPT 検証 → ヘッダの stateRoot を再導出して入力ヘッダと比較。MPT は Merkle ハッシュなので 1 バイト改竄 → 親ハッシュ変化 → 再導出 root が入力 root と一致しない → 検証エラー → guest abort。
  • Revm が Geth より zkVM に適する 3 性質: ① no_std 可能 + 静的ディスパッチ → 小さく決定論的、② Database trait が witness 抽象を綺麗に受ける、③ CPU で速い → サイクル数少 → 証明サイズ / 時間が小。Go の Geth は GC + goroutine で zkVM 苦手。
  • 専用 zkEVM が遅い Risc0 を使う 2 シナリオ: ① 任意 Rust ロジックを証明したい(アプリ固有計算、AI 推論、署名集約など)→ 専用 zkEVM の EVM 縛りから外れる、② 複数チェーン対応 / 新 chain への即移植性が欲しい(Risc0 は Ethereum 以外も chain spec 差し替えで対応)。

合格基準

  • 4 層スタックを即答できる。
  • Revm が zkVM 適合な 3 性質を言える。
  • Witness パターン(ホスト収集 → WitnessDB read → 欠落は失敗)を説明できる。
  • env::commit_slice の公開出力の意味を言える。
  • 汎用 vs 専用の使い分けを 2 シナリオで言える。

まとめ(3行)

  • zkEVM プローバは Revm を RISC-V にコンパイル → zkVM 内で実行 → 証明出力。ホスト preflight が witness 収集、guest が Revm + commitment、プローバ + 検証者コントラクトで完結。
  • Revm が選ばれる 3 理由(モジュラー / 決定論的 / CPU で速い)= zkVM が罰する 3 性質(非決定性 / syscall / 巨大バイナリ)の逆。
  • 汎用 zkVM(Risc0 / SP1)は柔軟性のためにプローバ速度を諦め、専用 zkEVM(Linea / Scroll)は速度のために柔軟性を諦める。