卷 I · 分家CH 03深度 3/23

抽查为什么平时不管用

这本书叫《抽查》,而这一章要证明抽查是没用的。一份一千零二十四步的计算记录,作弊者只改了一格——你随手戳三格抓住他的概率是 0.29%;你要抽到 99% 的把握,得戳 4714 格,比整份记录还长 4.6 倍。朴素的抽查不但抓不到人,还比老老实实重算一遍更贵。这本书剩下的四分之三,都是在修这个「没用」。

4714 格扩散率作弊者控制的旋钮

◻ 本章先赊三条
第一条。抽查的力气不是来自你抽了多少次,而是来自错误的扩散率——而这个旋钮,默认握在作弊者手里。 第二条。现实里的抽样审计之所以还能抓到人,是因为造假通常是成片的;而计算作弊只需要改一格 第三条。因此整个领域真正的技术核心,是一句听起来毫不神秘的话:先把要检查的东西改写一遍

一份真的计算记录

先造一个具体的东西,后面十几章都会用它。规则简单到可以口算:

从一个数开始,反复做同一个动作:
     加上一个固定的常数,然后立方。

     x₀ = 12345
     x₁ = (x₀ + c₀)³
     x₂ = (x₁ + c₁)³
     ……一共 1024 步。

为了不让数越滚越大,所有算术都在「模 65537」下做
(也就是算完除以 65537 取余数)。

这个东西有个名字叫 MiMC,是一种专门为了「在证明系统里便宜」而设计的哈希函数,第 17 章会解释它为什么长这样。现在你只要把它当成一段普普通通的、有 1024 步的计算。跑出来的前八格是:

格号   0      1      2      3      4      5      6      7    …   1023
值   12345  26688  46524  58581  11769  24101  24183  19394  …  61995
     └─输入                                                      输出─┘

这 1024 个数就是这本书里的「计算记录」——把一段计算的每一步中间状态都写下来的那张表。证明者声称:「我从 12345 出发,按规则跑了 1024 步,得到 61995。」他要证的就是这句话。

✎ 术语正名

这张表在文献里叫 trace(执行轨迹)。中文常见的翻译是「踪迹」「轨迹」,都容易让人以为是日志之类的东西。它其实就是草稿纸:你做一道长算术题时写在纸上的每一行中间结果。

整本书要做的事,用一句话说就是:让别人相信你的草稿纸是对的,而不必让他看这张纸,也不必让他重算一遍。这句话值得记住,因为后面所有的技术,都可以放回这句话里检查它在解决哪一半。

作弊者只需要改一格

现在假设证明者撒谎。他想让最后那个输出变成别的数——比如把一笔转账的金额改掉,或者让一道本来无解的题「有解」。他要付出的最小代价是什么?

答案是:改一格。他把第 700 格的数悄悄加 1,然后从那里往后重新算下去。得到的整张表在逻辑上是自洽的(每一步都符合规则),只有第 700 格那里断了一下。

好,现在轮到你了。你是验证者,不想读完 1024 个数(那就等于重算了)。你决定随机戳几格看看。

你戳几格抓住他的概率
1 格0.0977%基本等于没戳
3 格0.2927%一千次里抓到三次
10 格0.9723%
100 格9.3083%已经读了十分之一的表,抓住的概率还不到一成
1000 格62.3576%几乎把整张表读完了,还有三分之一漏网

把要求提到「99% 的把握」,需要戳多少格?

要 99% 把握      → 4714 格   (整份记录只有 1024 格,是它的 4.60 倍)
要 2⁻⁴⁰ 把握     → 28378 格  (27.7 倍)

你没看错。因为是随机戳、允许重复,要 99% 的把握必须戳 4714 次——而整张表只有 1024 格。与其这样,不如从头到尾读一遍,1024 次,还是 100% 的把握。

这就是抽查在原始形式下的处境:它不但不省事,它比老老实实重做还贵。

旋钮在谁手里

把上面那件事写成算式,就能看清问题出在哪。设作弊者污染了整张表的比例是 δ(错误的扩散率),你戳 k 次:

抓住的概率 = 1 − (1 − δ)^k

k 是你的旋钮:你想戳几次就戳几次,只是每次都要花钱。
δ 是他的旋钮:他想污染多少就污染多少,而他当然会选最小的那个。

作弊者会把 δ 压到 1/1024。于是你的 k 必须涨到几千,才能把乘积拉起来。这是一场你注定输的军备竞赛,因为对方的旋钮比你的便宜得多。

反过来看就很清楚了——如果作弊者被迫大改,抽查立刻变得极其好用:

他改了多少格扩散率 δ你只戳 3 格,抓住的概率
1 格0.10%0.2927%
10 格0.98%2.9012%
100 格9.77%26.5290%
512 格(一半)50.00%87.5000%

同样戳三格,抓住的概率从 0.29% 跳到 87.5%。差别完全不在你身上,全在他改了多少。

◆ 这本书的技术核心,一句话

既然赢不了这场军备竞赛,那就把对方的旋钮拿走

办法是:不要直接抽查那张原始的表,而是先把它改写成另一种形式,这种形式有一个性质——原表里任何一处哪怕只改一个数,改写后的版本都会有 98% 以上的位置对不上。

这样 δ 就不再由作弊者决定,而是由编码方式决定,被钉在 0.98 附近。他想小改也小改不了:一改就是一大片。于是 k = 3 就够了。

第 11 章会把这件事做出来,用的是一个高中生就能验证的事实。那一章是这本书的招牌。

顺便说,这也解释了为什么现实中的抽样审计还能抓到人:财务造假通常是成片的。一家公司要虚增十个亿的收入,做不到只改一张凭证——它得伪造一整条业务链:合同、发票、出入库单、银行流水、物流记录。δ 天然就很大,所以抽 30 张就有相当高的命中率。

