卷 IV · 压短CH 17深度 17/23

证明我验过一个证明

验证一份证明,本身也是一段计算——而且是一段很短的计算(第 14 章:544 次哈希)。既然短,为什么不为它再生成一份证明?这个听起来像文字游戏的动作,是这门技术最后一块拼图,它打开三扇门。而能不能推开这三扇门,取决于一个具体的数字:把 SHA-256 写进电路要 33512 条约束,换成为算术而生的哈希只要 182 条。我把这 33512 条亲手数了一遍。

33512 条184×自指

◻ 本章先赊三条
第一条。「证明我验过一个证明」不会指数爆炸。每一层的成本是固定的,所以套 n 层的总成本是线性的,而不是指数的。 第二条。它可行不可行,卡在一个非常具体的工程数字上——「在电路里算一次哈希有多贵」。这个数决定了整个方向能不能走通。 第三条。这一步打开的三扇门里,最反直觉的一扇是:一条区块链的完整历史,可以被压成一个大小恒定的文件。

那个自指的动作

回忆第 14 章那台机器的验证过程:算一遍 Fiat–Shamir 的挑战,然后核对 34 条 Merkle 路径,一共 544 次 SHA-256。没有别的了。

这是一段极其规整、没有分支、长度固定的计算。而第 10 章告诉我们:任何这样的计算都能被摊成一张表、写成一堆约束、然后被证明。

陈述: 「存在一份证明 π,使得 Verify(陈述S, π) = 接受。」
见证: ⟦ 那份证明 π 本身 ⟧

于是我生成一份新的证明 π′,它证明的是上面这句话。
验证者拿到 π′,验一遍,就等于确信「有人拿着一份合法的 π」。

而 π 本身,可以扔了。

注意最后那行——那才是这一整章的意义:旧证明不必再传,它被一份新证明代表了。而新证明的大小,和旧证明是不是很大、和被证的原始计算有多长,全都没有关系。

门一:把整条历史压成一个文件

把上面那个动作接成链:

第 1 块:证明「第 1 块合法」                        → π₁
第 2 块:证明「第 2 块合法,且我验过 π₁」             → π₂
第 3 块:证明「第 3 块合法,且我验过 π₂」             → π₃
……
第 n 块:证明「第 n 块合法,且我验过 π_{n−1}」        → π_n

拿到 π_n 的人,只验这一份,就确信了从第 1 块到第 n 块的全部历史。
而 π_n 的大小,和 n 无关。

这不是思想实验。Mina 协议整条链的证明大小恒定在几十 KB 量级(官方长期宣传的数字是「约 22 KB」)——不管链上已经过了多少年、多少个区块。一部手机可以从零开始完整验证整条链,而不需要下载任何历史数据。

对照一下:以太坊的全量历史数据在 2026 年是若干 TB 量级,比特币也有几百 GB。「区块链必然越来越大」这个所有人都接受的前提,被这个自指的动作直接取消了。

门二:把一个大计算切开并行地证

第 14 章那笔账里最难看的一项是「证明者慢 164 倍以上」。递归给出了缓解办法:

把一亿步的计算切成 1000 块,每块十万步。
    → 1000 台机器并行,各证一块,得到 1000 份证明。
    → 两两合并:证明「我验过这两份证明」,得到 500 份。
    → 继续合并……10 轮之后剩下 1 份。

墙上时间从「一台机器跑一亿步的证明」
变成「一台机器跑十万步的证明 + 10 层合并」。

证明生成从一件本质串行的事,变成了一棵可以铺满整个集群的树。今天所有把证明时间压到秒级的系统,靠的都是这一招——第 21 章那个「一个以太坊区块 35 秒」,背后是几十上百块 GPU 在跑这棵树。

门三:把一种证明翻译成另一种

第 16 章那张表里的方案可以串联,做法就是递归:

用 STARK 证明那个巨大的计算        → 45 KB,不用仪式,抗量子,但太大不适合上链
再用 Groth16 证明「我验过那份 STARK」  → 128 字节,上链便宜

最终上链的是 128 字节。
而那 45 KB 的 STARK 从来没有上过链。

于是你同时拿到了两边的好处:证明者那一头用透明、抗量子、扛得住大计算的 STARK;链上那一头用极短的 Groth16。代价是最外面那一层仍然带着 Groth16 的假设(仪式、不抗量子)。这是今天多数生产系统的形状。

卡点:在电路里算哈希有多贵

