レッスン9 — zkEVM with Revm
問い
Linea、zkSync、Scroll、Polygon zkEVM — どの本番 zkEVM ロールアップも同じ主張: 「verifier は我々を信用しない、verifier は 250 バイトの証明を検査するだけ」。証明を生成するのが プローバ で、動かすのは geth でも nethermind でもなく Revm。zkVM 内で使うのに 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-counter の guest/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::getがNone→Database::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可能 + 静的ディスパッチ → 小さく決定論的、②Databasetrait が 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)は速度のために柔軟性を諦める。