5.4 零知识证明:不亮底牌的自证(zk-SNARKs 与 zk-STARKs)


5.4 零知识证明:不亮底牌的自证

**零知识证明(ZKP)**是证明者让验证者确信某陈述为真、却不泄露陈述之外的任何信息的协议,需同时满足完备性(真陈述总能说服人)、可靠性(假陈述几乎无法蒙混)与零知识性(验证者学不到额外信息)。工程主线是把计算编译成电路,再压缩成短证明(zk-SNARKs)或抗量子的透明证明(zk-STARKs)。本节用洞穴传说与两个可运行的协议复现三性质,再拆现代流水线。

环形山洞与一句咒语

环形山洞的通道走到尽头,被一扇魔法门隔断,念对咒语才能穿过。你想向我证明你知道咒语,又不想让我听到咒语本身——零知识证明的原始场景就此成立。办法:你先随机走进左侧或右侧通道,我在洞外喊声"从左边出来"或"从右边出来"。你若真会咒语,无论身处哪侧都能穿门照办;若不会,你只有一半的概率蒙对(恰好在被要求的那侧)。单轮防不住骗子,但连着几十轮,蒙混概率按一半的幂次坍缩到尘埃——我用"几乎为零的错误率"换来了"你确实会咒语"的信念,而咒语本身从头到尾没有出过你的口。

区块链需要它,动机直白得像产品需求:矿工要验证"这笔交易花的是我自己有权花的钱、账是平的",但金额、余额、收款方都该保密;第 6 章还会看到第二动机——把海量计算的验证压缩成一次毫秒级验签。ZKP 让"验证"与"知晓"正式分家:验证者确认结论,不占有过程。

图 5-4 从洞穴到流水线:ZKP 的两副面孔

图 5-4 从洞穴到流水线:ZKP 的两副面孔

协议复现一:洞穴的骰子版

把山洞协议写成代码,验证"诚实者百战百胜、骗子指数露馅":

import random def cave_round(knows_spell: bool) -> bool: """一轮洞穴挑战:验证者视角的通过判定""" entered = random.choice(["左", "右"]) # 证明者随机进入 asked = random.choice(["左", "右"]) # 验证者随机点名 if knows_spell: return True # 会咒语必能穿门 return entered == asked # 不会则碰运气 def convince(knows_spell: bool, rounds: int = 20): for r in range(rounds): if not cave_round(knows_spell): return f"第 {r+1} 轮穿帮" return f"撑满 {rounds} 轮" random.seed(11) print("诚实者:", convince(True)) # 每轮都过 print("骗子: ", convince(False)) # 期望撑不过几轮 cheat_survive = sum(cave_round(False) for _ in range(20000)) / 20000 print(f"骗子单轮存活率实测 {cheat_survive:.3f} -> 约一半;二十轮后只剩二分之一的二十次方")

零知识性在哪?验证者看到的只是"左右左右……"的点名序列与"成功、成功……"的结果——这些信息用一枚硬币自己也能生成(模拟器论证的直觉版),因此除了"你会咒语"这一件事,验证者的知识没有净增。

协议复现二:不透露口令的持证证明

第二个实验更进一步:证明"我知道某个数的离散对数",全程不透露该数。协议是现代签名与 SNARK 挑战机制的共同祖先——承诺、挑战、应答三段式(Sigma 协议):

import random # 小素数域上的教学参数(真实系统用大素数与椭圆曲线) p, g = 2333, 73 class Prover: def __init__(self, secret): self.x = secret self.y = pow(g, self.x, p) # 公开承诺值 def commit(self): self.r = random.randrange(1, p - 1) self.t = pow(g, self.r, p) # 承诺:g^r return self.t def respond(self, c): # 应答:s = r + c*x(模 p-1,由费马小定理的周期性) return (self.r + c * self.x) % (p - 1) def verify(y, t, c, s) -> bool: return pow(g, s, p) == (t * pow(y, c, p)) % p # g^s == t * y^c alice = Prover(secret=1999) # 秘密从不外传 y = alice.y t = alice.commit() c = random.randrange(0, 1) # 二元挑战(此处取单比特演示) s = alice.respond(c) print(f"公开信息: y={y}, 承诺 t={t}, 挑战 c={c}, 应答 s={s}") print("验证:", verify(y, t, c, s)) # True:确信她持有秘密 # 冒充者没有 x,只能事后凑 s -> 被随机挑战击穿 impostor_r = random.randrange(1, p - 1) fake_t = pow(g, impostor_r, p) fake_s = (impostor_r + 1 * y) % (p - 1) # 赌挑战为 1 print("冒充验证(挑战恰为 0 时):", verify(y, fake_t, 0, fake_s))

读代码抓三个要点:承诺先行——先亮 g^r 再接受挑战,杜绝"看了题再编答案";挑战必须随机——随机性是可靠性的发动机( Fiat-Shamir 变换用哈希替代真人掷硬币,把交互压成单方可验的非交互证明,链上场景的标配);应答可验不可逆——验证等式成立说明 s 同时含 r 与 x 的信息,但 s 是一次一密式的混合,反解不出 x。把这套骨架换到大素数、换成多项式承诺,就是现代 SNARK 的心脏。

工程现实:SNARK 与 STARK 的选型账

工程流水线把"某计算成立"编成电路再压缩成证明,两大主流的账本对比如下:

维度 zk-SNARKs zk-STARKs
可信设置 需要(仪式生成公共参数,废料有毒则可造假) 不需要(透明:参数纯来自公开哈希)
证明尺寸 极小(几百字节内) 大(数十至数百千字节级)
验证成本 毫秒级、常量级 与电路规模相关,较高
证明生成 较快(相对) 较慢(更大的多项式运算)
抗量子 否(依赖椭圆曲线配对类难题) 是(只依赖哈希与纠错码假设)
典型应用 隐私转账、递归证明聚合 大规模计算完整性、抗量子路线

⚠️ 工程现场的三条纪律:其一,电路正确性是新的攻击面——数学没破、电路写错,历史上有隐私链因电路旁路导致伪造证明;其二,可信设置仪式(多方参与、只要一人诚实销毁废料即安全)是治理事件,参与方与流程要公开可审计;其三,证明生成的算力成本决定产品形态——手机钱包跑不动重型证明,"本地生成、外包验证"的分工(以及随之的委托隐私问题)要提前设计。

💡 三句判词:可靠性靠随机挑战,零知识靠可模拟;SNARK 卖紧凑,STARK 卖坦荡;Fiat-Shamir 让对话变成单方演讲

攻防边界:ZKP 护住与护不住的

护住的:陈述本身的秘密(余额、见证数据、电路私有输入)、计算的完整性(外包计算没被篡改)。护不住的:陈述的选择本身——你证明"我有权动这笔钱"这个动作仍然可见(隐藏行为要做默克尔树式聚合或池化);元数据(时间、频率、证明的电路标识);错误的陈述设计(电路只证明程序员让它证明的事,逻辑漏洞照样"合法"自证)。量子前景上,STARK 系与哈希承诺天然抗量子,SNARK 系依赖的配对难题则需向第 6 章的后量子迁移看齐。

重器入柜。下一节回到主力小仪器:同态加密的密文运算、环签名的人群掩护、混币的博弈论——它们与 ZKP 组合成真实隐私链的完整拼图。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U