上面三扇门都有同一个前提:把验证器写成电路。而验证器干的活是 544 次 SHA-256。第 10 章那张成本表说过,位运算在电路里是黄金——现在把这个数算出来。

我按最朴素的写法把一整个 SHA-256 压缩函数摊成了 R1CS 约束,逐条数:

电路算出来的 SHA-256("abc")
    ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
node:crypto 算出来的
    ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
★ 逐位相同                            true

★ 这个电路用掉的 R1CS 约束条数          33512
  其中:输入 512 位的 booleanity        512
  参考:circomlib 的优化实现约           3 万条(同一个数量级)

那个电路不是估算,它真的算出了正确的哈希值。(顺带一提:书里这个数由 JavaScript 算出,我又用 Python 独立写了一遍,同样得到 33512 和同一个摘要。)

现在把它代进递归的账:

要在电路里做的事用 SHA-256用 MiMC倍数
一次哈希33512 条182 条184×
一条 16 层的 Merkle 路径536192 条2912 条184×
34 条路径(= 一次递归验证)1820 万条9.9 万条184×

1820 万条约束,是一个「理论上可以、实践中别想」的数字;9.9 万条则是笔记本电脑几秒钟的事。

◆ 于是有了一族只为电路而生的哈希函数

MiMC(2016)、Poseidon(2019)、Rescue、Griffin——它们的设计目标和 SHA-256 完全不同:

SHA-256MiMC / Poseidon
为谁优化CPU 上的位运算电路里的乘法门
内部操作异或、移位、32 位加法只有域加法和乘方
在你笔记本上约 1 微秒更慢
在电路里33512 条182 条
被研究了多久二十多年,久经考验不到十年,仍在被密码分析

最后一行是要付的账:这些新哈希的安全性远没有 SHA-256 那么有把握。它们的代数结构非常简洁——而简洁的代数结构正是密码分析最喜欢的下手处。已经有几个同类设计因为被找到攻击而调整过参数。用它们,是拿「安全裕度」换「约束数」。这笔交易在这本书里出现过很多次,只是这一次换的是密码学本身的成熟度。

折叠:不必每一步都完整验证

2021 年之后有一个更省的路子,叫折叠(folding,代表作 Nova):

完整递归:每一步都把「验证上一份证明」整个写进电路。
折叠:    每一步只把两个「待证的实例」用随机数线性组合成一个,
          这个动作比完整验证便宜一两个数量级。
          折 n 次之后,最后只做一次真正的证明。

直觉上,折叠做的事和第 12 章那个 Freivalds 一样:用一个随机线性组合,把「这两件事都成立」压成「这一件事成立」,代价是一个可忽略的错误概率。递归的思想没变,只是把最贵的那一步从「每一层」推迟到了「最后一次」。

⌨ 自己跑一遍

那 33512 条约束你可以自己数一遍,而且能顺便验证这台电路算得对不对。关键在于:每个操作按它在 R1CS 里的真实代价计费。

import hashlib
cost = 0

def xor(a, b):                      # 每位一条:a⊕b = a+b−2ab
    global cost; cost += 32
    return [x ^ y for x, y in zip(a, b)]
def xor3(a, b, c): return xor(xor(a, b), c)          # 64 条
def ch(e, f, g):                    # g + e(f−g):每位一条
    global cost; cost += 32
    return [(x & y) ^ (~x & 1 & z) for x, y, z in zip(e, f, g)]
def maj(a, b, c):                   # t=a⊕b, u=ab, v=ct:每位三条
    global cost; cost += 96
    return [(x&y) ^ (x&z) ^ (y&z) for x, y, z in zip(a, b, c)]
def add(*xs):                       # 模 2³² 加:拆成 32+进位 位,每位一条 booleanity
    global cost; cost += 32 + (len(xs) - 1).bit_length()
    return bits(sum(num(x) for x in xs) & 0xffffffff)

bits = lambda n: [(n >> (31 - i)) & 1 for i in range(32)]
num  = lambda b: int(''.join(map(str, b)), 2)
rotr = lambda b, k: b[32-k:] + b[:32-k]              # 旋转:免费(只是接线)
shr  = lambda b, k: [0]*k + b[:32-k]                 # 右移:免费

# …… K、H0 常数表和 64 轮主循环见 SHA-256 标准,照抄即可 ……
# 全部代码约 60 行,跑完打印:

# 电路算出的 SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
# hashlib 算出的           = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
# 逐位相同: True
# ★ R1CS 约束条数: 33512
# MiMC(91 轮 × 2): 182  → 差 184 倍

