零知识虚拟机(zkVM)把「可验证计算」从专用电路的世界拉回到通用编程模型:开发者用 Rust 写普通程序,编译器产出 RISC-V 指令,证明系统为这段执行生成一份可被链上合约验证的证明。它解决的核心问题不是「如何隐藏数据」,而是「如何让验证方相信某段程序确实被正确执行过」。
在 Rollup 状态转换证明、跨链消息验证、AI 推理可验证性等场景里,zkVM 正在替代手写电路成为首选工具。本文聚焦 RISC Zero 与 SP1 两套主流实现,从指令集设计、guest/host 模型、证明生成与聚合递归,一路讲到链上验证成本、性能基准与工程选型。
目录
- 1. zkVM 的定位与核心概念
- 2. RISC Zero 架构解析
- 3. SP1 架构解析
- 4. guest 与 host 编程模型
- 5. 证明生成流程与工作流
- 6. 证明聚合与递归
- 7. 链上验证器与验证成本
- 8. 与 zkEVM 和 Circom 的取舍
- 9. 性能基准与硬件需求
- 10. 应用场景与工程实践
1. zkVM 的定位与核心概念
1.1 从专用电路到通用虚拟机
手写电路(如 Circom)要求开发者把计算翻译成约束系统,一次 SHA-256 哈希就要几千条约束,且循环、动态内存、可变长度数组都极其难写。zkVM 把这一层抽象彻底翻转:
- 执行侧:程序照常编译成目标指令集(RISC Zero 与 SP1 都选 RISC-V),在本地解释器或模拟器中运行,产生执行轨迹(trace)。
- 证明侧:证明系统把「轨迹符合指令语义」与「初始状态到最终状态的转移正确」编码为算术约束,生成 STARK 证明。
- 验证侧:验证器只需检查证明与公开输入(public inputs),不需要重跑程序。
代价是通用性带来的开销:每条指令都要被证明系统逐条约束,虚拟机开销可达原生执行的 100 倍到 1000 倍。因此 zkVM 的核心工程命题始终是「如何在保持通用性的同时把开销压下来」。
1.2 可验证计算的三个角色
一个完整的可验证计算系统包含三类参与者,职责边界必须清晰:
| 角色 | 职责 | 信任假设 |
|---|---|---|
| Prover | 执行程序并生成证明 | 不诚实,但无法伪造有效证明 |
| Verifier | 校验证明与公开输入 | 诚实且正确实现验证逻辑 |
| Guest 程序 | 定义「被证明的是什么」 | 代码哈希被固定进验证器 |
关键设计原则是:guest 程序的身份(image ID)必须作为公开输入绑定进证明。否则攻击者可以拿一段合法但语义不同的程序生成证明来冒充。RISC Zero 的 image_id 与 SP1 的 vk_hash 都承担这个角色。
2. RISC Zero 架构解析
2.1 RISC-V 指令集与执行模型
RISC Zero 选择 RV32IM(32 位整数、乘法扩展)作为 guest 指令集。选择 32 位而非 64 位是刻意的权衡:寄存器宽度直接决定约束规模,32 位可以让每个周期(cycle)的约束数量显著下降,而绝大多数验证逻辑(哈希、签名校验、状态根计算)并不需要 64 位运算。
其证明系统经历了几代演进:早期基于 RISC-V 的 STARK 证明,随后引入**连续证明(continuations)**把长执行切成多段,每段生成一个证明再递归组合,从而让任意长度程序的证明内存占用保持有界。这是 zkVM 能处理上亿 cycle 执行的关键。
// RISC Zero 中一次可验证执行的最小骨架(host 侧)
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts};
let env = ExecutorEnv::builder()
.write(&input_data)? // 写入 guest 可读的私有输入
.build()?;
let prover = default_prover();
let receipt = prover.prove(env, GUEST_ELF)?; // 生成证明
receipt.verify(GUEST_ID)?; // 本地自检
let output: Output = receipt.journal.decode()?; // 读取公开输出
2.2 证明系统与递归组合
RISC Zero 的证明默认输出 STARK receipt,体积大、验证 gas 高;要上链通常再用 Groth16 包装(wrap),把证明压到约 200 字节、验证成本降到几十万 gas。递归组合有两条路线:
- 同构递归:STARK 证明 STARK,全程无需可信设置,但验证器实现复杂、链上不可行。
- 异构包装:STARK 证明最终交给 Groth16 电路验证,需要一次可信设置(通常用多方可信仪式),但链上验证极便宜。
生产系统几乎都走第二条路:链下递归聚合,链上一次 Groth16 验证。
3. SP1 架构解析
3.1 预编译与加速指令
SP1 的核心差异化在于预编译(precompiles):把高频且证明昂贵的操作(keccak256、sha256、secp256k1 签名校验、bn254 配对、ed25519)做成专用加速电路,guest 里调用这些操作时不再逐条展开 RISC-V 指令,而是走一条约束更紧凑的专用路径。
// SP1 guest 中调用预编译(以 secp256k1 恢复为例)
use sp1_zkvm::prelude::*;
pub fn main() {
let msg_hash: [u8; 32] = sp1_zkvm::io::read();
let sig: [u8; 65] = sp1_zkvm::io::read();
let recovered = sp1_zkvm::lib::secp256k1::ecrecover(&sig, &msg_hash);
sp1_zkvm::io::commit(&recovered); // 公开输出
}
一个 keccak256 预编译相比纯指令实现可以带来数量级的加速。对以太坊状态验证这类 keccak 密集负载,预编译覆盖率几乎决定了整体成本。
3.2 可插拔的证明后端
SP1 把证明后端做成可替换组件:早期依赖 Plonky3,现在同时支持不同的字段与哈希组合,并允许按场景选择是否启用 lookups(如 LogUp)。这种「核心虚拟机 + 加速器 + 可换后端」的分层设计,使得升级证明系统不必重写 guest 程序。
代价是复杂度:预编译的语义必须与 RISC-V 实现严格一致,任何差异都会造成「同一条 guest 代码在不同模式下结果不同」的共识风险。因此 SP1 的预编译都有对应的纯指令回退路径,并靠差分测试保证一致性。
4. guest 与 host 编程模型
4.1 guest 侧的约束
guest 代码运行在受限环境里,几乎所有 zkVM 都要求 no_std:
- 不可用系统调用:文件 IO、网络、时间、随机数一律不可用,所有输入必须由 host 通过指定通道注入。
- 不可用浮点:浮点运算难以高效约束,guest 里必须用定点数或整数运算替代。
- 内存受限:guest 的内存使用会直接放大证明成本,应避免大缓冲区。
- 确定性:guest 必须是纯函数式的,任何非确定性都会让证明无法复现。
// guest 侧:读输入、计算、提交公开输出
#![no_main]
sp1_zkvm::entrypoint!(main);
pub fn main() {
let n: u64 = sp1_zkvm::io::read();
let mut acc: u64 = 0;
for i in 0..n {
acc = acc.wrapping_add(i); // 溢出行为必须显式,避免 UB
}
sp1_zkvm::io::commit(&acc);
}
4.2 host 侧的职责
host 是普通的 Rust 程序,负责准备输入、驱动证明、处理产物:
| 环节 | host 侧动作 | 常见坑 |
|---|---|---|
| 输入准备 | 序列化私有输入 | 字节序与类型不一致 |
| 执行 | execute 快速跑一遍拿输出 | 与 prove 结果不一致说明有非确定性 |
| 证明 | prove 生成 receipt | 内存峰值常是执行阶段的数十倍 |
| 验证 | 校验 image id 与 journal | 忘记校验 image id 等于放弃安全 |
| 提交 | 把 journal 与证明发到链上 | journal 编码格式须与合约约定一致 |
先用 execute 拿输出、再 prove 是最省时的调试模式:执行通常毫秒级,而证明可能几分钟,先确认逻辑再付出证明成本。
5. 证明生成流程与工作流
5.1 从执行轨迹到证明
证明生成的内部阶段可以粗分为四步:
- 轨迹生成:解释执行 guest,逐周期记录寄存器与内存状态。
- 约束编码:把每条指令的语义转换为算术约束,用 AIR 或类似形式表达。
- 多项式承诺:对轨迹列做低度扩展并承诺(FRI / Merkle),得到 STARK 证明。
- 证明包装:递归压缩后再用 Groth16 包装成常量大小证明。
其中第 1 步的成本被低估得最多:轨迹规模与执行 cycle 数成正比,而 cycle 数又受 guest 实现细节影响极大——一次不必要的内存拷贝可能让证明时间翻倍。
5.2 完整工程流水线
开发流水线:
1. 写 guest(Rust, no_std)→ cargo build 产出 ELF
2. 计算 image id / vk hash → 写入部署脚本
3. host 侧 execute → 校验输出与预期一致
4. host 侧 prove → 生成 STARK receipt
5. wrap → Groth16 proof + journal
6. 部署 Verifier 合约(绑定 image id)
7. 链上调用 verify(proof, publicInputs) → true/false
第 2 步的 image id 必须与链上验证器绑定的值一致,否则所有证明都会被拒绝。工程上应把它纳入版本管理与 CI 校验,避免 guest 改动后忘记同步部署。
6. 证明聚合与递归
6.1 递归证明的两种形态
**递归证明(recursive proof)**指用一个证明去证明「另一个证明是有效的」。在 zkVM 里它有两个用途:
- 切分长执行:把百万 cycle 的执行切成 N 段,每段一个证明,再递归合并成单个证明。这让内存占用与执行长度解耦。
- 批量聚合:把来自不同用户、不同程序的多个证明聚合成一个,链上只验证一次,摊薄验证成本。
6.2 聚合成本模型
设单条证明的生成成本为 C_prove、链上验证成本为 C_verify,聚合 n 条后:
聚合总成本 ≈ n * C_prove + (n-1) * C_recursive + 1 * C_wrap
链上成本 = 1 * C_verify (与 n 无关)
当 n 较大时,链上成本被摊薄到接近零,而链下成本线性增长。因此聚合的收益边界取决于「链上 gas 单价 / 链下证明成本」的比值——在 gas 昂贵的 L1 上,聚合几乎总是划算的。
| 方案 | 链上验证成本 | 链下成本 | 适用场景 |
|---|---|---|---|
| 单条 STARK | 极高(数千万 gas) | 低 | 链下验证 |
| 单条 Groth16 | 约 25 万 gas | 中 | 单次验证 |
| N 条递归聚合 | 约 25 万 gas | 高(线性) | 批量结算 |
| 递归 + 状态延续 | 约 25 万 gas | 高 | 长执行证明 |
7. 链上验证器与验证成本
7.1 Groth16 包装与验证合约
链上验证的标准形态是 Groth16:证明是三个群元素,验证只需常数次配对运算。
// 简化后的验证接口(Groth16 verifier 由工具链生成)
interface IVerifier {
function verifyProof(
uint256[2] calldata a,
uint256[2][2] calldata b,
uint256[2] calldata c,
uint256[] calldata publicSignals
) external view returns (bool);
}
contract ZkApp {
IVerifier public immutable verifier;
bytes32 public immutable imageId; // 绑定 guest 程序身份
function submit(uint256[2] calldata a, uint256[2][2] calldata b,
uint256[2] calldata c, uint256[] calldata signals) external {
require(verifier.verifyProof(a, b, c, signals), "INVALID_PROOF");
// signals[0] 应为 imageId,防止换程序冒充
require(uint256(imageId) == signals[0], "WRONG_PROGRAM");
_applyState(signals);
}
}
7.2 成本量级
验证成本主要由配对运算主导,与证明所描述的计算规模无关——这正是 zkVM 的经济学基础。
| 验证方式 | 链上 gas 量级 | 说明 |
|---|---|---|
| STARK 原生验证 | 10^7 级 | 通常只在 L2 或链下可行 |
| Groth16(BN254) | 约 25 万 | 主流上链方案 |
| Plonk 类 | 约 30 万 | 通用可信设置,验证稍贵 |
| 证明 + 状态更新 | 25 万 + 存储写 | 实际 DApp 的总成本 |
注意公开输入的个数也会带来线性成本:每个 public input 在验证时都要参与 MSM 运算,把大量数据塞进 public inputs 会显著推高 gas。工程上应把「需要链上读取的数据」与「仅供链下审计的数据」分开,后者用 journal 摘要代替。
8. 与 zkEVM 和 Circom 的取舍
8.1 三类技术路线对比
| 维度 | Circom 电路 | zkEVM | zkVM |
|---|---|---|---|
| 编程模型 | 手写约束 | EVM 字节码 | RISC-V 指令 |
| 通用性 | 极低 | 中(限 EVM 语义) | 高(任意 Rust) |
| 开发效率 | 低 | 中 | 高 |
| 证明效率 | 极高 | 中 | 较低 |
| 生态工具 | 成熟 | 快速演进 | 快速演进 |
| 典型用途 | 特定算法证明 | L2 执行证明 | 跨链、AI、通用计算 |
zkEVM 本质是「为 EVM 语义定制的 zkVM」:它用大量预编译与查找表把 EVM 高频操作做快,代价是只能证明 EVM 执行。zkVM 反过来,牺牲部分效率换取通用性。
8.2 选型决策
- 算法固定且对成本极度敏感(如混币协议、特定签名方案):手写 Circom。
- 目标是证明 EVM 执行(Rollup、L2 结算):zkEVM。
- 需要证明任意程序逻辑(跨链消息、AI 推理、游戏状态):zkVM。
- 需要快速原型:zkVM,因为 Rust 生态与调试体验远优于电路。
实务中常见组合:用 zkVM 做业务逻辑证明,把最热的哈希与签名部分交给预编译或外部电路,形成混合架构。
9. 性能基准与硬件需求
9.1 证明开销构成
| 阶段 | 占比量级 | 优化手段 |
|---|---|---|
| 执行与轨迹生成 | 5%~15% | 减少 cycle、避免大内存 |
| 约束求值与承诺 | 50%~70% | 提高预编译覆盖率 |
| 递归聚合 | 10%~30% | 减少分段数、合并证明 |
| Groth16 包装 | 5%~15% | 固定流程,难以优化 |
预编译覆盖率是最大的杠杆:一段纯 RISC-V 的 keccak 实现与预编译版本的差距可达 50 倍以上。
9.2 硬件建议
开发调试: 8 核 / 32GB execute 模式足够
小规模证明:16 核 / 64GB 单机即可
生产证明: 32~64 核 / 256GB 内存是主要瓶颈
批量聚合: 多机集群 + GPU 部分后端支持 GPU 加速
内存而非 CPU 通常是瓶颈:轨迹与多项式承诺的中间数据规模远超直觉。生产环境应把证明服务与业务服务物理隔离,避免证明任务把节点内存吃满。
10. 应用场景与工程实践
10.1 典型场景
- 跨链证明:证明「源链上某笔交易确实发生」,目标链验证后放行,取代多签中继。
- Rollup 状态转换:证明状态根转移的正确性,比 zkEVM 更容易支持自定义逻辑。
- AI 推理验证:证明模型在给定输入上确实产出了该输出,用于去中心化推理市场。
- 链下计算上链:把复杂策略回测、风险计算的结果以证明形式提交,链上只验证。
- 隐私应用:结合隐私输入,证明「我有资格但不说我是谁」。
10.2 工程落地清单
- 固定 guest 版本:image id 与部署合约必须成对更新,纳入 CI 校验。
- 区分公开与私有输入:只有必须被链上读取的才进 public inputs,其余留在 journal。
- 先用 execute 再 prove:把非确定性 bug 挡在昂贵的证明阶段之前。
- 设置证明超时与重试:证明服务会因内存压力失败,需要任务队列与幂等重试。
- 监控 cycle 数:cycle 数是成本的第一指标,应在 CI 中对 guest 改动做回归对比。
- 审计 guest 逻辑:证明保证「执行正确」,不保证「逻辑正确」,业务漏洞依然存在。
10.3 速查表与一句话记忆
| 概念 | 关键点 | 代表实现 |
|---|---|---|
| zkVM | 通用指令集 + 证明系统 | RISC Zero、SP1 |
| image id | 绑定 guest 程序身份 | 必须作为公开输入校验 |
| 预编译 | 高频操作专用加速电路 | SP1 keccak、secp256k1 |
| 连续证明 | 长执行切段递归 | RISC Zero continuations |
| Groth16 包装 | 压缩到常量大小上链 | 约 25 万 gas 验证 |
| 递归聚合 | N 条证明合并为一条 | 摊薄链上成本 |
| no_std | guest 的运行时约束 | 无浮点、无系统调用 |
一句话记忆:zkVM 用通用性换开发效率,用预编译覆盖率和递归聚合把成本压回可用区间。
延伸阅读
- 电路约束与 Circom 工具链,理解 zkVM 要替代的那一层
- 零知识证明的信任模型、可信设置与隐私语义
- Rollup 架构与证明系统在 L2 中的位置
- L2 扩容路线与证明成本的经济学
- 智能合约验证器的安全边界与常见陷阱
- Web3 区块链专题 — 区块链 Web3 专题
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。