本記事は、ゼロ知識証明の汎用化とzkVM(パート1)の続きとなります。
ゼロ知識証明に関する基本的な説明は、前回(パート1)記事またはレポート「ゼロ知識証明の現在地〜ブロックチェーンを超えた活用可能性〜(日本総合研究所 先端技術リサーチ)」をご覧いただければ幸いです。
はじめに:zkVM(ゼロ知識仮想マシン)への注目
イーサリアム財団のリサーチャーであるジャスティン・ドレイク氏は、Ethereum のカンファレンス「Devcon 2024」で、ビームチェーン(Beam chain) と呼ばれるプロジェクトについて発表し、そのコア技術として zkVM(ゼロ知識仮想マシン) を位置付けました。ビームチェーンは主にコンセンサス・レイヤーに焦点を当てており、zkVM はステート・トランジション(ブロックチェーンが「現在の状態」から「新しい状態」へ移行する際の「数学的かつプログラム的な約束事」)関数の正当性を証明するための SNARK証明1を、オフチェーンで生成するために利用されるとのことです。
ドレイク氏は講演の中で zkVM のメリットとして、
-
プログラマが暗号や SNARK の深い専門知識を持たなくても使える
-
高水準言語で実装されたコードを汎用的に ZK 証明化しやすい
の 2 点を挙げています。
図1.Devconでのドレイク氏の講演資料より(https://www.youtube.com/watch?v=rGE_RDumZGg)
こうした流れもあり、徐々にzkVMへの注目も高まってくることと思います。
本記事では、前回記事の続きとして、zkVMの話題を掘り下げ、zkVMを用いてどのように証明を生成し、検証することができるのかワークフローを概説したいと思います。
zkVMの種類と整理
前回の記事では、「zkVM はプログラムを直接算術回路に変換するのではなく、まずプログラムを実行し、その実行トレース(メモリやレジスタなどの状態遷移)の整合性を証明する」と述べました。これは Circom のような「直接回路を生成する」タイプの ZKP 技術と比較すると分かりやすい説明ですが、実際の zkVM の実装方法はプロダクトによって大きく異なります。
zkVM を分類する際、ISA(Instruction Set Architecture, 命令セットアーキテクチャ)に注目する方法があります。ISA とは、プロセッサが実行する命令セットを規定するインターフェースのことです。たとえば、Polygon zkEVM や Scroll zkEVM、zkSync Era などの zkEVM プロジェクトは Ethereum の Ethereum VM(EVM) 命令セットを模倣し、EVM 互換の命令セットを ISA としています。一方、zkWASM プロジェクト(例:DelphinusLab の zkWASM)は WebAssembly(WASM)バイトコードを対象とし、WASM 準拠の ISA を採用します。
また近年では、RISC-V 2と呼ばれるオープンな ISA を用いるプロダクトが増えています。具体的には、RiscZero や SuccinctLabsの「SP1 が代表例です。先述のジャスティン・ドレイク氏も講演の中で、RISC-V ベースの zkVM が事実上の業界標準になりつつあると述べています。
一方、Cairo(StarkNet で使用)や Valida のように、独自の ISA を採用するプロダクトもあります。
ISA が異なると、その周辺エコシステム(EVM や WASM)が異なり、利用可能なライブラリやツールセットも変わってきます。これは開発者にとっての使い勝手(開発者体験)に大きく影響するため、ひとつの重要な分類基準になるでしょう。
しかし、zkVM プロダクト間の差異は ISA によるものだけではありません。証明システム全体としての設計にも根本的な違いがあり、それが各 zkVM プロダクトの競争力やユースケースに最適化されるかどうかを左右します。本記事では、zkVM を実現するアプローチに基づき、これらの違いを整理してみます。
zkVMを実現するアプローチ
複数の小さな回路を結合するアプローチ
1 つ目のアプローチは、SNARK ベースで、仮想マシン(EVM や WASM)に対応した「ユニバーサルな算術回路(ユニバーサル回路)」を構築し、プログラムの解析や実行トレースに基づいて必要な制約を生成する方法です。このアプローチでは、ルックアップテーブルや条件分岐の仕組みを活用し、特定の命令に対応する小さな算術回路(サブ回路)を動的に有効化します。Polygon zkEVM や Scroll zkEVM、DelphinusLab の zkWASM などがこの方式を採用しています。
具体的には、ユニバーサル回路の中に加算・乗算・メモリアクセスなど、さまざまな命令に対応するサブ回路を含め、実行トレースに記録された命令に応じてそれらを選択的に利用します。また、命令だけでなくメモリアクセスやスタック操作といった「実行トレース全体の整合性」も制約として組み込むため、効率的で柔軟な証明生成が可能です。一方、大規模なプログラムほど証明生成に時間がかかる傾向がありますが、証明サイズを比較的コンパクトに抑えられる利点があります。
図2.論文「ZKWASM: A ZKSNARK WASM Emulator」記載のアーキテクチャ図3。さまざまなサブ算術回路(Circuit)から構成される大きなユニバーサル回路として機能する。
実行トレース全体を一括証明するアプローチ
2つ目は、実行トレースそのものをモデル化し、FRI(Fast Reed-Solomon Interactive Oracle Proofs of Proximity)を用いたSTARKベース4の証明を生成するアプローチです。この方法では、実行トレースを多項式として表現し、その整合性を一括して証明します5。STARKベースの特徴として、証明生成が比較的高速である一方、証明サイズが大きくなる傾向があります。
かつてSTARKベースのアプローチでは、CairoVMのように独自のISAやDSLを用いることが一般的でしたが、最近ではRISC-VのようなオープンなISAを活用する動きが増えています。これにより、RISC-Vのエコシステムで用いられている既存のツールを最大限活用して、RustやC++などの高級言語を活用したzkVMが実現され、開発者体験が大幅に向上しています。具体例として、RiscZeroやSP1が挙げられます。
図3.RISC Zeroの証明システム (https://dev.risczero.com/proof-system/) 。FRIによるSTARK証明が多用されている。図では最終的な証明サイズを抑えるためのGroth16によるSNARK証明への変換についても記載されている(STARKからSNARKへの変換はオプションである。)
その他のアプローチ
他にも、JoltのようにLassoルックアップ引数を活用したzkVMフレームワークや、Nexusのように折り畳み方式のSNARKスキームを特徴とするzkVMがあります。前述の2つのアプローチと比べてマイナーではありますが、競争の激しい分野であるため、今後より優れたアプローチが登場する可能性もあります。
2つのアプローチの比較。
「複数の小さな回路を結合するアプローチ」では、命令ごとに多数のサブ回路を作成する必要があるため、プロダクトの大半を回路設計が占めがちです。たとえば zkWASM プロジェクトでは、ソースコード(約 22k 行)のうち 12k 行ほどが回路関連のコードだという話もあります6。回路の品質=証明の品質であるため、保守やセキュリティ確保に相応のコストがかかるという課題があるでしょう。
一方、「実行トレース全体を一括して証明するアプローチ」は STARK ベースなので、証明サイズが大きくなり、ブロックチェーン上での検証は困難です。しかし、RiscZero のように STARK の検証を Groth16 の SNARK 証明に落とし込んでコンパクトに圧縮し、ブロックチェーン上での検証を可能にする例も存在します。さらに、このアプローチは証明プロセスの並列化が容易7なため、GPU を活用した大幅な証明速度の向上も見込めます。
zkVMのワークフロー
ゲストとホスト
RISC Zero を例に、zkVM のワークフローを説明します。はじめに、ゲスト(Guest)コード と ホスト(Host)コード の概念を説明します。zkVM におけるゲストコードとは、ゼロ知識証明を適用して「証明したいロジック」そのものです。つまり、証明対象となる計算や処理を定義し、zkVM 内で実行される部分といえます。RISC Zero の場合、Rust で書かれた任意のロジックを RISC Zero 対応のコンパイラを介して ELF 形式のバイナリに変換し、それを zkVM 上で動かす形になります。
ゲストコードは zkVM 上の隔離されたメモリ空間で実行されるため、外部とのデータや I/O のやり取りは制限されます。ゲストコードは「入力を受け取り、計算を行い、結果を出力する箱」のようなイメージです。
use risc0_zkvm::{guest::env, sha::Digest};
use tiny_keccak::{Hasher, Keccak};
fn main() {
// 入力値(ここではString型として)を受け取り
let data: String = env::read();
// 入力値に対して計算を実行(ここではKeccak-256ハッシュを計算)
let mut hasher = Keccak::v256();
hasher.update(&data.as_bytes());
let mut output = [0; 32];
hasher.finalize(&mut output);
let digest = Digest::from_bytes(output);
// 計算結果(ハッシュ値)をホスト側に出力(zkVMの出力にコミット)
env::commit(&input);
}
上記コード(RISC Zero公開のexampleより)では、env::read()
で入力値を受け取り、env::commit()
で結果をホスト側に受け渡しています。ここで実行された処理は zkVM によって暗号学的に追跡され、計算結果だけでなく「証明(プルーフ)」も、zkVM を管理・制御しているホスト側で受け取れます。
一方、ホストコードは zkVM の実行をトリガーする役割を果たします。ホストコードを通じてユーザー(開発者や運用者)は zkVM を管理・制御できるようになり、RISC Zero の場合は zkVM の実行環境(ExecutorEnv)を初期化し、ゲストコードに渡す入力値などを設定します。セットアップが完了すると、コンパイルされたゲストコード(ELF ファイル)をロードし、実行を呼び出したうえで、計算終了後に結果や生成された証明を受け取ります。
上記のゲストコードと応対するexampleのホストコードの抜粋です。
use keccak_methods::{KECCAK_ELF, KECCAK_ID};
use risc0_zkvm::{default_prover, sha::Digest, ExecutorEnv, Receipt};
fn provably_hash(input: &str) -> (Digest, Receipt) {
//zkVMの実行環境をセットアップします.
let env = ExecutorEnv::builder()
.write(&input) //Keccak-256ハッシュ化を行う文字列をzkVMの入力値として渡す.
.unwrap()
.build()
.unwrap();
let prover = default_prover();
// env は入力値が指定された実行環境
// KECCAK_ELF はゲストコードをコンパイルしたバイナリ
// prove() を実行し、receipt(計算結果を含む証明データセット全体)を取得する
let receipt = prover.prove(env, KECCAK_ELF).unwrap().receipt;
// receiptは証明データセット全体.journalがゲストコードの出力結果.
let digest = receipt.journal.decode().unwrap();
(digest, receipt)
}
zkVM(仮想マシン)やホスト、ゲストといった用語のため、既存の仮想化ソフトウェアをイメージする方もいらっしゃるかもしれません。zkVMの場合は、これらの関係性がやや異なることには、注意が必要です。
ホストOSは物理ハードウェア上で動作し、仮想化ソフトウェア(例: VMware, VirtualBox, KVM)を通じてVMを管理します。
VM(仮想マシン)は、ゲストOSを動作させる仮想環境を提供します。ゲストOSは、VM上で動作するオペレーティングシステムであり、アプリケーションを実行します。
これに対して、zkVMのホストコードは、zkVMの初期化やゲストプログラムの入力管理、証明結果の受け取りを行いますが、zkVM内での計算やゲストプログラムの実行には直接関与しません。従って、ホストコードが行うのは、あくまでzkVMの制御であり、セットアップ(ゲストのロードやインプットの受け渡し)と結果の受領に留まります。
仮想化ソフトウェアの場合における「ホスト(OS)」は、その上でVMが動作するのに対し、zkVMの場合は、「ホスト(コード)」と協調してzkVMが動作するイメージとなります。
ゲストコードとホストコードは分離されており、zk アプリケーションを構築する際は両方を実装する必要があります。
zkVM アプリケーション(≒ ゲストコードの証明を生成する仕組み)のワークフローはおおよそ以下のとおりです。
1.ゲストコードのコンパイル
ゲストコードを ELF 形式のバイナリへ変換。
2.ホストコードのコンパイル
上記バイナリを読み込み、ゲストコードを実行するためのロジックを備えたホストコードをコンパイル。
3.ホストコードの実行
ホストコードが zkVM を制御し、ゲストコードを実行して証明を生成。
RISC Zero では、これら 3 段階を意識しなくても cargo run
コマンドひとつでホストコードの実行まで行えますが、内部的には
1)ゲストコードのコンパイル → 2) ELF 生成 → 3) ホストコードが ELF をロードして実行
という手順が踏まれているとイメージすると理解しやすいでしょう。
RISC Zero ツールチェーンをインストールすると、Rust の RISC Zero プロジェクトのテンプレートを生成するための Cargo 拡張機能が利用できます。
cargo risczero new hello-world --guest-name hello_guest
ここでは、hello_guestという名前のゲストプログラムを持つ、テンプレートが生成されます。テンプレートの内部では、既にRISC ZeroのzkVMを扱うためのクレート(ライブラリ・SDK)が組み込まれており、開発者はゲストコードとホストコードの実装のみに注力できます。
このテンプレートを用いた実装について、RISC Zeroが短いチュートリアルを公開しています。
RISC Zero では、証明や公開出力について独自の用語を使用しており、やや混乱を招く場合があります。以下、簡単に補足します。
- レシート(Receipt): ゲストコードの検証に必要なデータセット全体。ジャーナル(公開出力)とシール(証明)を含み、証明を検証するための情報を一括して保持します。
- ジャーナル(Journal): ゲストコードによる計算結果で、公開される出力。
- シール(Seal): 暗号学的に算出される STARK 証明そのもの。
- セッション(Session): ゲストコードの実行トレース。
非公開(プライベート)入力
前回の記事でも述べたように、「zk」と名の付くプロダクトであっても、ゼロ知識性はオプション扱いされるケースが増えています。基本的には「結果を算出したロジックが正しいかどうかを証明する」有効性証明が中心となっており、zkVM においても「ゲストコードで計算された結果が正しいかの証明」が主な役割です。(したがって、ゼロ知識性に対応していない zkVM のプロダクトもある点には注意しましょう。)
RISC Zero ではゼロ知識性をサポートする機能として、非公開(プライベート)入力に対応しています。ゲストコードは入力を受け取り、結果を出力する「箱」のように扱われるため、どのような入力をゲストコードに渡すかはホスト側のみが知ることになります。
ここで、証明者と検証者という 2 人のユーザーを考え、各ユーザーがそれぞれ独立して zkVM を運用しているケースを想定しましょう。すると、公開・非公開の区分は以下のように整理できます。
- ゲストコードそのもの → 公開
- ゲストコードを実行した計算結果+証明(ジャーナル)→ 公開
- ゲストコードへの入力 → 非公開
では、ホストコードはどうでしょうか。
先述のとおり、ホストコードは zkVM の管理・制御を行います。そのため「証明したい側」と「検証したい側」ではホストコードの中身が異なるはずです。
-
証明者: 「ゲストコード + 非公開入力」から結果と証明を得るよう zkVM に要求する
-
検証者: 「証明者から受け取った結果 + 証明」を暗号学的に正しいかどうか zkVM に検証させる
つまり、証明者と検証者では別々のホストコードを構築して利用している、という前提になります。したがって、証明者側のホストコードは非公開入力を知る一方、検証者側は別のホストコードを使うため、非公開入力が共有されることはありません。
証明を検証する
zkVM によって得られた「結果 + 証明」は、どのように検証できるのでしょうか。zkVM には検証機能が備わっており、検証者もホストコード経由で zkVM にアクセスする必要があります。RISC Zero では、検証者側のホストコードを以下のように記述します。
use keccak_methods::{KECCAK_ELF, KECCAK_ID};
use risc0_zkvm::{default_prover, sha::Digest, ExecutorEnv, Receipt};
//略
// 証明者から受け取ったreceiptを検証.KECCAK_IDは,ゲストコードを識別するための「イメージID」
receipt.verify(KECCAK_ID)
.expect("receipt verification failed");
println!("I provably know data whose keccak hash is {:x?}", digest);
receipt.verify()
ではレシート(結果 + 証明)の検証を行います。このときの引数に「イメージID」と呼ばれる、ゲストコードを識別するためのパラメータを渡しています。
イメージID とは、RISC Zero で定義される「ゲストコードのプログラムや初期状態を暗号学的に識別するハッシュ値」のことです。検証者側がこのイメージID を知っているということは、どのゲストコードについて証明・検証を行っているかを、証明者と検証者のあいだで合意できていることを意味します。
Groth16 などの SNARK 証明では、検証者が「検証鍵」を所持しており、これは算術回路と紐づいています。検証者が検証鍵を持っていることで、どの算術回路の証明を検証するのか、証明者との合意が取れる仕組みです。一方、STARK ベースでは「検証鍵」という概念がありません。そこで RISC Zero ではイメージID という仕組みを用い、証明したいゲストコードを暗号学的に紐づけることで、「証明者側と検証者側が同じゲストコードについて処理を行っている」ことを保証しています。
おわりに
本記事では、zkVM を実現するアプローチの整理と、RISC Zero を例にした zkVM アプリケーション構築方法の概要を紹介しました。zkVM のプロダクトごとに独自の用語が存在するため、ワークフローが分かりにくくなることもありますが、全体像を把握できればアプリケーションの開発は格段に進めやすくなるはずです。
特に、通常のプログラミングとほぼ同様の感覚でゼロ知識証明を活用できる点は、開発者にとって新鮮な体験と言えるでしょう。
次回は、具体的なユースケースを例に挙げつつ、zkVM を用いたアプリケーションをひとつ構築してみたいと思います。
-
SNARKは、Succinct Non-interactive Argument of Knowledgeの略。計算の有効性を示す証明技術。前回記事(パート1)参照。 ↩
-
カリフォルニア大学バークレー校で開発されたオープンで拡張可能な命令セットアーキテクチャ(ISA)。シンプルかつ柔軟な設計とライセンス料不要の特性により、仮想CPUを効率的かつカスタマイズ自在に開発できることからzkVM開発に採用されている。 ↩
-
S. Gao, G. Li and H. Fu, "ZKWASM: A ZKSNARK WASM Emulator," in IEEE Transactions on Services Computing, vol. 17, no. 6, pp. 4508-4521, Nov.-Dec. 2024, doi: 10.1109/TSC.2024.3422798. ↩
-
STARKは、Scalable Transparent ARgument of Knowledgeの略。前回記事(パート1)参照。 ↩
-
「一括して証明」と述べましたが、効率化のためトレース全体がセグメントに分割されて、セグメントごとに証明が算出され、これを再帰的に結合するプロセスが行われることもあります。 ↩
-
https://zkproof.org/wp-content/uploads/2024/11/Formal-Verification-of-zkWasm.pdf ↩
-
SNARK で一般的な KZG コミットメントではペアリング演算を用いるため並列化のボトルネックが多い一方、STARK ベースの FRI コミットメントは演算が独立しているため、並列化しやすいとされています。 ↩