RISC0是一个zkVM
近来得闲,看看RISC0的源代码。zkVM一直是想深入的话题。zkVM将零知识证明技术抽象封装。复杂的业务,通过上层语言描述,经过zkVM的执行,轻松生成证明。可是迟迟看不清,zkVM的需求。只是觉得,复杂的程序也能快速地生成证明是一件很酷的事情。欢迎更多对zkVM技术感兴趣的小伙伴一起留言讨论。
RISC0的源代码:https://github.com/risc0/risc0.git 。本文中使用的RISC0源代码的最后一个commit如下:
commit ba4fdb21c06dd543462d4bd7f3a7873f0066f22f
Author: Austin Abell <austinabell8@gmail.com>
Date: Thu Dec 19 21:07:30 2024 -0500
update RSA patch to tag, use in compat test (#2649)
Switched rsa tag from specific commit
RISC0的业务逻辑实现采用rust语言。源代码中,相对重要的模块如下:
bonsai - Bonsai证明服务。通过API接口提交需要证明的指令,Bonsai返回相应的证明。
groth16_proof - risc0的最后的groth16的证明。电路由circom语言实现。
risc0/circuit - risc0项目中使用的电路,包括rv32im电路/keccak电路/“压缩”电路等等。注意,具体的电路并非在这个项目中实现,这个目录下,只是电路功能相关接口。具体的电路实现方法在后面介绍。
risc0/core - risc0证明系统依赖的域的定义。risc0采用的是Baby Bear域。
risc0/groth16 - 实现groth16的verifier。
risc0/r0vm - risc0的命令行实现。
risc0/zkp - risc0证明系统。
risc0/zkvm - VM的相关实现,包括guest和host间的调用,host端的API和prover的接口实现。
rzup - risc-v的编译/管理工具链
RISC0,或者说,所谓的zkVM的大体逻辑类似,guest程序(基于特定指令集)在某个vm环境中执行并生成相应的证明。
图片来自https://dev.risczero.com/api/zkvm/
RISC0,就是这样一个VM。实现VM环境的部分通常称为Host。在VM中被执行的部分通常称为Guest。Guest程序是基于RV32IM指令集(但不支持RCS等特权功能的相关的指令)。Guest程序通过rust编译器生成指令序列后,通过Executor执行,可能生成多个Segment。这些Segment打包成Session,发送给Prover进行证明并生成最后的结果以及证明(Receipt)。
用于零知识证明的VM通常是弱化的Virtual Machine。这种VM主要侧重于计算的结果,主要由CPU以及MEMORY构成。CPU基于RV32IM指令集。RISC0的内存定义在risc0/zkvm/platform/src/memory.rs。
pub const MEM_BITS: usize = 28;
pub const MEM_SIZE: usize = 1 << MEM_BITS;
pub const GUEST_MIN_MEM: usize = 0x0000_0400;
pub const GUEST_MAX_MEM: usize = SYSTEM.start;
pub const STACK_TOP: u32 = 0x0020_0400;
pub const TEXT_START: u32 = 0x0020_0800;
pub const SYSTEM: Region = Region::new(0x0C00_0000, mb(16));
pub const PAGE_TABLE: Region = Region::new(0x0D00_0000, mb(16));
pub const PRE_LOAD: Region = Region::new(0x0D70_0000, mb(9));
内存大小设置为2^28比特,也就是32M字节。从0x0c000000地址开始的16M字节是系统地址。寄存器是映射到系统地址上的。从0x0d000000开始的16M字节是页表信息。除了这些外,还定义了栈的起始位置,以及程序的起始位置。
模拟器(Emulator)模拟CPU执行以及内存管理,实现在risc0/circuit/rv32im/src/prove/emu目录。指令集的实现在risc0/circuit/rv32im/src/prove/emu/rv32im.rs。
通过Executor的run函数,调用Emulator执行Guest程序。
pub fn run<F: FnMut(Segment) -> Result<()>>(
&mut self,
segment_po2: usize,
max_cycles: Option<u64>,
mut callback: F,
) -> Result<ExecutorResult> {
...
let mut emu = Emulator::new();
loop {
...
emu.step(self)?;
...
}
}
调用Emulator执行程序主要目的是将程序分割成多个Segment以及为每个Segment生成对应的系统状态。这里的系统状态主要就是内存(包括寄存器)信息。先系统状态表示。
系统状态由SystemState表示:
pub struct SystemState {
pub pc: u32,
pub merkle_root: Digest,
}
系统状态,包括pc以及内存(包括寄存器)的数据。内存的数据状态采用merkle树根表示。注意,从代码实现上看,内存的数据并不是采用“merkle”树结构hash,而是不停的迭代计算sha256 hash。总之,整个系统的状态由两部分组成:1/ 内存数据信息 2/ 寄存器信息。在定义了系统状态后,就可以将程序进行划分。
Segment定义在risc0/circuit/rv32im/src/prove/segment.rs:
pub struct Segment {
pub partial_image: MemoryImage,
pub pre_state: SystemState,
pub post_state: SystemState,
pub syscalls: Vec<SyscallRecord>,
pub insn_cycles: usize,
pub po2: usize,
pub exit_code: ExitCode,
pub index: usize,
pub input_digest: Digest,
pub output_digest: Option<Digest>,
}
Segment主要由如下的几个部分组成:
1、insn_cycles:当前Segment包含的指令个数。
2、pre_state/post_state:当前Segment中的指令执行前后的系统状态。
3、input_digest/output_digest: 可能的输入和输出信息。
一个完整的程序,分割成多个Segment后,组成一个Session,发送给Prover,生成Segment对应的证明。
Prover分为两种:一种是本地生成(local),一种是远程服务(remote)。RISC0提供了远程服务,取名:bonsai。重点说说,本地Prover吧。按照实现的语言,本地的Prover又分成三种:rust,cuda以及metal。为了更好的支持多语言实现,RISC0定义了硬件抽象层(HAL)。HAL又分为两种:域HAL以及电路HAL。
域HAL,主要实现域相关的计算以及数据管理:risc0/zkp/src/hal/mod.rs
。
电路HAL,主要实现电路的witness生成:risc0/circuit/rv32im/src/prove/hal/mod.rs
。
pub(crate) trait CircuitWitnessGenerator<H: Hal> {
#[allow(clippy::too_many_arguments)]
fn generate_witness(
&self,
mode: StepMode,
trace: &RawPreflightTrace,
steps: usize,
count: usize,
ctrl: &H::Buffer<H::Elem>,
io: &H::Buffer<H::Elem>,
data: &H::Buffer<H::Elem>,
);
}
到此,RISC0代码逻辑框架基本了解。为了证明一个Segment,首先调用电路HAL生成witness,接着调用域HAL的相关计算接口完成zkp的计算和生成。到目前为止,RISC0电路逻辑还未涉及。其实,risc0的代码仓库中并没有电路的构建逻辑。约束电路的相关代码逻辑在另外一个仓库:https://github.com/risc0/zirgen.git。
Zirgen是一个有趣的电路编译工具。Zirgen也是一个DSL(domain specfic language)。通过这个DSL编写后的电路,Zirgen可以将电路约束以及witness生成的逻辑“编译”成多种语言的实现。
图片来自:https://github.com/risc0/zirgen
通过Zirgen实现了RISC0的zkVM电路以及聚合电路(Recursion)。聚合电路是将多个Segment的zkVM的证明聚合成一个证明。
zkVM电路的逻辑实现在zirgen/circuit/rv32im/v2/dsl/top.zir。Top()函数实现了电路最顶层的设计,读取指令执行前状态,执行一个指令并生成执行后状态。
Zirgen编译系统借助于LLVM以及MLIR,实现了自己的Dialect(方言)。通过MLIR的方言转换为Rust/C++/CUDA代码。这部分逻辑需要对LLVM/MLIR编译系统比较熟悉。我也不是特别清楚。具体的逻辑感兴趣的小伙伴可以自行查看。
这种通过LLVM/MLIR编译系统生成电路的方式比较有趣。我比较感兴趣的是,众所周知,LLVM/MLIR能将计算映射到某种特定硬件,但是LLVM/MLIR是否能完成“约束”的映射?或者说,这种映射是否是完备的?LLVM/MLIR的编译优化能否完全借用?
RISC0证明系统实现了基于 FRI 协议、DEEP-ALI 和 HMAC-SHA-256 的伪随机函数 (PRF) 的 zk-STARK。设计文档中非常详细的描述整个算法流程:
https://dev.risczero.com/proof-system-in-detail.pdf
具体的代码实现在risc0/zkp/src/prove/prover.rs。
图片来自https://github.com/risc0/risc0/blob/main/website/docs/proof-system/proof-system.md。
通过模拟器的执行,一段程序被Executor分割成多个Segment。这些Segment被Prover证明并生成多个Recepit。这些Receipt进一步通过聚合电路证明合并成一个证明。为了实现链上验证(减少验证开销),这个证明又被Groth16证明生成最终的证明。
总结:
RISC0是一个zkVM。它支持基于RISC-V指令集的程序执行。zkVM的电路通过Zirgen编写。Zirgen通过LLVM/MLIR编译系统将电路约束转化成多种语言的实现逻辑。RISC0证明系统实现了基于 FRI 协议、DEEP-ALI 和 HMAC-SHA-256 的伪随机函数 (PRF) 的 zk-STARK。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!