卷 III · 摊开CH 11深度 11/23

编码一次,作弊染遍全局

这是这本书的招牌,而它一点也不神秘。把那 1024 个数看成一条曲线,再把这条曲线在 65536 个点上求一遍值。就这一步。做完之后,作弊者哪怕只改原表里的一格,新旧两张编码表也会有 64513 格对不上——他想小改也小改不了。同样戳三格,抓住的概率从 0.29% 变成 99.9996%。而这一切成立的理由,是你初中就学过的一句话。

★★ 0.29% → 99.9996%1023 / 65536低度扩展

◻ 本章先赊三条
第一条。「1024 个数」和「一条 1023 次曲线」是同一样东西的两种写法,可以自由互换,不丢任何信息。 第二条。两条不同的曲线,最多在 d 个点上重合(d 是它们的次数)。这句话就是整个领域的支点,而它是初中知识。 第三条。这一步不含任何密码学——没有哈希,没有椭圆曲线,没有难解问题。它是纯粹的组合事实。

回到那个死胡同

第 10 章把程序摊成了一张表。现在验证者面对的困境是:

读整张表  → 和重算一遍没区别(第 1 章说这不叫「简洁」)
只戳几格  → 作弊者改一格,抓住的概率 0.29%(第 3 章)

第 3 章已经指出了唯一的出路:把「作弊改多少」这个旋钮从他手里拿走。现在动手。

第一步:1024 个数 = 一条曲线

初中的事实:两点定一条直线,三点定一条抛物线。推广开来——

◆ 插值定理

给定 n 个点(横坐标互不相同),有且只有一条次数不超过 n−1 的多项式曲线穿过它们全部。

所以:那 1024 个数,就是一条 1023 次曲线在 1024 个位置上的取值。这两种表述完全等价——从表能算出曲线(叫「插值」),从曲线能算出表(叫「求值」),来回不丢任何东西。

这一步在有限域里同样成立(第 10 章说过,域里加减乘除齐全,所以插值这套代数照搬)。

第二步:把曲线画长

一条 1023 次曲线,在 1024 个点上有取值,在别的点上当然也有取值。于是我们做一件看起来毫无必要的事:

原表:  曲线在 1024 个位置上的值    ← 证明者本来就有的
编码表:曲线在 65536 个位置上的值   ← 把同一条曲线多算 64 倍的点

信息没有增加(还是那条曲线),
数据量涨了 64 倍。这个倍数叫 blowup。

看起来纯属浪费。但接下来这一步会把它变成整本书里最值钱的 64 倍。

第三步:那句初中的话

◆ 支点

一个不恒为零的 d 次多项式,最多有 d 个根。

推论:两条不同的、次数都不超过 d 的曲线,最多在 d 个位置上取值相同。

(因为它们的差是一条不恒为零的、次数不超过 d 的曲线,而「取值相同」就是「差为零」,也就是差的根。)

现在把这句话对准我们的场景:

诚实的表  → 曲线 A(1023 次)
作弊的表  → 曲线 B(1023 次)
两张表只差一格 → A ≠ B(因为插值是唯一的,表不同则曲线必不同)

于是 A 和 B 在整个域上最多有 1023 个位置取值相同。

而编码表有 65536 个位置。所以:

    相同的位置  ≤ 1023            →  1.5610%
    不同的位置  ≥ 64513           →  98.4390%

作弊者改了一格,编码之后有 98.44% 的位置对不上。而他对此无能为力——他不能选择「只改一点点」,因为编码不给他这个选项。

实测:把 65536 个位置全部数一遍

这不是估算。本机把两张编码表的 65536 个位置逐个比对数了一遍:

原始记录里不同的格子              1 / 1024      = 0.0977%

编码后相同的格子(穷举全域实测)    1023 / 65536  = 1.5610%
  理论上限(1023 次曲线最多几个根)  1023
编码后不同的格子                  64513         = 98.4390%
放大倍数(blowup)                64×

实测值 1023,理论上限 1023——顶到了。(顶得这么准是因为两条曲线的差恰好是「过那 1023 个未被改动的插值点」的那条曲线,它在这些点上必然为零,一个不多一个不少。)

于是那张抽查表整个翻了个面:

你戳几格原始记录编码之后
1 格0.0977%98.439026%
2 格0.1952%99.975634%
3 格0.2927%99.999620%
5 格0.4873%99.99999991%

戳三格漏掉的概率是 3.804 × 10⁻⁶,也就是 262914 分之一。而第 3 章里同样戳三格,抓住的概率只有千分之三。

改多改少,一个样

还有一个细节值得单独拿出来,因为它是这套机制真正的杀招:

