本文介绍了零知识虚拟机(ZKVM)的概念,它能创建零知识证明来验证机器指令的正确执行。文章通过一个简化的栈式ZKVM示例,展示了如何使用Circom实现基本的算术运算,并探讨了提高ZKVM效率的现代方法,如查找表和递归证明。 ZKVM在零知识Layer2区块链中至关重要,并可用于验证机器学习算法的正确执行。
一个 零知识虚拟机 (ZKVM) 是一个可以创建 ZK 证明的虚拟机,该证明验证它正确执行了一组机器指令。这允许我们获取一个程序(一组操作码),一个虚拟机规范(虚拟机如何运行,它使用什么操作码,等等),并证明生成的输出是正确的。验证者不必重新运行程序,只需检查生成的 ZK 证明 — 这使得验证变得简洁。这种简洁性是使 ZK layer 2 区块链可扩展的基础。它还使某人能够检查机器学习算法是否按照声明运行,而无需重新运行整个算法。
与名称相反,ZKVM 在保持计算私密性的意义上很少是“零知识”的。相反,它们使用 ZK 算法来生成一个简洁的证明,证明程序在某个输入上正确执行,以便验证者可以用指数级减少的工作量来复查计算。即使显示程序输入是可选的,但避免意外的数据泄露以及让多方就私有状态达成一致是非常具有挑战性的工程问题,仍然存在尚未解决的挑战和扩展限制。
ZKVM “计算”操作码中的每个步骤,然后约束该操作码已正确执行。约束的设计方式必须使我们可以使用任意但有效的操作码序列。
我们可以将 ZKVM 视为一系列“状态”转换。“状态转换函数”采用先前的状态和当前要执行的操作码,并生成一个新状态。ZKVM 实现“状态转换函数”和模拟其行为的电路约束。请注意,“状态”可以包括诸如“程序计数器”之类的东西,或者 VM 正常运行所需的其他簿记信息。
本教程将构建一个极其简单的 ZKVM,该 ZKVM 仅支持基本算术,但可扩展以用于其他操作码。此处提供的 VM 只有一个栈,没有内存或存储。我们在文章末尾提供了有关 ZKVM 进一步研究的建议。
本教程假定对 EVM 的工作方式(或其他基于栈的架构,例如 Java 虚拟机)有基本的了解。我们大量借鉴了上一章的栈示例,因此我们假设你已经了解了该示例。
我们将构建一个简单的基于栈的 ZKVM,它带有一个特殊用途的信号,该信号包含计算结果。VM 接受一系列操作码和数字,然后将最终结果输出到一个我们称为 out
的特殊信号。
我们的 ZKVM 仅具有以下操作码:
为简单起见,所有操作码都采用单个参数,但只有 PUSH 使用该参数。其余指令忽略该参数。我们为不使用参数的操作码提供参数的原因是,我们不必根据操作码有条件地检查参数是否存在。
没有 STOP 或 RETURN 操作码(稍后会解释替代方法)。VM 接受一个 steps
参数,并在执行 steps
多条指令后返回栈底部的任何值。
以下动画给出了在此架构中将两个数字相加的简单示例:
https://img.learnblockchain.cn/2025/04/16/zkvm.mp4
在 Circom 中,循环不能是可变长度的,它们必须始终执行固定数量的迭代,因为底层 Rank 1 Constraint System (R1CS) 本身必须是固定大小的。同样,程序的大小也不能是可变的。但是,无论程序运行如何,它们都必须具有相同数量的操作码。
要运行操作码数量少于固定数量的程序,我们只需用 NOP 填充它,直到程序达到最大大小。为了知道何时“停止执行”,用户必须提供上述 steps
参数以确定何时将返回栈底部的值。
关于我们架构的一些注意事项:
我们可以对上一章的栈代码进行一些关键更改,以构建满足上述规范的 ZKVM。
回想一下,以前复制前一个栈的规则是:
sp
大于或等于 1,并且列 j
比 sp
低 1 个索引,并且当前指令是 PUSH 或 NOP,我们应该复制列 j
sp
大于或等于 2,并且列 j
比 sp
低 2 个索引,并且当前指令是 POP,我们应该复制列 j
规则 A 保持不变,但 B 需要更新为以下内容:
sp
大于或等于 2,并且列 j
比 sp
低 3 个索引,并且当前指令是 ADD 或 MUL,我们应该复制列 j
此更改的原因是,先前的 POP 指令没有更改栈上倒数第二个项目,它只是删除了最上面的项目。但是,ADD 有效地将栈弹出两次并压入总和。同样,MUL 将栈弹出两次并压入乘积。
先前的栈实现仅将新值写入栈指针。但是,新的实现可以将总和或乘积写入栈指针下方两个索引的位置。例如,下面栈中的 12 在加法后将变为 15,并且该位置位于栈指针下方两个索引的位置:
加法前:
[12 , 3, sp] (sp = 3)
加法后:
[15, sp] (sp = 2)
在这里,我们有 12 作为栈的底部,sp
指向栈上方的空白区域。
因此,我们需要一个信号来指示特定列低于栈指针两个元素。
下面的代码很大程度上源自上一章的栈,但它实现了本章中描述的更新。具体来说:
pragma circom 2.1.6;
include "circomlib/comparators.circom";
include "circomlib/gates.circom";
template AND3() {
signal input in[3];
signal output out;
signal temp;
temp <== in[0] * in[1];
out <== temp * in[2];
}
// i 是列号
// bits 是我们需要多少位
// 用于 LessEqThan 组件
template ShouldCopy(i, bits) {
signal input sp;
signal input is_push;
signal input is_nop;
signal input is_add;
signal input is_mul;
// out = 1 如果应该复制
signal output out;
// 健全性检查
is_add + is_mul + is_push + is_nop === 1;
is_nop * (1 - is_nop) === 0;
is_push * (1 - is_push) === 0;
is_add * (1 - is_add) === 0;
is_mul * (1 - is_mul) === 0;
// 计算 ≠ 0 比 > 0 更便宜,以避免
// 将数字转换为二进制
signal spEqZero;
signal spGteOne;
spEqZero <== IsZero()(sp);
spGteOne <== 1 - spEqZero;
// 计算 ≠ 0 和 ≠ 1 比 ≥ 2 更便宜
signal spEqOne;
signal spGteTwo;
spEqOne <== IsEqual()([sp, 1]);
spGteTwo <== 1 - spEqOne * spEqZero;
// 当前列是 1 或更多
// 低于栈指针
signal oneBelowSp <== LessEqThan(bits)([i, sp - 1]);
// 当前列是 3 或更多
// 低于栈...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!