先钉死,再挑战
上一章说抽查的力气来自扩散率。但在那之前还有一个更基础的前提,一旦破了,扩散率是多少都无所谓:证明者必须在看到你要戳哪一格之前,就把整条带子固定住。否则他就是在看着答案填空——本机实测,一千轮,1000 比 0 全过。这一章讲那个负责钉死的装置:承诺。
一千轮,1000 比 0
把上一章的抽查再演一遍,只改一个地方:让作弊者在听到你要戳哪一格之后,才决定那一格填什么。
情形甲(没有承诺)
验证者:「我要看第 391 格。」
作弊者:(现在开始现算第 391 格应该是几)「是 24183。」
验证者:(一查,对的)「通过。」
——他每一次都能过,因为他只需要把被问到的那一格算对。
情形乙(有承诺)
作弊者:(先交出整张表的一个 32 字节指纹)「钉死了。」
验证者:「我要看第 391 格。」
作弊者:只能报出他当初写进表里的那个数,改不了。
——他必须在不知道会被问哪一格的情况下,事先猜对。
跑一千轮,用最简单的「二选一挑战」来量:
1000 轮 · 先看挑战再作答(没有承诺) 1000 / 1000 全过 1000 轮 · 先钉死再看挑战(有承诺) 485 / 1000 = 48.5%
48.5% 正是我们想要的——它就是第 2 章那个「每轮蒙对的概率」,重复几十轮就压下去了。而 100% 是一堵墙:再抽多少轮都没用,因为每一轮他都是满分。
所以这本书里所有的协议,第一步永远是同一个动作:把话钉死。
信封
负责钉死的那个装置叫承诺(commitment)。它的物理原型是一个信封:
你把一个数写在纸上,塞进信封,封口,把信封交给对方。
藏得住 hiding 对方拿着信封,看不出里面写的是什么。
改不了 binding 你也没法在事后换一张纸进去——
等到「打开」的时候,你只能拿出当初塞进去的那张。
这两条是拉扯的。做到「藏得住」最简单的办法是什么都别给他——但那样你事后想写什么都行,改不了就没了。做到「改不了」最简单的办法是直接把纸给他——但那样就藏不住了。承诺是同时做到这两件事的装置。
承诺不是加密。这是这个概念被误解得最多的地方。
加密是可逆的:有钥匙的人可以从密文还原出明文,这是它存在的目的。承诺是单向的:谁都不能从承诺值还原出原文,包括你自己。想让别人看到里面装的是什么,唯一的办法是你主动把原文和盐一起公布出来,让别人自己算一遍对上。
换个说法:加密解决「我想让你以后能看到,但别人现在不能看」;承诺解决「我想证明我现在就想好了,但先不告诉你想的是什么」。它锁住的不是内容,是时间。
最简单的承诺:哈希 + 盐
实际做法就一行:
承诺值 = SHA-256( 盐 ‖ 消息 ) 盐 salt:一串随机字节,通常 32 字节,你自己留着。 打开:把「消息」和「盐」都公布出来,别人自己算一遍,对得上就认。
为什么「改不了」?因为要换一张纸进去,你得找到另一对 (盐′, 消息′),让它们哈希出同一个值——那是哈希碰撞,对 SHA-256 来说要试约 2¹²⁸ ≈ 3.4 × 10³⁸ 次(生日界)。
那为什么必须加盐?看下面这个实测:
# 承诺一个 0–9 的数字(比如猜拳、比如投票选项),不加盐 承诺值 = 7902699be42c8a8e… # 攻击者:把 0 到 9 挨个哈希一遍,对比 穷举 10 个候选找回原值 → 7 用了 28.2 微秒 # 加 32 字节盐之后 承诺值 = e392ce4831ca8376… 要猜中得试 2²⁵⁶ 次
28.2 微秒。不加盐的哈希承诺,在消息空间很小的时候,等于直接把答案喊了出来。而「消息空间很小」在现实里是常态:一个投票选项、一个出价档位、一个 0–100 的分数、一个手机号(1 亿量级,现代 GPU 几秒钟)。
看到 hash(x) 被当成承诺用,先问一句:x 的可能取值有多少种?
如果这个数目在 2⁶⁰ 以内,这个「承诺」就是明文。加盐的成本是 32 个字节,不加盐的成本是整个协议。
承诺让「抽查」变得可能
把这一章和上一章接起来,就得到了这本书前半部分的骨架:
| 步骤 | 谁做 | 解决的问题 |
|---|---|---|
| 1. 把整张表承诺掉 | 证明者 | 他不能事后改答案(这一章) |
| 2. 抛出随机挑战:戳哪几格 | 验证者 | 他事先不知道会被问哪里(第 2 章) |
| 3. 打开被戳中的那几格 | 证明者 | 只暴露必要的部分(第 13 章) |
| 4. 检查这几格是否自洽 | 验证者 | 抓不抓得住取决于扩散率(第 11 章) |
这四步就是所有现代证明系统的形状。后面十九章全部是在把这四步里的某一步做得更好:第 8 章会把第 2 步的「验证者抛骰子」换成一台哈希(于是不需要验证者在场了),第 11 章会把第 4 步的成功率从 0.29% 拉到 99.9996%,第 13 章会让第 3 步的代价从「整张表」压到「512 字节」。
顺便,第 3 步那个「只打开被戳中的那几格」,正是零知识长出来的地方——既然验证者只看到了几格,他自然学不到整张表。这也是为什么这本书把零知识排在卷 II 中间,而不是开头:它是这个结构的副产品,不是设计目标。
先亲手把不加盐的承诺撬开,这件事快得让人不适:
import hashlib, time
sha = lambda s: hashlib.sha256(s.encode()).hexdigest()
secret = 7
naive = sha(str(secret)) # 不加盐地承诺一个 0–9 的数字
print('承诺值', naive[:16], '…')
t = time.perf_counter()
for guess in range(10): # 攻击:把所有可能挨个试
if sha(str(guess)) == naive:
print('反推出原值 =', guess, '用了 %.1f 微秒'
% ((time.perf_counter() - t) * 1e6))
break
import os
salt = os.urandom(32).hex() # 加盐之后
print('加盐承诺', sha(salt + str(secret))[:16], '… 要猜中得试 2^256 次')
再把「顺序」这件事跑出来。同一个作弊者,只有出手顺序不同:
import random
rounds = 1000
no_commit = sum(1 for _ in range(rounds) # 先看挑战再答:必胜
if True)
committed = sum(1 for _ in range(rounds) # 先钉死再看挑战:只能赌
if random.randint(0, 1) == random.randint(0, 1))
print('没有承诺 %d/%d' % (no_commit, rounds))
print('有承诺 %d/%d = %.1f%%' % (committed, rounds, 100 * committed / rounds))
# 没有承诺 1000/1000
# 有承诺 485/1000 = 48.5%
把 secret 从 7 换成一个手机号试试——搜索空间涨到 10 亿,纯 Python 大约要几分钟,而一张现代显卡在几秒内就能跑完。「消息空间够大」从来不是一条能依赖的防线。
python3 -c "import hashlib;h=hashlib.sha256(b'7').hexdigest();print([g for g in range(10) if hashlib.sha256(str(g).encode()).hexdigest()==h])"
在线跑:python.org/shell,纯标准库。
- 密封投标。所有人把报价装进信封,同时开标。这就是承诺的原型,而且它的两条性质在这里都有名字:信封不透光是「藏得住」,开标前不许换标书是「改不了」。招标现场那些防止调包的流程(编号、签字、录像),全都是在加固后一条。
- 科研预注册。研究者在收数据之前,先把假设和分析方法公开登记。这直接对应本章第一句话:不预注册,研究者就可以在看到数据(挑战)之后再决定分析口径(答案),于是他永远能「发现显著结果」。p 值操纵(p-hacking)就是「没有承诺的证明系统」在统计学里的形态,它的通过率同样是 100%。
- 网上猜拳。两个人隔着网络怎么公平猜拳?先各自发
hash(盐 ‖ 我的出招),都发完之后再公布原文。这是最小的完整承诺协议,而且它必须加盐——否则对方把「石头/剪刀/布」三个字符串哈希一遍就全知道了。三个候选,比上面那个 0–9 还容易撬。 - Git 的提交哈希。一个 commit hash 承诺了整个仓库在那一刻的完整内容(包括全部历史)。你事后改任何一个字节,哈希就变了。这就是为什么「把 commit hash 写进发布说明」是有意义的——它把「我发布的就是这份代码」从一句承诺变成了一个可核对的事实。第 13 章会说明它内部的树状结构如何让「只打开一个文件」变得便宜。
- 反过来的例子:抽奖。正规的链上抽奖必须先承诺随机种子、再公布。没有这一步的抽奖,主办方可以在看到参与名单之后再挑种子——和上面那个 1000/1000 是同一件事。
「承诺不就是哈希一下嘛。我把数据 sha256 一下发出去,就算钉死了。」
哈希只给了你「改不了」那一半,而且「藏得住」那一半它一点都没给。刚才实测过:一个 0–9 的数字,28.2 微秒就被反推出来了。哈希函数的设计目标里从来没有「隐藏低熵输入」这一条。
还有一个更隐蔽的坑:就算加了盐,你也得把盐真的随机取。用时间戳当盐、用计数器当盐、几个承诺共用一个盐——这些写法在代码评审里看起来都「加了盐」,但搜索空间根本没变大。
正确的心智模型是:承诺 = 哈希(改不了)+ 足够的随机性(藏得住)。这两半来自不同的地方,缺谁都不行。第 13 章会看到一个更强的版本(多项式承诺),第 15 章会看到一个连「藏得住」都要靠一场仪式来保证的版本。
这一章的一句话
承诺是把「我事先就想好了」这句口头保证,变成一个可以事后核对的数学事实;它锁住的不是内容,是顺序——而顺序一旦丢了,无论抽查多少次、扩散率多高,作弊者都是满分。
卷 I 到此结束。你已经有了这台机器的两个零件:先钉死(第 4 章)和随机戳(第 2、3 章),也知道了它缺的那个关键零件是「让作弊染遍全局」(第 11 章补上)。
下一卷把这几个零件装成一台最小的、真正能跑的证明机器——它只有三步,跑在比特币和以太坊每天都在用的那条椭圆曲线上。而且它一轮就买到 256 比特的可靠性,比山洞的 40 轮强了不止一点。更有意思的是第 6 章:同一个人对两个不同的挑战都答对了,我就能当场把他的私钥算出来——这不是攻击,这是「知道」这个词的定义。