作弊者把那一格 +1        → 编码后相同的格子 1023
作弊者把那一格 +31337    → 编码后相同的格子 1023(一样)

改动的「大小」完全不影响结果。只要改了,就是 98.44%。这跟你的直觉可能相反——在普通的数据里,改一个比特和改一整段,痕迹的大小明显不同。而在这里,「改了」和「没改」之间是一道悬崖,中间没有斜坡。

这正是第 3 章那句话的兑现:扩散率 δ 不再由作弊者决定,而是由编码方式决定,被钉在 0.98 附近。

✎ 术语正名

这个动作在文献里有好几个名字,它们说的是同一件事:

  • 低度扩展(low-degree extension,LDE):强调「把一个低次多项式扩展到更多点」。
  • 里德–所罗门编码(Reed–Solomon code):1960 年发明的纠错码,和二维码、CD、DVD、卫星通信、RAID-6 用的是同一个东西。它当年要的性质和这里一模一样:任何小扰动都产生大差异。
  • 码距(distance):两个不同码字之间至少有多少比例的位置不同。上面那个 98.44% 就是码距。

这一步没有任何密码学。它是 1960 年的纠错码理论,比公钥密码还早。零知识证明的核心引擎,是一件在通信工程里已经用了六十年的旧工具。

工业界为什么不用 64 倍

blowup 越大,码距越大,需要抽查的次数越少。那为什么不用 1000 倍?因为编码表是要被证明者算出来、承诺掉、并存起来的。blowup 直接乘在证明者的成本上。

真实系统的典型选择是 blowup = 8

blowup码距(至少)每格逃脱概率压到 2⁻¹⁰⁰ 要抽几格证明者成本
6498.4%1/6417×64
887.5%1/834×8
250%1/2100×2

这是一笔非常清晰的交易:blowup 换查询次数。而查询次数直接决定证明大小(第 13 章会算:34 次查询 ≈ 17.1 KB)。「证明大小」和「证明者成本」在这里是同一个旋钮的两端,第 16 章那张选型表的第一行就是它。

◆ 但有一个洞,必须现在就说

上面全部的推理都建立在一个前提上:作弊者提交的那张 65536 格的表,确实是某条 1023 次曲线的求值。

可他凭什么要老实?他完全可以交上来一张乱填的表——比如在诚实编码表的基础上只改动少数几个位置。这样的表根本不对应任何低次曲线,上面那条「最多 1023 个点重合」的推理对它不适用

所以真实的协议里还必须有一步:让验证者确信「你交的这张表,确实接近某条低次曲线」。这一步叫低度测试,工业界的做法叫 FRI——它的思路依然是抽查,只是抽的方式更巧(反复把曲线「对折」成一半长度,每折一次抽查几个点是否自洽,折 log n 次直到短得可以直接读完)。

大部分零知识科普会跳过这一段,于是留下一个不成立的论证。这本书不跳过,但也不展开——FRI 的完整可靠性分析是一篇正经论文的量,第 23 章给了入口。你只要记住这个洞的形状:「抽查能抓住小改动」这句话,前提是被抽查的东西真的是一份编码。

⌨ 自己跑一遍

这一章那个招牌数字,你可以在自己机器上花 0.3 秒复现出来。纯标准库,六十行:

import hashlib
P, G = 65537, 3                        # 素数域,3 是它的原根