数完之后有两个实验特别值得做:

(1)rotrshr 也计上费(比如每次 32 条),看总数涨到多少——你会发现它翻了一倍多。「哪些操作免费」这件事,直接决定了一个哈希函数在电路里的价格。

(2) 数一下 MiMC:91 轮,每轮一次立方( 一条、 一条),一共 182 条。然后把两个数除一下:184。

sha_cost, mimc_cost = 33512, 91 * 2
for name, depth, paths in [('一次哈希', 1, 1), ('16 层路径', 16, 1),
                           ('34 条路径', 16, 34)]:
    print('%-10s SHA-256 %10d 条    MiMC %8d 条'
          % (name, sha_cost*depth*paths, mimc_cost*depth*paths))
# 一次哈希      SHA-256      33512 条    MiMC      182 条
# 16 层路径     SHA-256     536192 条    MiMC     2912 条
# 34 条路径     SHA-256   18230528 条    MiMC    99008 条

python3 sha_count.py

在线跑:python.org/shell。想看真实的电路实现,去 circomlib 的 sha256/poseidon.circom——它们的约束数会打印在编译输出里,可以和你数的对一下。

▸ 在现实里
  • Mina。整条链的证明大小恒定在几十 KB 量级(官方长期使用的数字是约 22 KB)。「同步一条链」这件事,从「下载几百 GB」变成了「下载一个附件」。
  • 所有把证明时间压到秒级的系统。第 21 章会给出 2026 年的数字:一个以太坊区块的证明,单张 H100 上约 35 秒,用几十上百块 GPU 可以压到 10 秒以内。这个「用更多机器换更短时间」的能力,完全来自门二。
  • Poseidon 已经成了事实标准。今天几乎所有新的 zk 应用,链上数据结构都用 Poseidon 而不是 Keccak/SHA-256 做 Merkle 树,理由就是这一章那个 184 倍。而这带来一个生态割裂:以太坊主网的原生数据结构用 Keccak,于是「在电路里验证一段以太坊状态」比「验证一段 zk 原生状态」贵两个数量级。这是一个纯粹由历史造成的、正在被昂贵地兼容的问题。
  • 类比:增量编译和缓存。递归证明在结构上很像 make 的时间戳、Docker 的层缓存、Git 的 packfile:把「重新验证全部历史」换成「验证一个代表历史的短对象」。区别在于这里的「代表」是密码学意义上的,谁都伪造不了。
✗ 这个直觉是错的

「证明里套证明,肯定会指数爆炸吧。第一层验第二层,第二层验第三层……成本雪球滚下去,很快就算不动了。」

不会,而且理由很干净:每一层的电路是固定大小的。验证器的工作量是 O(log n) 的(34 条路径 × 树高),把它写成电路得到一个固定的约束数(比如 9.9 万条)。第 100 层要证的东西,和第 2 层一模一样大。

所以套 n 层的总成本是 O(n) × 每层固定成本——线性,不是指数。而且如果按门二那样做成树形合并,墙上时间只有 O(log n)

真正的困难不是爆炸,是那个固定成本本身太大:如果它是 1820 万条,那么「固定」也没用,一层都跑不动。这就是为什么这一章的关键数字是 33512 和 182,而不是任何一个关于递归的定理。

还有一个技术细节值得知道,因为它解释了一堆奇怪的曲线名字:把「验证 A 系统的证明」写进 B 系统的电路,要求两个系统的域能对上。这催生了「循环曲线对」(cycle of curves,比如 Pasta 曲线)——两条曲线互为对方的标量域,于是可以无限交替递归。看到 Pallas / Vesta 这类名字,那就是为了这一章存在的。

✓ 结账

这一章的一句话

一旦验证比执行便宜,验证本身就成了一段值得再证一次的短计算;这个自指动作让历史可以被压成恒定大小、让证明可以并行生成、让不同方案可以串联——而它能不能落地,只取决于「在电路里算一次哈希要多少条约束」这一个工程数字。

卷 IV 到此结束。这台机器的原理、成本、信任模型、选型和组合方式,你都有了。

下一卷去看它今天真正在做的三件事。第一件和隐私毫无关系——它省下的是「全世界重算一遍」这笔浪费,规模是上千亿美元级的;第二件比大多数人以为的脆弱得多,一个 1000 人的隐私池,有效匿名集可能只有 1.6 人第三件正在把「出示证件」这个动作整个换掉——人家要的是 1 比特,而你今天递过去的是 30 多比特。