掌握SP1 zkVM设计 - 第1部分:如何执行客户程序

  • gavin.ygy
  • 发布于 2024-12-28 12:25
  • 阅读 39

本文介绍了SP1 zkVM的设计原理,重点分析了zkVM如何执行用户程序,并生成零知识证明。文章详细解释了zkVM的编译器、指令集架构、以及证明系统的核心代码实现,帮助读者理解这一前沿技术的运作机制。

1. 背景

在 DevCon 上,以太坊基金会的研究员 Justin Drake 宣布了一项针对以太坊共识层的升级提案,称为 Beam Chain,该提案结合了 zkVM 技术。他还在 11 月底透露,基金会正在投资数千万美元用于 zkVM 技术。这表明,三年前由 Risc0 发起的 zkVM 运动(通用零知识证明)正在蓬勃发展,逐渐成为区块链的支柱,为更广泛的应用,如 zkAI,迈出了坚实的第一步。

本文系列以 SP1 zkVM 为例,分析 zkVM 的原理、来宾程序的执行、零知识证明的生成及验证过程。目的是激发兴趣,帮助读者揭开围绕 zkVM 的神秘面纱,掌握前沿 zkVM 技术的关键技术要点。

2. zkVM 的优势

zkVM 是一个通用的零知识证明平台。开发者可以快速构建有意义的 ZK 应用,而无需构建电路、不需要使用自定义语言,也无需具备高级数学或密码学背景。

3. zkVM 设计概述

图像来源: https://www.lita.foundation/blog/a-zero-knowledge-paradigm-part-2--exploring-zk-vm-design-trade-offs

3.1 编译器

它决定了哪些高级语言可以被编译成机器代码。SP1 CLI 工具支持 Rust 语言并将程序编译为 RISC-V ELF 文件。

3.2 VM + ISA

指令集架构确定了由编译器生成并由 zkVM 执行的机器代码的格式,可以说它决定了 VM 的行为。当前主流的指令集(芯片)架构包括:x86、ARM、RISC-V、MIPS 等。SP1 使用 RISC-V RV32IM 指令集,支持的汇编命令可以在以下地址找到:https://github.com/johnwinans/rvalp/releases

3.3 VM + 证明者 + 验证器

这是运算化的过程,即将计算问题转化为代数问题。SP1 使用 AIR(代数中间表示法)进行运算化。

3.4 证明者 + 验证器

这是零知识证明系统。SP1 的证明系统由 STARK、Plonky3 和 Groth16 组成。

4. zkVM 的流程图

4.1 什么是来宾程序?

来宾程序是实施用户业务逻辑的程序,用户希望为该程序的正确执行生成零知识证明。不同的 zkVM 支持不同的高级语言用于来宾程序开发;例如,SP1 支持 Rust,而 Risc0 支持 C/C++/Rust。

4.2 VM 内的关键执行阶段

首先,反汇编阶段会反转来宾程序的 ELF 文件;然后,将反汇编文件中的 ISA 指令转换为 VM 指令;最后,执行每个 VM 指令,并生成执行记录以供证明系统使用。

为什么需要反转 ELF 文件?这是出于性能原因,因为 VM 本质上是针对指令集设计的 CPU 的软件仿真。例如,如果目标 CPU 有 8 位寄存器操作,软件模拟这些操作将效率低下。

5. SP1 VM 的核心代码分析

代码库: https://github.com/succinctlabs/sp1,开发分支:提交 a3de183ecfd723c79819a,作者:Yuwen Zhang <yuwen01@gmail.com>。

在 SP1 脚本中,调用 client.execute(ELF, stdin.clone()).run().unwrap() 将继续执行在 crates/prover/src/lib.rs 文件中定义的 execute() 函数。

/// 生成指定输入的 SP1 程序证明。
#[instrument(name = "execute", level = "info", skip_all)]
pub fn execute&lt;'a>(
    &'a self,
    elf: &[u8],
    stdin: &SP1Stdin,
    mut context: SP1Context&lt;'a>,
) -> Result&lt;(SP1PublicValues, ExecutionReport), ExecutionError> {
    context.subproof_verifier.replace(Arc::new(self));
    let program = self.get_program(elf).unwrap(); //注释 1)

    let opts = SP1CoreOpts::default(); //根据机器上的可用内存,自动设置 shard_size 和 shard_batch_size。类似于 Risc0 技术中的段大小和总段数。

    let mut runtime = Executor::with_context(program, opts, context); //为程序创建运行时
    runtime.write_vecs(&stdin.buffer);
    for (proof, vkey) in stdin.proofs.iter() {
        runtime.write_proof(proof.clone(), vkey.clone());
    }
    runtime.run_fast()?; //注释 2)
    Ok((SP1PublicValues::from(&runtime.state.public_values_stream), runtime.report))
}