def ntt(a, inverse=False):             # 快速求值/插值(数论变换)
    n = len(a); a = a[:]; j = 0
    for i in range(1, n):
        bit = n >> 1
        while j & bit: j ^= bit; bit >>= 1
        j ^= bit
        if i < j: a[i], a[j] = a[j], a[i]
    ln = 2
    while ln <= n:
        w = pow(G, (P - 1) // ln, P)
        if inverse: w = pow(w, -1, P)
        for i in range(0, n, ln):
            wn = 1
            for k in range(ln // 2):
                u, v = a[i+k], a[i+k+ln//2] * wn % P
                a[i+k] = (u + v) % P
                a[i+k+ln//2] = (u - v) % P
                wn = wn * w % P
        ln <<= 1
    if inverse:
        ni = pow(n, -1, P); a = [x * ni % P for x in a]
    return a

def lde(vals, N=65536):
    coef = ntt(vals, inverse=True)          # 插值:1024 个值 → 1023 次曲线
    return ntt(coef + [0] * (N - len(vals)))  # 求值:曲线 → 65536 个点

# 第 3 章那张计算记录
h = hashlib.sha256(b'spotcheck-mimc').digest(); C = []
for i in range(1024):
    if i % 16 == 0: h = hashlib.sha256(h).digest()
    C.append(int.from_bytes(h[(i%16)*2:(i%16)*2+2], 'big') % P)
x, trace = 12345, []
for i in range(1024):
    trace.append(x); x = pow(x + C[i], 3, P)

fake = trace[:]; fake[700] = (fake[700] + 1) % P      # ← 只改一格
print('原始记录不同的格子:', sum(a != b for a, b in zip(trace, fake)), '/ 1024')

A, B = lde(trace), lde(fake)
same = sum(a == b for a, b in zip(A, B))
print('编码后相同的格子: %d / 65536 = %.4f%%' % (same, 100 * same / 65536))
for k in (1, 2, 3):
    print('抽 %d 格:原始 %.4f%%   编码后 %.6f%%'
          % (k, 100*(1-(1-1/1024)**k), 100*(1-(same/65536)**k)))
原始记录不同的格子: 1 / 1024
编码后相同的格子: 1023 / 65536 = 1.5610%
抽 1 格:原始 0.0977%   编码后 98.439026%
抽 2 格:原始 0.1952%   编码后 99.975634%
抽 3 格:原始 0.2927%   编码后 99.999620%

三个实验值得亲手做:(1)+ 1 换成 + 31337,相同格子还是 1023。(2) 改两格、改十格,相同格子只会更少(≤ 1023 恒成立)。(3)N=65536 改成 N=8192(blowup 8),相同格子仍是 1023,但占比变成 12.5%——正好是上面那张表的第二行。

python3 lde.py # 本机 0.3 秒

在线跑:python.org/shell(65536 点的 NTT 在网页版可能偏慢,可先把 N 调到 8192)。想看工业级实现,找 winterfell(Rust)或 plonky3ldt 模块。

▸ 在现实里
  • 你口袋里就有一个。二维码用的是里德–所罗门码,最高纠错等级下遮住 30% 仍能读出来。它和这一章用的是同一个编码——只是那里用「大码距」来恢复被毁的数据,这里用它来被改的数据。一个正着用,一个反着用。
  • CD 上的划痕。1982 年的 CD 标准里就有 RS 码,能纠正连续 4000 个比特的错误(约 2.5 毫米的划痕)。旅行者一号从太阳系边缘发回的数据、每一次卫星通信、RAID-6 阵列,全都靠它。这个 1960 年的工具,六十年后成了零知识证明的引擎。
  • 「哈希也能做到类似的事」——但不能。哈希确实让「改一个比特就完全变样」,但它没有中间层次:你只能验整个文件(要读完),不能「抽查几个位置」。RS 编码的价值恰恰在于它把整体的一致性摊到了每一个局部,于是抽查才有意义。
  • 数据可用性采样。以太坊的 Danksharding 路线用这一章这套东西来解决另一个问题:如何让一个轻节点确信「这一大块数据真的被发布了」,而不必下载它。做法就是把数据 RS 编码,然后随机抽查几十个位置。同一把锤子,第二颗钉子。
✗ 这个直觉是错的

「把数据变长 64 倍,信息量也就多了 64 倍,所以验证者要检查的东西更多了才对——这怎么可能让验证变便宜?」

编码没有增加任何信息。65536 个值和那 1024 个值携带的信息完全相同——它们是同一条曲线,1024 个点就把它定死了,剩下 64512 个点是推出来的,不是新的。

多出来的不是信息,是冗余。而冗余的用处正是:让任何一处的谎言,在很多个地方同时露出马脚。验证者不需要读完 65536 个值,他只要戳 3 个——正因为谎言被摊薄到了几乎每一个位置上。

这里有一个更一般的判据,值得从这本书里带走:「加冗余」和「加信息」是两件事。校验位、副本、纠错码、审计留痕,全都是在不增加信息的前提下增加冗余;而冗余买到的东西是「局部可检查性」——你不用看全部,就能对全部下判断。这句话是整个零知识证明大厦的地基,而它跟密码学没有一点关系。

✓ 结账

这一章的一句话

把一张表看成一条曲线、再把曲线画长几十倍,就能把「改一格」强行变成「错几万格」;抽查从此有了意义,而这一步用的是纠错码,不是密码学。

但我们还差一步。上面比较的是两张表——可现实中验证者手里只有一张表,他没有诚实版本可以对照。他真正要检查的是:「这张表满足第 10 章那 1620 条约束吗?」

下一章把这两件事接起来:「一堆约束全部成立」可以被改写成「一条多项式恒等式成立」,而验证一个多项式恒等式,只需要在随机的一个点上代进去算一次。而且这一章会给你一个完全不含密码学的、可以当场感受的例子——验一个 512×512 的矩阵乘法,不重算,快 58 倍。