阅读本页,了解 Rand 上每一个证明背后的机器:它证明了什么,程序的一次运行如何变成验证者(validator)可以在几毫秒内检查的东西,以及隐私从何而来。关于证明的成本以及钱包如何生成证明,参见 Proofs。关于参数和哈希构造,参见 Cryptography。关于程序所依据的指令集,参见 The RISC-V ISA。
每个证明所陈述的唯一命题
对于链上运行的每一个程序,zkVM 证明的都是同一类命题:
一个摘要为
hc的程序,从base_pc开始,在由H_IN承诺的私有输入和由H_PUB承诺的公开输入段上运行,在2ᵗ − 1个周期内停机,至多消耗GAS_LIMIT的 gas,并写出了这八个输出字。
一笔屏蔽转账(shielded transfer)就是关于某一个固定程序的这一命题,该程序即隐藏资产捆绑包 guest(hidden-asset bundle guest),其 hc 在创世文件中固定为 hc_bundle。自 chain 17 起,每笔转账会生成两次证明:第二个较小的证明针对授权 guest(auth guest)(创世文件中的 hc_auth),用以表明发送方持有花费密钥(Proofs);自 chain 21 起,这第二个证明也可以改为多签授权 guest(multisig auth guest)(hc_auth_multisig)的证明,它表明一个隐藏的 Dilithium2 密钥集合中有足够多的密钥签了名,而不透露是哪一个集合,也不透露有多少个。机密调用(confidential call)则是关于某个已部署程序的同一命题。所有这些共用一台机器、一个验证者和一套约束。因此,部署程序是一次数据操作(存储其字和 hc),而不是密码学操作:不存在针对每个程序的电路,也不需要可信设置。
该证明系统是用 Plonky3 构建的 STARK。它只需要一个哈希函数:没有设置仪式,也不使用椭圆曲线,因此没有任何东西可供 Shor 算法破解。
流水线
secret input ──► run the program ──► execution trace ──► polynomials ──► commit ──► FRI ──► proof
(keys, (RV32IM emulator) (nine tables, (one per (8× code, (80 queries,
notes, paths) one row per step) column) Merkle) 20-bit grinding)
every validator: recompute the challenges from the Merkle roots, open 80 random positions,
check the rules there → ≈ 16 ms
上面一行在发送方的机器上运行一次,一个捆绑包约需 100 秒,旁边的授权证明约需 7 秒。下面一行在每个验证者(validator)上运行,只依据公开字段和证明本身。
1. 运行程序并记录一切
该机器是一个 32 位 RISC-V 处理器:RV32IM,去掉 FENCE、CSR 指令和 EBREAK(The RISC-V ISA)。证明者(prover)首先在模拟器上运行程序,并为每个时钟周期记录一行。这份记录就是执行轨迹(execution trace)。任何持有它的人都可以逐步重放这次运行;方案的其余部分,则是为了让验证者(verifier)无需持有轨迹,也能确信轨迹是正确的。
程序只通过八个系统调用与外界交互,别无他途:
| syscall | no. | what it does |
|---|---|---|
HALT | 0 | 停止;八个输出字成为最终值 |
WRITE_OUTPUT | 1 | 设置八个公开输出字之一 |
READ_INPUT | 2 | 读取私有输入的一个字 |
POSEIDON2 | 3 | 就地对内存中的一个缓冲区做哈希 |
KECCAK | 4 | 一次 Keccak-f[1600] 置换 |
SHA256 | 5 | 一次 SHA-256 压缩 |
READ_PUBLIC | 6 | 读取公开输入段的一个字 |
POSEIDON2_LEN | 7 | 将消息长度绑定进哈希的 POSEIDON2(约束集 7) |
MODQ | 8 | 对内存中 256 个系数的多项式做一次模 q = 8,380,417(Dilithium 的素数)运算:NTT、逆 NTT、四路点积或乘减(约束集 9,chain 21)。它会向证明中添加可选的 modq 表;只有多签授权 guest 调用它,使用它时一次 Dilithium2 验证需要 80,572 个周期,而用普通指令则需要 880,102 个 |
注记承诺(note commitment)、作废符(nullifier)和默克尔路径(Merkle path)并未内置于机器之中。它们是普通的 guest 代码,在内存中准备一条带域标签的消息,然后调用 POSEIDON2。该系统调用不做填充,因此 [a] 与 [a, 0] 的哈希相同。已发布的 guest 只对定长消息调用它。对长度可变的数据做哈希的新程序,应使用 POSEIDON2_LEN(The RISC-V ISA)。
没有中止
处理器没有“拒绝完成”的指令,因此程序无法在检查失败时停止。捆绑包 guest 转而记录失败:每次检查失败,就把一个 1 通过 OR 写入名为 bad 的寄存器,而 bad 是输出摘要中最后被哈希的字:
D = H(64; anchor, nf₀..₃, cm₀..₃, fee, burn_a, burn_r, burn_asset, time, c, bad)
c 是授权证明同样会公布的授权承诺(自 chain 17 起;chain 14 至 16 的摘要中没有 c)。
链从不从发送方那里获取 D 的原像。它根据交易的公开字段重建原像,自行设定 bad = 0,做哈希,然后比较。作弊者可以为一次有检查失败的运行生成一个完全有效的证明,但那次运行打印出的是 H(…, 1),而链要找的是 H(…, 0)。要使二者匹配,需要一次哈希碰撞。
2. 将轨迹拆分为九张表
用一张表来描述整个处理器会过于宽大,因此轨迹被拆分为若干专门的表,作为一个批次一并证明:
| table | rows at tier t | what it records |
|---|---|---|
program | 声明值,≥ 2⁷ | 程序的指令字,在电路中解码;限定 CPU 可以取指的内容 |
cpu | 2ᵗ | 每个周期一行,其前是对程序、私有输入和公开段做哈希的若干行;保存 35 个公开值 |
memory | 声明值,≥ 2ᵗ⁺² | 寄存器和 RAM,按(空间、地址、时间)排序 |
alu | 2ᵗ⁺¹ | 每一次算术、逻辑、移位、比较、乘法和除法,按字节分块(limb)记录 |
range, nibble | 各 256 | 固定查找表:字节、2 的幂、4 位 AND/OR/XOR |
poseidon2 | 2ᵗ⁺² | 哈希置换,每轮一行 |
input | 声明值,≥ 2⁷ | 已承诺的私有输入字 |
public | 声明值,≥ 2⁷ | 公开段的字(对捆绑包而言,即交易绑定) |
另有两张表 keccak 和 sha256,只在程序调用了它们的证明中出现,出现时各至少 2⁷ 行;捆绑包 guest 两者都不使用。约束集 9(chain 21)以同样的方式增加了第三张可选表 modq:每次 MODQ 调用占一个 1,024 行的块,其高度在新的头部字段 modq_log_height 中声明,且只出现在多签授权 guest 的证明里;cpu 表增加了该系统调用所需的列,这就是每个验证密钥都发生变化的原因。所有已声明的表都有 2⁷ 行的下限,这随约束集 7 一同引入(section 9)。
层级(tier) t 取 10、12、14、16、18 或 20 之一,决定各表的大小。每个捆绑包都在层级 14 上证明:一张 16,384 行的 CPU 表,guest 使用其中约 12,400 行。授权 guest 在层级 10 上运行 250 个周期;多签授权 guest 在层级 18 上运行 85,830 至 251,946 个周期,对应一至三个签名,且所有取决于签名数量的高度都以三为下限,因此 1-of-N 的花费看起来与 3-of-N 的花费一样。其余部分是填充,所以证明只会透露程序需要哪个层级,而不会透露它在层级内部用了多少步(Tiers)。
内存表使 RAM 变得可检查。由于它先按地址、再按时间排序,“一次读取返回写入该地址的最后一个值”就变成了相邻两行之间的一条规则。
3. 用总线连接各张表
验证者如何知道 CPU 所用的和是 ALU 计算出的和?通过总线(bus)。当 CPU 需要一次加法时,它发送一条消息 (op, a, b, c);ALU 为其计算的每一行接收一条消息。该机器共有十七条总线:
| bus | between |
|---|---|
PROGRAM, PROGRAM_WORD | cpu ← program:取指,以及程序摘要所吸收的字 |
MEMORY | cpu ↔ memory:每一次读和写 |
ALU | cpu → alu |
RANGE8, POW2, AND4, OR4, XOR4 | 针对固定表的字节范围检查、移位和按位运算 |
POSEIDON2 | cpu → poseidon2:每次置换一条消息 |
INPUT_DIGEST, INPUT_READ | cpu ← input:输入摘要所吸收的字,以及程序的读取 |
PUBLIC_DIGEST, PUBLIC_READ | cpu ← public:公开段的同一对总线 |
KECCAK, SHA256, MODQ | cpu → 可选的哈希表,以及(约束集 9)模 q 表 |
BLIND | 每张表 → 固定循环中的下一张表:在第一行各一个随机值(约束集 7) |
总线用**对数导数论证(LogUp)**来检查。各表提交之后,抽取一个随机值 α,每张表对其发送或接收的消息维护一个 ±1/(α − m) 的运行和。当且仅当每条发出的消息都被接收,即两侧的多重集相同,这些和才会恰好相互抵消。哪怕伪造一条消息,也会留下残差,随机的 α 几乎必然将其暴露。内存总线使用与之相近的方法,即多重集置换检查。
每张表公布其运行和的最终值,即它的终值,验证者检查各表终值之和为零。到约束集 6 为止,终值是其所在表内容的一个可检查的函数。自约束集 7 起,每张表多出五列,并在首行通过 BLIND 总线发送一个随机值、接收相邻表的随机值。每个终值都按挑战域中的一个均匀元素移动,而总和仍为零。
摘要总线与读取总线刻意分成两对。LogUp 按消息平衡,而不是按消费者平衡,因此若共用一条总线,证明者就可以从摘要中丢掉一个字,而对该字的读取仍然成功。
4. 将迹锚定到外部世界
迹中共有 35 个公开值,全部位于 CPU 表:
| 个数 | 含义 |
|---|---|
| 1 | 入口 pc |
| 1 | 层级 |
| 8 | 输出字(对于 bundle,即摘要 D) |
| 8 | hc,程序摘要,由程序表中的字在迹内部计算得出 |
| 8 | H_IN,私有输入摘要,对输入计算,前面带一个新的 128 位盐 |
| 8 | H_PUB,公开段摘要,不加盐 |
| 1 | GAS_LIMIT,证明者声明的 gas 上限(约束集 8) |
由于 hc 在电路内计算,关于另一个程序的证明无法冒充关于 bundle guest 的证明。验证者从不接触任何指令字,它只把 hc 与自己预期的摘要比较。由于 H_IN 加了盐,它绑定私有输入却不泄露输入,即使对于能够猜出输入的人也是如此。由于 H_PUB 不加盐,验证者可以用手头已有的字重新计算它。对于 bundle,这些字就是交易绑定:blake3("rand-tx-bind-1" ‖ chain_id ‖ tx),其中 tx 是证明字节被清空后的交易,因此被复制到目标、备注、信封或 chain id 不同的交易上的证明无法通过验证。自 chain 20 起(创世 binding_domain: 1),它是 blake3("rand-tx-bind-2" ‖ genesis_hash ‖ chain_id ‖ tx),因此即使 chain id 相同,证明也无法在另一条链上通过验证;这些字是公开输入,所以电路中没有任何改动。auth 证明针对同样的八个字生成。自 chain 20 起,调用证明以同样的方式绑定:其段是程序在部署时的公开字,后接八个调用绑定字(rand-call-bind-2,chain 20 之前为 rand-call-bind-1),因此被复制到另一个费用 bundle 之下的调用证明无法通过验证。
Gas 是机器衡量一次运行重量的度量,自约束集 8 起进入电路。CPU 表带有一个 GAS 列,从 1 开始,每行加 1,Poseidon2 吸收行加 3,SHA256 行加 64,KECCAK 行加 192。在 HALT 行,电路通过差值的四个经范围检查的肢(limb)证明 GAS ≤ GAS_LIMIT,这些肢从不打开:上限是公开的,而运行实际花费的 gas 不是。验证者在构造密钥之前,会拒绝超过证明自身头部所可能需要的上限:
gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ / 32) + 63·(2ˢ / 64)
其中 k 和 s 分别是 keccak 表和 sha256 表声明的对数高度,表不存在时对应项为零。无哈希的 tier-10 头部的上限是 1,279,tier-14 的是 20,479。链按调用声明的上限计价(费用)。
5. 把规则变成多项式恒等式
每一列都是 Goldilocks 域中的一列数,即模 p = 2⁶⁴ − 2³² + 1 的整数。由 n 个数构成的列表等同于一个次数小于 n 的多项式:当这些数被排布在域的子群 {1, ω, ω², …} 上时,通过它们的那个多项式。Goldilocks 具有直至 2³² 的各种二的幂大小的子群,因此这一转换是一次快速傅里叶变换。
于是机器的一条规则变成列多项式之间的一个等式。“程序计数器前进四,除非该指令跳转”示意性地写作:
is_jump(x)·(pc(ωx) − target(x)) + (1 − is_jump(x))·(pc(ωx) − pc(x) − 4) = 0
对子群中的每个 x 成立,其中 pc(ωx) 表示“下一行”。一张表的全部此类规则合在一起就是它的 AIR(代数中间表示)。CPU 和 ALU 的规则达到 8 次,其余的保持在 2 到 4 次。
在子群的每个点上都为零的多项式能被 Z(x) = xⁿ − 1 整除。证明者用随机权重把所有规则合并成一个多项式 C(x),再除以 Z(x)。若每条规则在每一行都成立,商就是一个真正的低次多项式;若有一条规则在某一行不成立,商就不是。“一次 16,000 个周期的运行,每一步都正确吗?”就变成了“这个函数是否接近某个低次多项式?”,而这个问题只需随机选取几个值查看就能回答。
6. 在问题揭晓之前先承诺
证明者必须在看到验证者的随机问题之前固定它的多项式,否则它可以针对问题定制答案。它在比表大八倍的域上对每一列求值(膨胀因子 8,码率 1/8),并公布这些求值之上的 Merkle 根。这种冗余是一个 Reed–Solomon 码:低次多项式由其任意八分之一的值确定,因此,哪怕只改动迹中的一行,作弊者也必须改动扩展码字的大部分才能保持一致,而随机抽查会落在受损之处。
Merkle 树使用 Goldilocks 上的 Poseidon2:宽度 8,S 盒 x⁷,4 个完整轮、22 个部分轮、再 4 个完整轮,摘要为四个域元素(约 256 位)。每个叶子都用四个新的随机元素进行哈希,因此根不会泄露其下方的任何值。同一个置换在机器内部也用作票据承诺和作废符哈希。它的 86 个轮常数是节点源码(poseidon2_constants.rs)中的一张字面量表,因此任何库升级都无法改变它们。
7. 用 FRI 抽查
验证者此时持有 Merkle 根。它首先把商与迹联系起来:在表的域之外随机选一个点 z,索取每一列在该点的值以及商在该点的值,并直接检查 C(z) = Q(z)·Z(z)。剩下要确认的是,已承诺的函数确实是低次多项式,而这正是 FRI 所检验的内容。
FRI 通过折叠工作。把一个多项式拆成偶数部分和奇数部分,f(x) = fₑ(x²) + x·fₒ(x²)。对验证者给出的随机 β,f′(y) = fₑ(y) + β·fₒ(y) 在大小减半的域上次数减半。证明者承诺 f′,得到新的 β,再次折叠(Rand 每轮最多折叠八倍),直到只剩一个常数,并将其明文发送。随后,一次查询在第一层选取一个随机位置并一路向下追踪,在每一层检查上一层打开的值折叠后是否等于下一层的值。一个远离低次的函数无法让所有这些承诺保持一致。
Rand 运行 80 次查询。在已证明的接近性间隙定理下,每次查询最多给作弊者 9/16 的成功机会,因此 80 次查询给出 (9/16)⁸⁰ ≈ 2⁻⁶⁶·⁴。在已部署的 STARK 所依赖的 ethSTARK 猜想下,每次查询只给 1/8,远小得多。
8. 去掉对话
上面的一切都是按对话来描述的,但交易必须是任何验证节点都能独立检查的一条消息。Fiat–Shamir 变换去掉了验证者:证明者用 Poseidon2 双工海绵,把每个“随机”挑战计算为迄今所有承诺的哈希。改变任何一个承诺,都会改变其后的每个挑战,因此证明者无法挑选承诺来迎合挑战。
另有两项措施补上由此留下的缺口:
- 扩展域。 由
2⁶⁴个元素构成的域太小,不适合用来抽取挑战:作弊者只需让挑战落在某个多项式少数几个”坏值”之一上,无论运行多少次查询,成功概率都约为N/2⁶⁴。因此,迹仍留在 Goldilocks 中,但每个挑战都取自它的二次扩展域,约有2¹²⁸个元素。 - 磨算(grinding)。 在派生查询位置之前,证明者必须找到一个随机数 nonce,使其与转录(transcript)的哈希以 20 个零比特开头,约需一百万次哈希。诚实的证明者只需付出一次这一代价。而作弊者若反复重抽查询位置,直到这些位置避开其伪造之处,则每次尝试都要付出这一代价。这是 Rand 中唯一的工作量证明,且与共识毫无关系。
合计:查询贡献 66.4 比特,加上磨算的 20 比特,得到 86 比特可证明安全;在猜想成立的前提下约为 100 比特,此时扩展域对应的项为限制因素。
9. 零知识从何而来
STARK 本身并不具备零知识性:在查询位置处打开的值是迹多项式的取值,若不加处理就会泄露信息。Rand 的配置增加了四层:
- 每个被承诺的多项式都经过随机化(Plonky3 的隐藏式 FRI 混入四个随机码字),因此这 80 个被打开的值,可以由从未见过迹的人在分布意义上复现;
- 每个 Merkle 叶子都被加盐,因此根和认证路径不泄露任何信息;
- 证明者的随机性在每一次证明时都从操作系统重新抽取,因此对同一笔支付生成的两个证明看起来互不相关;
- 每个声明的表都至少有
2⁷行,每个 LogUp 终值都被盲化(约束集 7)。一个证明会在 80 个查询点和两个域外点处打开每张表,而隐藏承诺混入的随机行数与表的行数相同,因此只有当表至少有 82 行时,它才是被隐藏的。128 是满足这一条件的最小的 2 的幂。
第 4 层是一次修正。2026-09-27 的审查从生产环境的证明中恢复出了输入字数不超过 31 的调用的私有输入,从程序较短的程序表中读出了该程序的取指次数,并凭未盲化的终值区分出一个 bundle 的真实输入槽与虚设槽。这些证明是在链 15 及更早的链上生成的。链 16(约束集 7,配以无分支的 bundle guest)堵住了这三处,链 18 至 20 保留了这些修复。
所提供的保证是随机预言机模型下的统计零知识。证明不隐藏的内容是明确的:它的层级(tier)、它声明的表高度,以及它的公开值(其中包括 gas 上限)。某个声明的 keccak、sha256 或 modq 高度为 0,表示该次运行从未使用过该表,因此调用证明的头部会显示该调用是否做过哈希,并且在下限之上,还能以两倍以内的精度显示做过多少次。对于 bundle,公开值和字段是锚点、四个 nullifier、四个承诺、费用、销毁额、销毁资产、时间、授权承诺和交易绑定。一笔转账所转移的资产是私有输入,因此一笔 zUSD 支付与一笔 RAND 支付在链上看起来相同。hc 是确定性的,因此任何人都能看出某个证明涉及的是 bundle guest(每个转移价值的证明都是如此),或者对于调用,看出它运行的是哪个已部署的程序。
10. 验证者检查什么
Machine::verify_public(hc, words, proof) 按开销从小到大依次执行:
- 在据此确定任何规模之前,先检查证明声明的层级和表高度是否在界限之内(
check_declared_heights),因此格式错误的头部只花几次比较,而不是数秒的密钥构建;一个 bundle 必须声明 tier 14,且既没有哈希表也没有modq表;单密钥授权证明为 tier 10;多签授权证明必须恰好是其固定的 tier-18 形状(keccak表为2¹⁴行,modq表为2¹⁶行,内存表为2²⁰行);每个调用、invoke 和状态证明在构建任何密钥之前都会拒绝modq表; - 共有 35 个公开值,每个都是规范的域元素,其中
pv[HC] = hc,pv[TIER]等于证明的层级,每个输出字都小于2³²,且GAS_LIMIT不超过头部的gas_max; - 批量 STARK 通过验证,所用的验证者密钥按形状
(tier, program, input, keccak, sha256, public heights)缓存,而不是按程序缓存; H_PUB等于words的摘要,对于 bundle 而言,它就是节点自行计算的交易绑定。
在此之前,节点会对每个提交或经 gossip 传播的证明检查其编码是否规范:其 FRI 折叠计划必须与诚实证明者针对头部所声明的次数所生成的计划一致。自 fullnode v0.6.8(审计 v6,ZKV-4)起,该检查首先拒绝任何超过 25 比特的声明次数(MAX_HONEST_DEGREE_BITS,即内存表的 2²⁴ 上限加上零知识的翻倍,是被接受的证明所能携带的最大值)。在设立这一界限之前,声明一个巨大次数的头部会让节点在其事件循环上重新推导出规模极其庞大的折叠计划,且发送者想占多久就能占多久;验证者本来就会拒绝该证明,因此这一界限改变的只是说”不”的代价。现在,检查内部发生 panic 只会拒绝那一笔交易,而不会使节点停止运行。
随后,账本以 bad = 0 重新计算 bundle 摘要 D,并将其与证明的八个输出字比较。自链 20 起,它还要求 bundle 证明的 GAS_LIMIT 恰好等于创世的 bundle_gas_limit,即 20,479,单密钥授权证明的为 1,279(在链 21 上,多签授权证明的为 436,159),因此每笔转账声明的值都相同。在密钥已预热的情况下,整个检查约需 16 ms。
参数及其选取理由
下列每个数字都属于共识:验证者会检查它,用任何其他值生成的证明都无法通过验证,而更改其中任何一个都意味着新的约束集和一次硬分叉(约束集)。FRI 配置由创世文件固定(链 21 上为 fri_profile: production,与链 20 相同),而不由证明决定。
| 参数 | 取值 | 取值理由 |
|---|---|---|
| 基域 | Goldilocks,p = 2⁶⁴ − 2³² + 1 | 一个 64 位素数,其运算只需几条机器指令,并且含有大小为 2³² 以内每个 2 的幂的乘法子群,因此把一列变成多项式就是一次快速傅里叶变换。迹放在这里,是因为它是代价最低的域。 |
| 挑战域 | 二次扩域 F_p²(≈ 2¹²⁸) | 若挑战直接取自 Goldilocks,可靠性会留下约 N/2⁶⁴ 的下限,任何数量的查询都无法降低它。扩域把这一项压到至多 2⁻¹⁰⁰,适用于至多 2²⁵ 行的迹。只有挑战为更大的域付出代价,迹不必。若日后需要 128 位,备选的余量是三次扩域(≈ 2¹⁹²)。 |
| 膨胀因子 | 8(码率 ρ = 1/8,log_blowup = 3) | 同时出于两个原因。CPU 表与 ALU 表的规则次数为 8,开启零知识标志后商多项式的次数为 9,这正是膨胀因子 8 所能承载的上限;次数更高的规则会迫使膨胀因子取 16。此外,每次查询抓住作弊者的能力取决于码率:可证明的是每次查询 (1 + ρ)/2 = 9/16,推测的是 ρ = 1/8。更大的膨胀因子可以少做几次查询,代价是证明者要计算并保存的低次扩展按比例增大。 |
| FRI 查询次数 | 80 | 使可证明的可靠性保持在约 86 位的数值。它曾被削减为 27(约束集 3 和 4),达到了推测的 100 位目标(3·27 + 20 = 101),却只剩约 42 位可证明的位数;2026-09-12 的 zk 审计(发现项 ZM1)把它恢复为 80。查询占去证明大小的大部分,这就是捆绑包证明约为 1.50 MB 的原因。要达到可证明的 100 位,需要 97 次查询。 |
| 暴力搜索 | 20 位,在查询阶段 | 在抽取查询位置之前,证明者必须找到一个 nonce,使其哈希以 20 个零位开头:约一百万次哈希,诚实的证明者付出一次,作弊者每次重新抽取位置都要再付出一次。它以很低的代价增加 20 位;更多的暴力搜索(34 位同样可得到可证明的 100 位)会使每个诚实的证明都变慢。承诺阶段没有暴力搜索。 |
| 折叠度 | 每轮至多 8(max_log_arity = 3) | 按八折叠而不是按二折叠,FRI 层数减为三分之一,因此每次查询打开的 Merkle 路径更少,证明更小,代价是每层多做一点工作。 |
| 最终多项式 | 常数(log_final_poly_len = 0) | FRI 一直折叠到底,并以明文发送一个值,这是验证者最简单的终端检查。 |
| 哈希(电路内、Merkle、转录) | Poseidon2,宽度 8,S 盒 x⁷,4 + 22 + 4 轮,码率 4,容量 4 | 一种按域运算的低成本思路设计的哈希,因此在证明内部做哈希(承诺、作废符、Merkle 路径、程序摘要)消耗的是行,而不是成千上万次位运算。S 盒指数与 p − 1 = 2³²·3·5·17·257·65537 不能有公因子,7 是满足条件的最小值。宽度 8、码率 4 给出由四个元素组成的摘要,约 256 位。86 个轮常数是节点中的一张字面常量表(poseidon2_constants.rs)。 |
| 摘要宽度 | 4 个域元素(≈ 256 位) | 对原像类用途(作废符、承诺:面对 Grover 为 128 位)足够。但对转录中 128 位的量子抗碰撞性不够(2²⁵⁶/³ ≈ 2⁸⁵);白皮书的设计要求 384 位的转录哈希,而运行中的链尚未采用。 |
| 隐藏 | FRI 中 4 个随机码字;每个 Merkle 叶 4 个盐元素;每个证明使用全新的操作系统随机数 | 这是使 80 个被打开的位置和 Merkle 路径仅凭公开值即可被模拟的最小配置,因此证明是统计零知识的。同一笔支付做两次的证明看上去互不相关。 |
| 表下限 | 每个已声明的表至少 2⁷ 行(约束集 7) | 一个证明对每个表打开 80 个查询点和 2 个域外点,共 82 个,而有 h 行的表由 h 个随机行盲化。64 行太少,128 是下一个 2 的幂。查询次数高于 126 则需要 2⁸。 |
| 终值盲化 | 每个表一个 F_p² 的值,位于 BLIND 总线上(约束集 7) | LogUp 终值由原始迹计算得出,因此没有盲化时,它就是对其表的一个未加盐的承诺。盲化值取扩域元素,是因为仅用一个基域坐标会把平移限制在一个很薄的集合内,候选见证可以被拿来逐一测试。 |
| gas 权重 | 每行 1;每个 Poseidon2 吸收行 3;每个 SHA256 行 64;每个 KECCAK 行 192(约束集 8) | 每个权重计入该行拉入其他表的行数:一个吸收行对应一次 32 行的置换,一个 SHA256 行对应 466 列的 64 行,一个 KECCAK 行对应 2,612 列的 32 行以及 100 个内存行。 |
| 最大约束次数 | 8 | 由上述膨胀因子决定;每个表的次数由测试固定,使新规则不会悄悄超出。 |
| 档位 | 10、12、14、16、18、20 | 表按六个固定的档位定大小,每档是前一档的四倍,而证明只暴露其档位:即运行长度的一个粗粒度、经过填充的分档,而不是其周期数。每个捆绑包都是 14 档,每个授权证明都是 10 档;自 fullnode v0.5.6 起,调用上限为 14 档。 |
| 最大迹 | 2²⁴ 行(开启零知识位时为 2²⁵) | 扩域项所证明适用的大小;更长的迹会被拒绝,而不会在一个并不成立的可靠性声明下被证明。 |
| 公开值 | 35 个域元素 | 入口 pc、档位、八个输出字、三个八字摘要 hc、H_IN、H_PUB,以及声明的 gas 上限:这是能够指明程序、私有输入、公开输入以及工作量上界的最小陈述。到约束集 7 为止它是 34 个。 |
| 证明大小上限 | 默认 2 MiB;链 21 为 6 MiB,链 17 至 20 为 4 MiB(max_proof_bytes) | 2 MiB 高于已部署的 guest 在没有哈希表时生成的所有证明,低于携带 keccak 的证明(≈ 3.3 MB);链 17 至 20 设为 4,194,304 以容纳两者,链 21 设为 6,291,456,为多签授权证明留出空间(估计为 3.4 MB 或更多;尚未测量)。链 13 至 16 设为 8 MiB。该上限在证明被解码之前检查。 |
这些数字合起来的结果:可证明的可靠性为 86 位,在 ethSTARK 猜想下约为 100 位(此时限制来自扩域,而不是查询),面对量子对手约为 85 位,由 256 位的转录哈希决定。白皮书有意把这两个可靠性数字并列给出:每一个已部署的 FRI-STARK 都要靠该猜想来支撑其最后几位,评估部署规模的读者应当知道哪一种假设承担了哪些位。
证明之上的证明
一个捆绑包证明约为 1.50 MB,已经不小,1.37 MB 的授权证明与它并行,而每个节点都要存储它们全部。已经构建出的补救办法是递归:一个证明,其陈述是“我验证了这些证明”。fullnode 带有第二台更小的机器来做这件事,即递归虚拟机,其原生字是一个 Goldilocks 元素,其指令就是验证者自己的运算(域运算、Poseidon2、Merkle 和 FRI 步骤)。它的聚合程序重放对 N 个捆绑包证明的完整验证,并发布一个覆盖它们公开值的摘要,该摘要绑定到链、聚合器和一个 nonce。在其中验证一个捆绑包证明,在约束集 6 下耗费 1,968,619 行,在约束集 7 下为 2,044,506 行;在约束集 8 或 9 下尚未重新测量。它已经实现,并且在迄今所有的链上都由创世关闭,链 21 也不例外。聚合程序只验证捆绑包证明,因此在拆分授权的链上,授权证明仍会保留。它的生产环境证明所需的内存也超出了原先的规划:在约束集 7 下测得为 133 GB 以上(即将到来:区块聚合)。
该方案不声称什么
- 量子安全余量约为 85 比特,而非 128 比特。 对
n比特哈希进行量子碰撞搜索的代价约为2ⁿ/³。白皮书的设计要求 384 比特的转录哈希;运行中的链使用的是 256 比特的 Poseidon2 摘要。将其加宽需要一套新的约束集,并且是一次硬分叉。 - 100 比特是猜想,86 比特才是已证明的。 若将已证明的安全位数提高到 100,需要 97 次查询,每个证明的查询数据约增加五分之一。
- Poseidon2 常数是一次带种子的抽取,并非已发布的标准实例。目前未知有针对它的攻击;若改用标准常数集,链上的每一个哈希都会改变。
- 有些陈述是在证明之外检查的,而不是在证明之内。
TIER公开值由验证者与证明的形状比对,但并未被电路约束(审计 ZL1);bundle guest 的污点纪律(taint discipline)依靠结构性论证、弱化 guest 测试框架和变异模糊测试来支撑,而非经过机器检查。 - 证明的头部并不隐藏。 层级(tier)和两个哈希表的高度都是明文,因此一次调用的证明会显示该调用使用的是
KECCAK还是SHA256。存在一个将两张表都填充进去的 prover 选项,但没有任何钱包设置它:它会使 tier-10 的证明从约 1.3 MB 增至约 3.6 MB。 POSEIDON2系统调用对变长输入不是单射的。 之所以保持现状,是因为每一个承诺、nullifier、根和程序摘要都依赖于它。目前尚无已发布的 guest 使用POSEIDON2_LEN。- 证明速度慢,由发送方完成,或由发送方配对的 prover 完成:一个 bundle 在笔记本 CPU 上约需 100 秒和 5.7 GB 内存。GPU 内核于 2026-09-28 首次在 GPU(NVIDIA H100)上运行,并与 CPU 参考实现的结果一致;钱包目前仍在 CPU 上证明。
更多阅读
- 白皮书中的“The Zero-Knowledge Scheme: How a Rand Proof Works”一节(
randprotocol.tex)以及相应的可靠性定理,逐一讲解了同样的七个要素。 - 实现论文(
randprotocol_implementation.tex)规定了指令集和系统调用、bundle guest、海绵结构和摘要、每一张表和总线、机器配置以及递归虚拟机。 - 代码:fullnode 中的
crates/randprotocol-zkvm/src/——emulator.rs、tables/、machine.rs、gas.rs、guests.rs、hash.rs、poseidon2_constants.rs。