而计算作弊不是这样。改一个数字就够了,剩下的部分让计算机重新算一遍,天衣无缝。这就是为什么审计业的经验不能直接搬到这里来。

⌨ 自己跑一遍

这一章的每个数字都能用一行算术复核。先看那张「戳几格」的表:

import math

n = 1024                                  # 记录长度
for k in [1, 3, 10, 100, 1000]:
    p = 1 - (1 - 1/n) ** k
    print('戳 %5d 格:抓住 %.4f%%' % (k, 100 * p))

for target in [0.99, 2 ** -40]:
    need = math.ceil(math.log(1 - target) / math.log(1 - 1/n))
    print('要 %.10g 的把握,得戳 %d 格(记录只有 %d 格)' % (target, need, n))

# 反过来:他改得越多越好抓
for bad in [1, 10, 100, 512]:
    print('他改 %3d 格,你戳 3 格抓住 %.4f%%'
          % (bad, 100 * (1 - (1 - bad/n) ** 3)))
戳     1 格:抓住 0.0977%
戳     3 格:抓住 0.2927%
戳    10 格:抓住 0.9723%
戳   100 格:抓住 9.3083%
戳  1000 格:抓住 62.3576%
要 0.99 的把握,得戳 4714 格(记录只有 1024 格)
要 9.094947018e-13 的把握,得戳 28378 格(记录只有 1024 格)
他改   1 格,你戳 3 格抓住 0.2927%
他改  10 格,你戳 3 格抓住 2.9012%
他改 100 格,你戳 3 格抓住 26.5290%
他改 512 格,你戳 3 格抓住 87.5000%

再把那张计算记录本身跑出来。这段代码复现的就是书里那张表,后面十几章都会用到它,值得留一份:

import hashlib

P = 65537
h = hashlib.sha256(b'spotcheck-mimc').digest()
C = []                                   # 1024 个轮常数,从 SHA-256 里取
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)              # 加常数,立方,模 65537

print(trace[:8], '...', trace[-1])
# [12345, 26688, 46524, 58581, 11769, 24101, 24183, 19394] ... 61995

python3 -c "n=1024;print(*['%d 格 %.4f%%'%(k,100*(1-(1-1/n)**k)) for k in (1,3,10,100,1000)],sep='\n')"

在线跑:python.org/shell,纯标准库。上面那张表的前八格和最后一格,和书里印的逐个相同——这本书里每一张记录你都能自己跑出来。

▸ 在现实里
  • 体育兴奋剂检测。抽检的威慑力完全建立在「运动员不知道什么时候会被抽到」上。而一旦作弊方式变成「只在某几天用、其余时间干净」,δ 就被压到极小,抽检立刻失效。反兴奋剂机构的应对不是提高抽检频率(k),而是引入生物护照——记录长期指标,让任何一次用药都在整条曲线上留下痕迹。这正是「把小改动放大成大改动」的现实版。
  • 校验和与纠错码。你每天都在用「让一处错误染到很多位置」这件事:一个 CRC32 让任何一个比特翻转都改变整个校验值;RAID、二维码、光盘上的里德–所罗门码更进一步,能在几百个字节被毁掉的情况下把原始数据算回来。第 11 章要用的那套编码,和二维码用的是同一个东西。这不是巧合——它们要的性质一模一样:任何小扰动都要产生大差异。
  • 代码评审。一个 3000 行的 PR,评审者读 100 行,抓住 bug 的概率非常接近这一章那张表。这也是为什么「小 PR」不只是礼貌问题:它把 δ 抬高了——在一个 50 行的改动里,任何问题都占了很大比例。
  • 数据库审计日志。一条被篡改的记录,在普通日志里只影响一行;而在 Merkle 化的日志里(第 13 章),它会让之后的每一个哈希都对不上。同一个思想:让局部的谎言产生全局的痕迹。
✗ 这个直觉是错的

「多抽几次就行了。抽 3 次不够就抽 30 次,抽 30 次不够就抽 300 次——总能把概率堆上去。」

这个直觉在数学上不算错,但它把成本算反了。上面那张表里,戳 1000 格才有 62% 的把握,而整张表只有 1024 格——你已经付出了「重算一遍」的代价,却只拿到了「三分之二的把握」。抽查此时不是一个更便宜的方案,是一个更贵、更差的方案。

更要命的是,这里的成本关系是反过来的:记录越长(n 越大),要达到同样把握需要的抽查次数就越多(大约正比于 n)。抽查的开销随被检查的东西线性增长,这恰好是「简洁」的反面。

正确的方向不是加大 k,而是动 δ。第 11 章会把 δ 从 0.001 推到 0.98,然后 k = 3 就够了;工业系统用 k = 34 把作弊概率压到 2⁻¹⁰², 而且这个 k 不随记录长度增长。判据是:看到「抽样检查」四个字,先问 δ 是谁在定。如果是被查的人在定,这个检查大概率是摆设。

✓ 结账

这一章的一句话

抽查能不能抓到人,取决于一个作弊者说了算的旋钮——错误的扩散率;而这个领域全部的技术含量,就是把这个旋钮从他手里抢过来,办法是先把要检查的东西改写成「改一处就会错一大片」的形式。

不过在改写之前,还有一件更基础的事必须先办:他得先把话钉死。如果他可以在看到你要戳哪一格之后再决定那一格填什么,那么无论你戳多少格、无论 δ 是多少,他都是满分。下一章有一个一千轮的实测:先看题再答的作弊者,1000 比 0 全过。以及一个具体的坑——承诺不加盐时,把它反推回去只要 28.2 微秒