导语:把写电路变成写程序
零知识证明曾经是电路工程师的专属领域:为了证明一段逻辑,你必须把它翻译成 R1CS 约束,逐门优化,再为它做一次可信设置。zkVM(Zero-Knowledge Virtual Machine,零知识虚拟机)改变了这件事——它把「写电路」变成了「写程序」。你用 Rust 写一个普通函数,编译成 RISC-V 指令,zkVM 就能为这段程序的执行生成一份密码学证明。
一句话总结:zkVM 用「通用指令集的解释器电路」替代了「每个应用一份专用电路」,代价是每条指令都要被约束,收益是任何程序无需改电路即可被证明。
zkVM 与证明市场是一枚硬币的两面:前者解决了「证明什么」的通用性问题,后者解决了「谁来证明、多少钱、多快」的供给问题。当证明生成变成一种可外包、可竞价的服务,链上应用就获得了「按需计算」的能力——这正是证明市场试图构建的经济层。
1. zkVM 的概念与指令集选择
1.1 从专用电路到通用虚拟机
专用电路(如 circom 写的 Merkle 证明)把业务逻辑直接编码进约束系统,效率极高但毫无复用性:改一行逻辑就要重做可信设置、重新审计、重新部署验证合约。zkVM 的思路是把「执行」抽象出来:
应用代码(Rust/C/C++)
│ 编译
▼
目标指令集(RISC-V RV32IM)
│ 执行
▼
执行轨迹(execution trace):每一步的寄存器、内存、程序计数器
│ 算术化(arithmetization)
▼
约束系统:约束「每一步都是正确的指令语义」
│ 多项式承诺 + FRI / KZG
▼
证明(STARK 或 SNARK)
│ 验证
▼
链上验证合约:只需知道「程序映像 ID」与「公开输出」
关键洞察在于:约束系统只需要描述指令语义(比如「add 指令:rd = rs1 + rs2」),而与应用无关。因此同一套 zkVM 可以证明任意程序,链上只需要一个验证合约,通过程序映像 ID(program image ID,即编译产物 ELF 的哈希)区分不同的 guest 程序。
1.2 为什么 RISC-V 成为事实标准
选择指令集不是审美问题,而是证明成本问题。RISC-V 胜出的原因可以拆成四点:
| 维度 | RISC-V 的优势 | 对证明成本的影响 |
|---|---|---|
| 指令集规模 | 基础指令少且规整(RV32IM 约 40 余条) | 需要实现的约束门更少,电路更小 |
| 解码规则 | 定长 32 位、字段位置固定 | 解码约束可用查表实现,无需复杂分支 |
| 生态与工具链 | LLVM/GCC 后端成熟,Rust 一等公民 | guest 代码几乎零改动即可编译 |
| 可扩展性 | 预留 custom opcode 空间 | 可为 precompile 分配专用指令 |
对比之下,x86 是变长指令、解码极其复杂,把 x86 完整塞进约束系统会让解码本身成为瓶颈;EVM 则是为区块链设计的栈式字节码,虽然语义清晰,但它是领域专用的——用它做通用计算会非常别扭(这也是 zkEVM 与 zkVM 的分野起点)。
1.3 zkVM 与 zkEVM 的分野
很多人把两者混为一谈,但它们的优化目标相反:
| 维度 | zkVM | zkEVM |
|---|---|---|
| 目标 | 证明任意程序的执行 | 证明以太坊区块/交易的状态转换 |
| 指令集 | RISC-V(通用) | EVM 字节码(领域专用) |
| 兼容性诉求 | 语言级(Rust/C 可编译即可) | 字节码级(EVM 等价) |
| 典型场景 | 跨链桥、预言机、ZK 机器学习、协处理器 | Rollup、L2 状态证明 |
| 证明开销来源 | 通用指令的解释成本 | EVM 语义(存储、gas、状态)的建模成本 |
| 代表项目 | RISC Zero、SP1、Jolt、Nexus | Scroll、Polygon zkEVM、zkSync Era |
一句话总结:zkEVM 追求「让以太坊跑得动证明」,zkVM 追求「让任意程序跑得动证明」——前者为 L2 服务,后者为链上协处理器与跨链验证服务。
2. RISC Zero:通用 zkVM 的工程范式
2.1 guest 与 host 的分工
RISC Zero 把程序分成两半,这个划分是理解一切 zkVM 的钥匙:
- guest:运行在 zkVM 内部的代码,它的每一步执行都会被约束、被证明。guest 只能读取显式传入的输入,写出的输出会进入 journal。
- host:运行在 zkVM 外部的普通程序(你的主程序),它负责准备输入、调用 prover、拿到 receipt、提交到链上。host 的计算不被证明,因此可以随意调用网络、文件系统、GPU。
// methods/guest/src/main.rs —— 运行在 zkVM 内部,逐条指令被约束
use risc0_zkvm::guest::env;
use sha2::{Digest, Sha256};
fn main() {
// 从 host 读取私有输入:这些字节不会出现在证明的公开部分
let secret: Vec<u8> = env::read();
let commitment: [u8; 32] = env::read();
let mut hasher = Sha256::new();
hasher.update(&secret);
let digest: [u8; 32] = hasher.finalize().into();
// 断言失败 → 无法生成有效证明(这是可靠性的来源)
assert_eq!(digest, commitment, "secret does not match commitment");
// 写入 journal:公开输出,随 receipt 一起上链
env::commit(&digest);
}
// host/src/main.rs —— 运行在 zkVM 外部,不被证明
use risc0_zkvm::{default_prover, ExecutorEnv};
use methods::{HASH_GUEST_ELF, HASH_GUEST_ID};
fn main() {
let secret = b"correct horse battery staple".to_vec();
let commitment: [u8; 32] = Sha256::digest(&secret).into();
let env = ExecutorEnv::builder()
.write(&secret).unwrap()
.write(&commitment).unwrap()
.build().unwrap();
// 默认生成 STARK 证明;如需链上验证则包装为 Groth16
let receipt = default_prover().prove(env, HASH_GUEST_ELF).unwrap();
// 本地先自检一遍,避免把无效证明提交到链上浪费 gas
receipt.verify(HASH_GUEST_ID).unwrap();
// 上链三要素:seal(证明字节)、image_id(程序标识)、journal(公开输出)
println!("image_id = {:?}", HASH_GUEST_ID);
println!("journal = {:?}", receipt.journal.bytes);
}
2.2 receipt 与 journal
RISC Zero 把「证明」拆成三个可独立传递的部件,理解它们的边界是避免踩坑的前提:
- journal:guest 通过
env::commit()写出的公开输出,本质是一段字节串。链上合约只信任 journal 的内容,因为它是被证明覆盖的。 - seal:真正的证明字节。STARK 形态下是几十到几百 KB;Groth16 包装后约 256 字节。
- image_id:guest ELF 的哈希,标识「这段程序」的身份。链上验证时必须校验 image_id,否则攻击者可以用一个更弱的 guest 程序生成「合法」证明。
关键安全边界:journal 里的数据是被证明的,但读取 journal 的合约必须自己校验 image_id。很多早期集成事故的根因就是验证合约只验了证明有效,却没绑定 image_id,导致任何人可以用自己的程序伪造 journal 内容。
2.3 Groth16 包装与链上验证
STARK 证明无法在以太坊上直接验证(验证成本数百万 gas)。RISC Zero 的做法是:在链下用递归电路把整个 STARK 验证过程再证明一次,输出一个 Groth16 证明(seal),链上只需一个约 250k gas 的配对检查。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol";
contract CommitReveal {
IRiscZeroVerifier public immutable verifier;
// 只信任这一个 guest 程序:image_id 是编译产物 ELF 的哈希
bytes32 public immutable imageId;
bytes32 public latestCommitment;
constructor(IRiscZeroVerifier _verifier, bytes32 _imageId) {
verifier = _verifier;
imageId = _imageId;
}
function submit(bytes calldata seal, bytes calldata journal) external {
// 1) 验证证明:seal 覆盖了 sha256(journal),且 imageId 必须是白名单程序
verifier.verify(seal, imageId, sha256(journal));
// 2) journal 已被证明覆盖,可以安全解码业务数据
bytes32 commitment = abi.decode(journal, (bytes32));
latestCommitment = commitment;
}
}
3. SP1:RISC-V zkVM 的性能路线
3.1 SP1 的整体架构
SP1 是 Succinct 团队推出的 RISC-V zkVM,工程目标非常明确:把每周期证明成本压到最低。它的核心设计选择是「不用通用查表证明一切,而是为高频操作开专用通道」:
SP1 证明栈:
Rust guest 代码
│ rustc (riscv32im-succinct-zkvm-elf)
▼
SP1 核心 VM(RV32IM + 自定义 opcode)
│ 执行 → 轨迹分片(shard)
▼
分片证明:每个 shard 生成一个 STARK 证明
│ 递归聚合(core → compress → shrink → wrap)
▼
PlonK 证明(中间态,约 1 秒验证)
│ 再包装
▼
Groth16 证明(约 256 字节,链上约 250k gas)
分片(shard)是关键:单次执行被切成固定大小的片段并行证明,再通过递归把多个分片证明聚合成一个,这既控制了内存占用,也让 GPU 集群可以横向扩展。
3.2 precompile 与预编译表
纯 RV32IM 解释器证明的致命弱点是:一条 SHA-256 压缩函数要拆成上千条基础指令,证明成本被放大几个数量级。SP1 与 RISC Zero 都引入了 **precompile(预编译)**机制:把高频密码学操作做成专用约束,成本降到原来的几十分之一。
| 操作 | 纯 RV32IM 证明成本 | 使用 precompile | 典型加速比 |
|---|---|---|---|
| SHA-256 压缩 | 约 2000 周期 | 约 30 周期 | 60 倍以上 |
| Keccak-256 | 约 3000 周期 | 约 40 周期 | 70 倍以上 |
| secp256k1 签名恢复 | 数十万周期 | 约 2000 周期 | 100 倍以上 |
| bn254 配对 | 百万周期量级 | 约 1 万周期 | 100 倍以上 |
代价是:precompile 一旦被使用,guest 程序就与特定 zkVM 版本绑定,无法跨 zkVM 移植;而且 precompile 的约束若写错,会引入健全性漏洞——这是审计 zkVM 时最需要盯的地方。
3.3 PlonK 与 Groth16 的两段式证明
SP1 的证明管线是典型的「两段式」:STARK 负责处理大计算量(透明、可并行、适合 GPU),SNARK 负责链上验证(简洁、验证快)。
STARK(core 证明,每个 shard 一个)
→ 递归聚合(recursion)
→ shrink:压缩证明大小,降低后续电路规模
→ wrap:转为 PlonK(约 1 秒验证,可用于链下或 L2)
→ 可选再包装为 Groth16(链上最省 gas)
工程取舍:如果验证发生在你自己的 L2 或链下服务上,PlonK 已经够用(约 1 秒验证但 gas 更高);如果验证发生在以太坊 L1,几乎一定要走 Groth16 包装,因为包装本身要花 10~60 秒。
4. 证明生成与验证的完整流程
4.1 执行轨迹与 witness 生成
一切从执行轨迹开始。zkVM 逐条执行 guest 指令,把每一步的状态记录下来:
轨迹的每一行 = 一个「执行周期」(cycle):
[ pc, 指令字, rs1_val, rs2_val, rd_val, 内存读写地址与值, 内存 Merkle 根 ]
一个 100 万周期的 guest 程序,轨迹就有 100 万行。这个规模决定了后面所有成本:轨迹越大,约束系统越大,多项式承诺的计算量越大,证明时间越长。
4.2 约束系统与多项式承诺
轨迹本身不是证明,它需要被「算术化」成约束。核心约束分三类:
- 执行约束:每一步的 pc、寄存器、内存变化必须符合指令语义(如
add要求 rd = rs1 + rs2)。 - 连续性约束:上一步的内存状态与下一步必须一致,程序计数器转移必须合法。
- 边界约束:初始状态与最终状态必须与公开输入/输出一致。
然后把约束转成多项式恒等式,用多项式承诺(STARK 用 FRI,SNARK 用 KZG)来承诺,并证明「这些多项式在某个随机点上满足恒等式」。验证者只需检查少数几个点,这就是简洁性(succinctness)的来源。
4.3 从证明到链上验证
1. 生成阶段(链下、GPU 集群)
guest ELF + 输入 → 执行 → 轨迹 → 约束 → STARK 证明(分片)
2. 聚合阶段(链下、递归)
多分片 STARK → 递归证明 → shrink → wrap → PlonK/Groth16
3. 提交阶段(链上)
交易携带 seal + journal(几百字节到 256 字节)
4. 验证阶段(链上、常数时间)
验证合约:配对检查 + sha256(journal) 比对 + image_id 白名单校验
链上验证的成本是常数的,与 guest 执行了多久无关——这正是 zkVM 最迷人的性质:链下算一小时,链上验证一毫秒。
5. 证明聚合与递归
5.1 递归证明的两种形态
递归证明(recursive proof)指「证明中包含另一个证明的验证」。它在 zkVM 里有两种用途:
- 同构递归:用同一个 zkVM 证明「我验证了前一个证明且执行了新的一个分片」。这是分片聚合的基础,SP1/RISC Zero 都用它把 N 个分片证明压成 1 个。
- 异构递归:用一个证明系统去证明另一个证明系统的验证器(如「用 RISC Zero 证明 Groth16 验证通过」),这是 STARK 到 SNARK 包装的本质。
5.2 证明聚合的工程实现
分片 1 → STARK 证明 P1
分片 2 → STARK 证明 P2
分片 3 → STARK 证明 P3
│
├─ 递归节点 A:证明「P1 有效 且 P2 有效」 → PA
└─ 递归节点 B:证明「PA 有效 且 P3 有效」 → PB
│
▼
最终证明 PB:大小恒定,与分片数量无关
聚合的价值不只是「变小」,更是让验证成本不随计算量线性增长。一个跑 100 亿周期的 guest,如果不用聚合,链上要验证 1 万个分片证明;聚合后仍然只验证一个。
5.3 递归带来的成本结构变化
递归不是免费的午餐,它有自己的成本特征:
| 阶段 | 成本量级 | 说明 |
|---|---|---|
| 核心 STARK 证明 | 与周期数近似线性 | 主要开销,可 GPU 并行 |
| 递归聚合 | 每个递归节点固定开销 | 节点数约为分片数的对数或线性 |
| shrink 与 wrap | 固定开销,10~60 秒 | 与电路规模相关,难以并行 |
| 链上验证 | 约 250k gas | 与计算量无关 |
实践中的坑:分片切得太小,递归节点数爆炸,聚合成本反而超过证明成本;切得太大,单分片内存溢出(OOM)。SP1 的默认分片大小(约 2^21 周期)是经过大量调优的经验值,不要随意改。
6. STARK 到 SNARK 的包装与成本
6.1 为什么必须做包装
STARK 在以太坊上不可直接验证,原因很实在:FRI 验证需要做几十次 Merkle 路径校验和哈希运算,链上执行要数百万 gas。包装(wrapping)的思路是:把「STARK 验证器的逻辑」写成一段 guest 程序,用 zkVM 再证明一次,输出一个 Groth16 证明。
STARK 证明(几百 KB,链下验证约 10 ms)
│ 作为输入喂给一个「STARK 验证器 guest」
▼
Groth16 证明(约 256 字节,链上验证约 250k gas)
6.2 包装的成本模型
包装是固定成本,与原始计算量无关,因此对「小计算」而言包装可能比证明本身还贵:
总成本 = 核心证明成本(周期数 × 每周期单价)
+ 聚合成本(分片数 × 递归单价)
+ 包装成本(固定,约 10~60 秒 GPU 时间)
当周期数很小时(如 10 万周期),包装成本占比可达 80% 以上
当周期数很大时(如 10 亿周期),包装成本被摊薄到可忽略
结论:小任务不要走 Groth16 包装,直接用 STARK 或 PlonK 在链下/L2 验证更划算。这也是很多「用 zkVM 证明一个小哈希」的教程看起来昂贵的原因——它们的瓶颈不在证明而在包装。
6.3 验证合约的 gas 对比
| 证明系统 | 证明大小 | 链上验证 gas | 是否需可信设置 | 备注 |
|---|---|---|---|---|
| Groth16 | 约 256 字节 | 约 250k | 需要(电路特定) | 链上最省 |
| PlonK | 约 800 字节 | 约 300k | 需要(通用 setup) | 验证稍慢 |
| STARK(链上) | 数十 KB | 数百万 | 不需要 | 实际不可行 |
| 无证明(直接执行) | — | 与计算量成正比 | — | 无简洁性 |
Groth16 是链上验证的默认选择,代价是每个 guest 程序(严格说是每个电路)都需要一次 setup,且 setup 结果随 guest 代码变化而失效——所以guest 程序一旦部署就不要轻易改,改了就要重新 setup 并更新链上验证密钥。
7. 证明市场与激励设计
7.1 prover network 的角色构成
单机证明有两个问题:成本高(需要 GPU 集群)与可用性差(单点故障)。证明市场把证明生成外包给一个去中心化的网络:
| 角色 | 职责 | 激励来源 |
|---|---|---|
| 请求方(requester) | 提交证明请求与悬赏 | 获得证明,解锁业务 |
| 证明者(prover) | 运行 GPU 集群生成证明 | 获得悬赏与代币奖励 |
| 验证者(verifier) | 抽样验证证明正确性 | 质押收益与举报奖励 |
| 协议(protocol) | 匹配、结算、罚没 | 协议费 |
7.2 拍卖与竞价机制
证明市场本质上是一个计算资源拍卖行,主流设计有三种:
- 请求出价(request-for-quote):请求方挂出任务与最高价,prover 竞价接单。简单但容易价格战。
- 荷兰拍(Dutch auction):价格从高往低递减,直到有 prover 接单。适合有明确时限的任务。
- 批量拍卖(batch auction):把一段时间窗口内的所有请求打包,统一分配与定价。能缓解抢跑与价格波动,是较先进的方案。
一次典型竞价流程:
1. 请求方锁定悬赏金额(escrow)
2. 广播任务元数据:image_id、输入哈希、周期上限、截止时间
3. prover 提交报价(可加密,避免互相窥探)
4. 协议选出中标者(最低价 / 最可靠 / 加权评分)
5. prover 生成证明并提交
6. 链上验证通过 → 释放悬赏;失败 → 罚没质押
7.3 质押、罚没与经济模型
没有质押就没有可靠性。证明市场的核心安全假设是:作恶的成本必须高于收益。典型设计:
- prover 需质押代币才能接单,罚没金额与任务价值挂钩。
- 提交错误证明 → 全额罚没 + 信誉归零。
- 超时未提交 → 部分罚没,任务重新分配。
- 引入信誉分:高分 prover 可接更大额任务、获得优先匹配,形成正循环。
一个容易被忽略的激励缺口是验证者困境:如果验证成本高于举报奖励,理性参与者不会验证,作恶就有空间。主流做法是乐观抽样验证——随机抽取一小部分证明做冗余验证,用概率保证安全,同时把验证成本摊薄。
7.4 成本与延迟评估
以下数据是量级参考(实际随硬件、zkVM 版本、电路复杂度显著波动),用于做容量规划:
| 指标 | CPU 单机 | 中端 GPU 单卡 | 8 卡 GPU 集群 |
|---|---|---|---|
| STARK 证明吞吐 | 约 1M 周期/秒 | 约 10M 周期/秒 | 约 80M 周期/秒 |
| 1000 万周期证明耗时 | 约 10 秒 | 约 1 秒 | 约 0.15 秒 |
| 10 亿周期证明耗时 | 约 17 分钟 | 约 100 秒 | 约 15 秒 |
| Groth16 包装 | 不推荐 | 约 30~60 秒 | 约 20~40 秒 |
| 每周期成本(估算) | 基准 1x | 约 0.1x | 约 0.02x |
延迟分层:STARK 证明可以做到秒级,但加上聚合与 Groth16 包装后,端到端延迟通常落在 30 秒到数分钟。如果业务要求「亚秒级最终性」,zkVM 包装路线基本不可行,应改用可信执行环境(TEE)或委员会签名方案。
7.5 工程落地与常见踩坑
- 忘记校验 image_id:验证合约只验证明有效而不校验程序身份,等于允许任何人用任意程序伪造 journal。这是最高频、最严重的集成漏洞。
- guest 代码里用了不支持的标准库:guest 运行在
no_std或受限环境,std::fs、std::net、std::thread全部不可用,必须改写。 - guest 计算量失控:一个不小心引入的
println!或循环会把周期数推高一个数量级,证明成本爆炸。务必用env::cycle_count()或 profiling 工具盯住周期数。 - 误以为 host 的计算被证明:host 里做的任何校验都是「本地自检」,不可信。只有 journal 里的内容才是被证明的。
- 包装成本被忽略:小任务硬上 Groth16,成本主要花在包装上。评估时一定要算端到端成本,而不是只看 STARK 证明时间。
- precompile 版本漂移:升级 zkVM 版本后 precompile 约束可能变化,验证密钥与 image_id 都会变,必须同步更新链上合约,否则旧证明全部失效。
- 未做证明重放防护:同一份证明可以被重复提交。合约必须维护 nonce 或已消费 journal 哈希集合。
- 低估内存:大电路证明的峰值内存可达数百 GB,节点规格不足会 OOM 而不是变慢,扩容前先确认内存曲线。
8. 总结
- zkVM 的本质:用通用指令集解释器电路换取应用无关性,代价是每条指令都要被约束。
- RISC-V 是工程最优解:指令规整、工具链成熟、可扩展 opcode,让 guest 代码几乎零改动。
- RISC Zero 范式:guest 被证明、host 不被证明,receipt 由 seal、journal、image_id 三部分组成,链上验证必须校验 image_id。
- SP1 的性能路线:precompile 把密码学操作成本降两个数量级,分片加递归让 GPU 集群可横向扩展。
- 证明流程:执行轨迹到约束到多项式承诺再到链上常数时间验证,链上成本与计算量解耦。
- 递归与聚合:让验证成本不随计算量线性增长,但分片粒度选择直接影响总成本与内存。
- 包装是固定成本:小任务不要走 Groth16,端到端成本才是决策依据。
- 证明市场的核心是激励相容:质押罚没保证可靠性,抽样验证解决验证者困境,拍卖机制决定定价效率。
一句话总结:zkVM 把「可证明计算」从电路工程变成了普通编程,证明市场把「生成证明」从自建机房变成了可竞价的服务——两者结合,才让零知识证明第一次具备了规模化落地的经济基础。
相关阅读
- 零知识证明:zk-SNARKs、zk-STARKs、电路与隐私应用
- Layer2 扩容:Rollup 架构、数据可用性与跨 L2 桥
- Rollup 序列器、L3 与应用链
- 以太坊 EVM 与状态模型
- 区块链开发工具链
延伸阅读
以下是一个完整的端到端示例:guest 程序在 zkVM 内验证一个 Merkle 成员证明,host 生成并包装证明,链上合约消费 journal 并更新状态根。
// methods/guest/src/main.rs
// 证明:我知道 (leaf, path) 使得 Merkle 根等于公开的 root
use risc0_zkvm::guest::env;
use sha2::{Digest, Sha256};
fn hash_pair(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(left);
h.update(right);
h.finalize().into()
}
fn main() {
let leaf: [u8; 32] = env::read();
let path: Vec<[u8; 32]> = env::read();
let indices: Vec<bool> = env::read();
let expected_root: [u8; 32] = env::read();
let mut current = leaf;
for (sibling, go_right) in path.iter().zip(indices.iter()) {
current = if *go_right {
hash_pair(¤t, sibling)
} else {
hash_pair(sibling, ¤t)
};
}
// 约束:计算结果必须等于公开根,否则证明无法生成
assert_eq!(current, expected_root, "merkle path invalid");
// 公开输出:只暴露根,不暴露 leaf 与 path
env::commit(&expected_root);
}
// host/src/main.rs
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts};
use methods::{MERKLE_GUEST_ELF, MERKLE_GUEST_ID};
fn prove_membership(
leaf: [u8; 32],
path: Vec<[u8; 32]>,
indices: Vec<bool>,
root: [u8; 32],
) -> Vec<u8> {
let env = ExecutorEnv::builder()
.write(&leaf).unwrap()
.write(&path).unwrap()
.write(&indices).unwrap()
.write(&root).unwrap()
.build().unwrap();
// 指定 Groth16 包装,产物适合直接上链
let opts = ProverOpts::groth16();
let receipt = default_prover()
.prove_with_opts(env, MERKLE_GUEST_ELF, &opts)
.unwrap();
// 链下自检:无效证明绝不提交
receipt.verify(MERKLE_GUEST_ID).unwrap();
// 返回 seal,供链上验证合约使用
receipt.inner.groth16().unwrap().seal.clone()
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol";
contract MerkleRootRegistry {
IRiscZeroVerifier public immutable verifier;
bytes32 public immutable imageId;
bytes32 public currentRoot;
mapping(bytes32 => bool) public consumedJournal; // 重放防护
event RootUpdated(bytes32 indexed root, uint256 timestamp);
constructor(IRiscZeroVerifier _verifier, bytes32 _imageId) {
verifier = _verifier;
imageId = _imageId;
}
function updateRoot(bytes calldata seal, bytes calldata journal) external {
// 1) 重放防护:同一份 journal 只能消费一次
bytes32 journalHash = sha256(journal);
require(!consumedJournal[journalHash], "journal already consumed");
// 2) 验证证明,并绑定到白名单 guest 程序
verifier.verify(seal, imageId, journalHash);
// 3) journal 已被证明覆盖,解码业务数据
bytes32 root = abi.decode(journal, (bytes32));
require(root != bytes32(0), "empty root");
consumedJournal[journalHash] = true;
currentRoot = root;
emit RootUpdated(root, block.timestamp);
}
}
上述三段代码构成一个完整的闭环:guest 定义「什么必须为真」,host 负责「把证明生出来并包装」,Solidity 合约负责「在链上以常数成本验证并消费结果」。理解了这三者的职责边界,就掌握了 zkVM 应用的基本骨架——而证明市场则是在此之上,把 host 这一环从「自己跑」变成「买服务」的经济扩展。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。