zkVM 实战:RISC Zero 与 SP1 的架构对比与证明生成流程

系统对比 RISC Zero 与 SP1 两套 zkVM 的架构与工程模型:RISC-V 指令集与预编译加速、guest/host 编程模型、证明生成与聚合递归、链上验证器成本,以及与 zkEVM、Circom 的取舍和性能基准。

零知识虚拟机(zkVM)把「可验证计算」从专用电路的世界拉回到通用编程模型:开发者用 Rust 写普通程序,编译器产出 RISC-V 指令,证明系统为这段执行生成一份可被链上合约验证的证明。它解决的核心问题不是「如何隐藏数据」,而是「如何让验证方相信某段程序确实被正确执行过」。

在 Rollup 状态转换证明、跨链消息验证、AI 推理可验证性等场景里,zkVM 正在替代手写电路成为首选工具。本文聚焦 RISC Zero 与 SP1 两套主流实现,从指令集设计、guest/host 模型、证明生成与聚合递归,一路讲到链上验证成本、性能基准与工程选型。

前置:理解电路约束与证明系统是读懂 zkVM 的前提,零知识证明的信任模型与隐私语义。


目录


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 从执行轨迹到证明

证明生成的内部阶段可以粗分为四步:

  1. 轨迹生成:解释执行 guest,逐周期记录寄存器与内存状态。
  2. 约束编码:把每条指令的语义转换为算术约束,用 AIR 或类似形式表达。
  3. 多项式承诺:对轨迹列做低度扩展并承诺(FRI / Merkle),得到 STARK 证明。
  4. 证明包装:递归压缩后再用 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 电路zkEVMzkVM
编程模型手写约束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_stdguest 的运行时约束无浮点、无系统调用

一句话记忆:zkVM 用通用性换开发效率,用预编译覆盖率和递归聚合把成本压回可用区间。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「区块链 Web3」更多文章

  1. DeFi 风险管理与清算:抵押率、清算机制与坏账处置
  2. MPC 钱包与密钥管理:门限签名、2-of-3 架构与攻击面分析
  3. 形式化验证实战:Certora、Halmos 与 Echidna 的规约、边界与 CI 集成