上述注释 1)和 2)概述了 VM 的主要操作,将一一分析。

5.1 self.get_program(elf).unwrap()

get_program() 在 crates/prover/src/lib.rs 中定义。请注意以下注释。

/// 获取一个具有允许预处理形状的程序。
pub fn get_program(&self, elf: &[u8]) -> eyre::Result&lt;Program> {
    // from() 函数反汇编 ELF 文件,将 RV32IM 指令转换为 VM 指令,并将它们封装为可供 VM 执行的程序结构,然后返回。
    let mut program = Program::from(elf)?;

    if let Some(core_shape_config) = &self.core_shape_config {
        core_shape_config.fix_preprocessed_shape(&mut program)?;
    }
    Ok(program)
}

在将 ELF RISC-V 指令转换为 VM 指令时,SP1 利用一个名为 rrs_lib 的第三方库(见 crates/core/executor/src/disassembler/rrs.rs),该库可以模拟 RISC-V 指令(RISC-V CPU)。

此外,存储在 crates/core/executor/src/ 目录下的 memory.rs、opcode.rs、register.rs 等文件与指令转码过程有关。

5.2 runtime.run_fast()

run_fast() 在 crates/core/executor/src/executor.rs 中定义。

   /// 在不追踪和不发出事件的情况下执行程序。
   ///
   /// # 错误
   ///
   /// 如果程序执行失败,本函数将返回错误。
   pub fn run_fast(&mut self) -> Result&lt;(), ExecutionError> {
       self.executor_mode = ExecutorMode::Simple;
       self.print_report = true;
       while !self.execute()? {} // 为什么要递归地调用 execute()? 因为程序可能被划分为多个片段,每个片段需要按顺序执行。

       Ok(())
   }

crates/core/executor/src/executor.rs: /// 在 self.shard_batch_size 周期内执行程序,返回程序是否已完成。

pub fn execute(&mut self) -> Result&lt;bool, ExecutionError> {
    // 如果未初始化,则初始化随机数查找表。
    if self.record.nonce_lookup.len() &lt;= 2 {
        self.record.nonce_lookup = vec![0; self.opts.shard_size * 32];
    }

    // 获取程序。
    let program = self.program.clone();

    // 获取当前片段。
    let start_shard = self.state.current_shard;

    // 如果是第一个周期,初始化程序。
    if self.state.global_clk == 0 {
        self.initialize();
    }

    // 循环直到执行 `self.shard_batch_size` 个片段(如果设置了 `self.shard_batch_size`)。
    let mut done = false;
    let mut current_shard = self.state.current_shard;
    let mut num_shards_executed = 0;
    loop { //在片段中执行每个周期
        if self.execute_cycle()? { //如果是程序的最后一个周期,返回 true
            done = true;
            break;
        }

        if self.shard_batch_size > 0 && current_shard != self.state.current_shard {
            num_shards_executed += 1;
            current_shard = self.state.current_shard;
            if num_shards_executed == self.shard_batch_size {
                break;
            }
        }
    }
    //…
}

execute_cycle() 在 crates/core/executor/src/executor.rs 中定义:

/// 执行程序的一个周期,返回程序是否完成。
#[inline]
#[allow(clippy::too_many_lines)]
fn execute_cycle(&mut self) -> Result&lt;bool, ExecutionError> {
    // 获取当前程序计数器处的指令。
    let instruction = self.fetch();

    // 记录运行时的当前状态。
    #[cfg(debug_assertions)]
    self.log(&instruction);

    // 执行指令。
    self.execute_instruction(&instruction)?;

    // 增加时钟。
    self.state.global_clk += 1;
    //…
}

操作 self.execute_instruction(&instruction) 执行给定的 VM 指令,并为后续证明系统生成相应的执行记录,以便生成相关的轨迹。

上述描述了 SP1 虚拟机在执行来宾程序时的主要操作步骤。若要深入了解更多细节,读者需要自行阅读源代码。至于证明系统,将在后续文章中进行分析。

6. 参考文献

1) Lita 团队,《探索 zkVM 设计权衡》, https://www.lita.foundation/blog/a-zero-knowledge-paradigm-part-2--exploring-zk-vm-design-trade-offs

2) Mutourend,《zkVM 系列》, https://blog.csdn.net/mutourend/category_11883520.html

3) Risc0, https://dev.risczero.com/api/zkvm/optimization

4) SP1 技术白皮书, https://drive.google.com/file/d/1aTCELr2b2Kc1NS-wZ0YYLKdw1Y2HcLTr/view

  • 原文链接: medium.com/@gavin.ygy/ma...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
gavin.ygy
gavin.ygy
江湖只有他的大名,没有他的介绍。