
零知识证明这个词第一次砸到我面前是在一次需求评审上业务方要验证用户“符合某个条件”但用户死活不愿意把原始数据交出来。当时会议室里有人提了加密有人提了脱敏最后落到一个更根本的问题上——能不能只让对方相信“结论成立”而不暴露“结论为什么成立”。这就是零知识证明要解决的事。它不是一个具体的加密算法而是一类协议的设计思想证明者向验证者证明自己知道某个秘密或者某个命题为真但整个过程中除了“命题为真”这个结论验证者拿不到任何额外信息。它适合谁适合那些手里有敏感数据、又必须向外界自证清白的人也适合做隐私身份、链下计算验证、数据协作的开发者。下面我按自己实际做项目的顺序从原理拆到代码再聊选型和踩过的坑。1. 先把“零知识”拆成三件事证明、验证、不泄露1.1 一个环形洞穴的思想实验我习惯用环形洞穴来解释零知识证明因为它把“交互”和“随机挑战”这两个关键点都装进去了。想象一个环形的洞穴入口只有一个洞穴深处有一扇门门需要一句咒语才能打开。证明者说自己知道咒语但不想把咒语告诉验证者。怎么办让证明者先走进洞穴随机选择从左通道或右通道走到门边验证者在洞口外看不到证明者选了哪条路然后随机喊“从左出来”或“从右出来”。如果证明者真知道咒语他总能打开门从被喊的那一侧出来如果他不知道每轮只有一半概率蒙对。重复二十轮蒙对的概率就是百万分之一。这个实验里验证者得到的全部信息是“每一轮证明者都从指定侧出来了”他并不知道证明者一开始走的是哪边也不知道咒语是什么。零知识证明的“零知识”就体现在这里验证者被说服了但没有拿到任何可用于复现或泄露秘密的信息。生活里类似的场景很多比如你证明自己会开车不需要把驾驶技巧的全部细节讲出来上车跑一圈就行但零知识证明更严格它要求连“你具体怎么开”的过程都不留痕迹。1.2 完备性、可靠性、零知识性到底在说什么任何零知识证明协议至少要满足三个性质我在评审方案时也是按这三条逐项问。第一是完备性如果命题是真的证明者老老实实按协议走验证者必须接受。这条最基础相当于“真话一定能说通”。第二是可靠性如果命题是假的证明者不管怎么作弊验证者接受的概率必须低到可以忽略。注意这里的“低”不是“零”而是可忽略比如 2 的负 128 次方。第三是零知识性验证者从交互记录里学不到除命题为真之外的任何东西。形式上存在一个模拟器它不需要知道秘密也能生成与真实交互不可区分的记录。很多方案还会加一条知识可靠性证明者不仅能让验证者相信“存在某个解”而且证明者自己确实“知道”那个解。这条在简洁非交互证明里经常被强调因为有些协议只能证明“命题为真”不能证明“证明者掌握 witness”。我在做身份凭证类需求时特别看重这条否则可能出现“证明是别人代做的但凭证被转手”的问题。1.3 它和普通加密、签名的边界在哪里刚接触的人容易把零知识证明和加密混在一起。加密解决的是“内容别被看见”签名解决的是“内容没被篡改、来源可确认”零知识证明解决的是“我让你相信一个结论但不告诉你支撑结论的数据”。举个例子加密像把信锁进保险箱签名像在信封上盖火漆印零知识证明像你站在门口告诉保安“我有进入权限”保安信了但你的门禁卡号、权限等级、有效期他都没看到。这个边界很重要因为零知识证明不是万能药。它不能替代加密因为证明过程本身可能泄露元信息它也不能替代访问控制因为验证者信的是数学关系不是你的身份系统。实际项目里零知识证明通常和承诺、签名、哈希一起用先用承诺把数据锁住再用证明说明承诺里的数据满足某个条件最后用签名绑定证明者的身份。单独上一个零知识证明库往往解决不了完整的业务问题。2. 从交互式协议到非交互证明核心机制怎么跑起来2.1 承诺、挑战、响应Sigma协议的三拍节奏最经典的一类零知识证明叫 Sigma 协议结构非常固定就三拍承诺、挑战、响应。以“证明自己知道离散对数”为例公开参数是一个循环群、生成元和公开值 y证明者知道 x使得 y 等于生成元的 x 次方。第一拍证明者随机选一个 r计算承诺 t把 t 发给验证者第二拍验证者随机选一个挑战 c发回去第三拍证明者计算响应 s把 s 发回去。验证者最后检查一个等式等式成立就接受。这三拍的精髓在于“顺序不能乱”。证明者先承诺相当于把答案锁进盒子再把盒子交出去验证者后给挑战相当于随机抽题证明者最后开盒相当于按题作答。如果证明者先知道题再锁盒他就可以针对题目编一个能过的答案协议就废了。我见过有人为了省一次网络往返把承诺和响应合并发送结果整个协议的安全性直接归零这个后面会细说。2.2 为什么必须“先承诺后挑战”先承诺后挑战本质上是让证明者没有机会根据挑战来调整承诺。假设顺序反过来验证者先给挑战 c证明者再给承诺 t 和响应 s。证明者完全可以随便选一个 s然后反推出一个 t让等式成立而他根本不需要知道秘密。这在数学上叫“模拟”但模拟出来的记录和真实记录不可区分验证者就无法分辨证明者是不是在作弊。我在代码 review 时有个习惯看到零知识证明的实现先看随机数生成和挑战生成的位置。如果挑战是在承诺之前生成的或者承诺和响应用了同一个随机数基本可以直接打回。这个顺序不是形式主义它是整个安全性证明的支点。你可以把它理解成考试先发卷子再答题和先知道答案再出卷子难度完全不是一回事。2.3 哈希挑战变换把对话压成一条证明交互式协议有个麻烦验证者必须在线而且每轮都要真随机。实际系统里我们更希望证明者生成一条证明贴到公告板上任何人随时可验证。这就需要把“验证者的随机挑战”替换成“哈希函数的输出”。做法是把公开参数、承诺、命题上下文拼成一个字节串做一次哈希把哈希结果当作挑战。这样挑战看起来是随机的但实际上由承诺和上下文唯一决定证明者没法预先操纵。这个变换叫哈希挑战变换也有人叫非交互化变换。它把多轮交互压成一条证明验证者只需要重算哈希、验一个等式。但这里有个坑哈希输入必须把所有相关上下文都绑进去包括命题、公开值、协议版本、甚至链 ID 或域名。少绑一个字段就可能出现跨场景重放。比如同一个证明在 A 业务里有效被搬到 B 业务里也能过因为挑战没绑定业务标识。我在实际项目里会要求把“证明用途”写进哈希输入这相当于给证明加了一个不可伪造的标签。2.4 模拟器视角零知识性不是“感觉安全”零知识性的严格定义是存在一个多项式时间的模拟器它不需要知道 witness就能生成一个与真实交互不可区分的记录。这句话听起来绕但它的实际意义是验证者即使把交互记录拿回去反复分析也找不到任何关于 witness 的信息因为模拟器在没有 witness 的情况下也能造出一样的记录。换句话说记录本身不携带 witness 信息。这个视角帮我排除过很多“伪零知识”方案。有些方案只是把敏感字段哈希了一下就宣称零知识但验证者可以通过穷举或关联分析还原出原始值这就不满足零知识性。判断方法很简单问一句“如果我是验证者我能不能用这条证明去推断 witness 的任何一位”如果答案是能那它就不是零知识证明只是某种承诺或脱敏。真正的零知识证明安全性不依赖于验证者“不去分析”而依赖于数学上的不可区分。3. 技术路线选型不是所有场景都适合上zk-SNARK3.1 四类主流方案的能力边界市面上的零知识证明方案很多名字也花哨但按能力边界大致可以分成四类。我在选型时不看宣传语只看四个指标证明大小、验证成本、是否需要可信设置、开发难度。下面这张表是我内部评审时常用的对照具体参数会随实现变化但量级关系基本稳定。方案类别证明大小验证成本可信设置开发难度典型适用场景基于配对与多项式承诺的简洁证明几百字节常数级很低毫秒级通常需要高需要电路设计链上验证、凭证系统透明设置的哈希型证明几十 KB 到几百 KB中等随规模增长不需要中高抗信任假设、长期可验证内积论证类方案几百字节到几 KB对数或线性不需要中范围证明、聚合证明多方计算生成证明较小低依赖参与方高特定隐私计算场景这张表里没有“最好”的方案只有“最合适”的方案。链上验证对证明大小和验证成本极度敏感哪怕证明生成慢一点也愿意选常数大小方案离线审计场景更看重长期可验证性不愿意引入一次性可信设置那就倾向透明设置移动端或嵌入式设备则要反过来证明生成不能太重宁可证明大一点。3.2 可信设置与非可信设置的取舍可信设置是很多人心里的疙瘩。简单说这类方案需要一次性生成公共参数生成过程中会产生一些“有毒废料”如果这些废料没被销毁持有者理论上可以伪造证明。实际项目里通常用多方计算仪式来分散风险只要有一个参与方诚实地销毁了自己的份额参数就是安全的。但“信任”这件事很难向普通用户解释所以我在面向公众的场景里会非常谨慎地使用“需要可信设置”这个说法。透明设置方案不需要这种一次性仪式它只依赖哈希函数公共参数可以由任何人用公开随机数生成。代价是证明更大、验证更贵。我的经验是如果验证方是链上合约且证明调用频繁可信设置带来的性能优势很难被替代如果验证方是审计机构或监管方且系统生命周期很长透明设置的“无信任假设”更省心。选型时不要只算技术账还要算沟通账。3.3 算术化把业务逻辑翻译成电路零知识证明本身只认数学关系不认业务语言。要把“用户余额大于零”“身份证有效期在范围内”“模型输出等于某值”变成证明中间必须做算术化。最常见的形式是 R1CS每个约束长这样A 向量点乘 witness乘以 B 向量点乘 witness等于 C 向量点乘 witness。加减乘都能表示比较和范围检查则要拆成比特。举个例子证明 x 在 0 到 100 之间。你不能直接写一个“x 100”的约束因为电路里没有原生的比较指令。常见做法是把 x 拆成若干比特约束每个比特只能是 0 或 1即 b 乘以 b 减 1 等于 0再用二进制权重把比特加回 x最后约束 x 小于 100。这个过程中比特位数、边界条件、符号处理都容易出错。我见过一个范围证明漏了最高位约束导致用户可以声称一个负数在范围内链上验证还通过了因为电路根本没检查符号。3.4 选型清单按证明规模、验证成本、开发周期排序我自己的选型顺序是这样的先问证明在哪验证。链上验证优先看证明大小和验证 gas服务端验证可以放宽到几十 KB移动端验证则要避免复杂配对运算。再问能不能接受可信设置。面向公众、生命周期长的系统优先透明设置内部系统、性能敏感的场景可以接受多方仪式。最后问团队熟悉什么。电路开发不是普通业务开发学习曲线很陡选一个团队能维护的方案比选一个纸面参数最漂亮的方案更重要。还有一条经验不要一上来就上通用电路方案。很多需求用 Sigma 协议、范围证明、哈希树成员证明就能解决代码量小、审计成本低、出错面窄。通用电路方案适合逻辑复杂、约束多的场景但开发周期可能是前者的五到十倍。先做威胁模型再看能不能用简单协议覆盖覆盖不了再上电路。4. 手写一个可验证的零知识证明Demo4.1 环境与参数准备下面这段代码只依赖 Python 标准库目的是把协议跑通不是生产实现。教学参数我故意选得很小模数 23子群阶 11生成元 4。小参数方便手算和打印但绝对不安全生产环境必须用大素数群或椭圆曲线群并且使用经过审计的库。我之所以坚持手写一遍是因为很多坑只有自己实现过才会真正记住比如随机数复用、挑战空间、哈希绑定。先准备参数和基础函数。secrets模块用于生成密码学安全的随机数不要用random。pow的三参数形式做模幂Python 自带的大整数运算足够演示。import hashlib import secrets # 教学参数禁止用于生产 P 23 # 模数 Q 11 # 子群阶 G 4 # 生成元 def keygen(): x secrets.randbelow(Q - 1) 1 y pow(G, x, P) return x, y这段代码里私钥 x 落在 1 到 Q-1 之间公钥 y 是生成元的 x 次方。这里有一个细节x 不能取 0否则公钥恒为 1证明就失去意义。实际系统里还要检查公钥是否在正确的子群内防止小群攻击。教学代码省略了这些检查但你在生产实现里必须补上。4.2 交互式离散对数证明交互式版本分三步。证明者先选随机 r算承诺 t验证者给挑战 c证明者算响应 s验证者验等式。代码很短但每一步都有讲究。def commit(): r secrets.randbelow(Q) t pow(G, r, P) return r, t def respond(r, x, c): return (r c * x) % Q def verify(y, t, c, s): left pow(G, s, P) right (t * pow(y, c, P)) % P return left right验证等式的原理是左边是生成元的 s 次方右边是承诺乘以公钥的 c 次方。把 s 展开右边等于生成元的 r 加 c 乘 x 次方正好等于左边。这个等式成立说明证明者知道 x因为只有知道 x 的人才能算出正确的 s。但验证者看到的只有 t、c、s他不知道 r也不知道 x所以没有泄露秘密。这里最容易犯的错是 r 复用。如果同一个 r 用在两个不同的挑战上攻击者拿两组响应一减就能解出 x。我在 code review 里看到过有人把 r 存在全局变量里“省性能”这是典型的省小钱赔大钱。每次证明都必须重新生成 r而且要用密码学安全的随机源。4.3 非交互版本与挑战绑定把交互式协议变成非交互核心是把验证者的随机挑战换成哈希输出。哈希输入要包含公开参数、公钥、承诺和命题上下文。我通常还会加一个域分隔字符串防止不同协议之间的哈希碰撞。def challenge(y, t, statement): data fzk-demo-v1:{P}:{G}:{y}:{t}:{statement}.encode() return int(hashlib.sha256(data).hexdigest(), 16) % Q def nizk_prove(x, y, statement): r secrets.randbelow(Q) t pow(G, r, P) c challenge(y, t, statement) s (r c * x) % Q return t, s def nizk_verify(y, statement, t, s): c challenge(y, t, statement) return pow(G, s, P) (t * pow(y, c, P)) % P这段代码里statement是证明用途比如“证明我是某个凭证的持有者”。把它绑进哈希意味着同一个证明换一个用途就失效攻击者不能把 A 场景的证明搬到 B 场景。这个细节在实际系统里非常关键我见过因为没绑上下文导致证明被跨系统重放的案例修复成本很高。还有一个坑挑战取模后的空间。教学参数 Q 只有 11挑战空间太小伪造成功率是十一分之一多试几次就能蒙混过关。生产环境挑战至少 128 位通常 256 位。不要因为哈希输出很长就直接用取模之后的空间才是实际安全边界。4.4 哈希树成员证明隐藏“我是谁”很多场景不需要通用电路只需要证明“我在这棵树的某个叶子里”但不想暴露具体是哪个叶子。哈希树成员证明就能做这件事。先把每个叶子做哈希再两两合并成父节点一直到根。证明者给出叶子、路径上的兄弟节点和方向验证者从叶子一路算到根看是否等于公开的根。def h(data: bytes) - bytes: return hashlib.sha256(data).digest() def build_tree(leaves): level [h(x) for x in leaves] tree [level] while len(level) 1: nxt [] for i in range(0, len(level), 2): left level[i] right level[i 1] if i 1 len(level) else left nxt.append(h(left right)) tree.append(nxt) level nxt return tree def merkle_path(tree, index): path [] for level in tree[:-1]: sibling index ^ 1 if sibling len(level): direction left if sibling index else right path.append((direction, level[sibling])) else: path.append((self, level[index])) index // 2 return path def verify_path(root, leaf, path): cur h(leaf) for direction, node in path: if direction left: cur h(node cur) elif direction right: cur h(cur node) else: cur h(cur cur) return cur root这段代码能证明成员关系但它本身不是零知识的因为路径会泄露叶子在树中的位置叶子内容也可能被猜到。要变成零知识通常要把叶子换成承诺或者用零知识电路重新包装成员证明。我在实际项目里会先问一句验证者到底需要知道“某个人在名单里”还是需要知道“名单里某个人满足条件”前者用承诺加路径就够后者往往要上电路。4.5 跑测试与性能观察测试时我关注三件事正确性、伪造成功率、耗时。正确性用固定向量跑通伪造成功率用随机响应去撞验证等式耗时用time.perf_counter粗略观察。教学参数下一次证明和验证都在微秒到毫秒级看不出真实性能。生产环境里瓶颈通常在模幂运算和电路规模而不是哈希。观察项教学参数表现生产环境关注点证明生成耗时微秒级电路规模、模幂次数、并行度验证耗时微秒级配对运算次数、哈希次数证明大小几十字节常数大小还是随规模增长伪造成功率约 1/Q挑战空间至少 128 位我建议你在本地至少跑三组正常证明、篡改承诺、篡改响应。正常证明必须过篡改承诺或响应必须挂。如果篡改后还能过说明验证等式写错了或者挑战没绑进承诺。这个自测习惯能提前发现大部分低级错误。5. 踩坑实录那些让证明“看起来能过”的陷阱5.1 随机数复用是头号杀手随机数复用是零知识证明里最致命的错误没有之一。还是用离散对数证明举例同一个 r 生成承诺 t面对两个不同挑战 c1 和 c2得到两个响应 s1 和 s2。攻击者把两个响应相减得到 s1 减 s2 等于 c1 减 c2 乘以 x直接解出私钥 x。整个过程不需要破解哈希也不需要攻破群只要拿到两条用同一随机数的证明就行。我在项目里见过两种复用一种是开发者图省事把 r 缓存在对象里多次证明复用另一种是随机数生成器 seeded 不当不同进程生成相同序列。修复方法很直接每次证明都调用密码学安全的随机源不要缓存不要用时间戳做种子。还有一条如果协议需要多轮每轮的随机数必须独立不能从上一轮推导。5.2 挑战空间太小等于没挑战挑战空间决定了伪造成功率。如果挑战只有 8 位伪造者随机猜一个响应有 256 分之一概率通过重复几百次就能蒙中。有人觉得“我可以多验几轮”但多轮验证会带来交互成本和状态管理问题不如直接把挑战空间做大。生产环境里挑战至少 128 位通常 256 位取模后的空间也要足够大。这里还有一个隐蔽问题哈希输出取模后的分布。如果模数不是 2 的幂取模会带来微小偏置严格来说需要用拒绝采样或大整数映射。教学代码为了简洁直接取模生产实现要用标准做法。安全参数不是拍脑袋定的128 位是底线256 位更稳妥。5.3 电路约束漏写证明对了业务错了通用电路方案最怕约束漏写。验证者看到证明通过了以为业务逻辑也对了其实电路只约束了一部分。比如证明“我知道一个数 x使得公开值等于生成元的 x 次方”但电路没约束 x 的范围攻击者可以拿 x 加上子群阶来构造另一个 witness证明照样过但业务语义变了。再比如范围证明漏了符号位负数也能通过“大于零”的检查。我现在的做法是电路写完后先做一轮“反例测试”。故意构造边界值、负数、超大数、零点看证明能不能过。能过就说明约束有漏洞。然后再做一轮“约束覆盖检查”把业务规则逐条映射到约束确保每条规则至少有一个约束对应。电路审计比普通代码审计更贵因为错误往往不是崩溃而是“静默通过”。5.4 常见问题速查表下面这张表是我自己整理的排查顺序遇到证明异常时从前往后查基本能覆盖八成问题。现象可能原因排查方法修复建议真证明通不过挑战没绑全上下文打印哈希输入逐字段比对把命题、公钥、承诺、版本号全绑进去假证明也能过验证等式写错或挑战空间太小用随机响应撞验证检查等式挑战至少 128 位同一证明可跨场景重放哈希没绑业务标识换一个 statement 重验加入域分隔和用途字段证明生成偶尔失败随机数越界或模运算错误检查 r 的范围和取模用密码学安全随机源范围对齐子群阶验证耗时波动大群运算未预计算压测不同输入规模预计算生成元幂表或换更高效曲线这张表不是万能的但它能帮你快速定位“是协议问题还是实现问题”。我的经验是先怀疑实现再怀疑协议。大部分线上问题不是数学被攻破而是随机数、绑定、范围这些工程细节出了错。6. 应用场景与影响范围它到底能落到哪里6.1 身份与凭证只回答“是/否”零知识证明在身份场景里最自然的用法是把“出示全部信息”变成“回答一个布尔问题”。比如验证者只想知道“这个人是否年满十八岁”证明者不需要给出出生日期只需要给出一个证明说明自己持有的凭证里的出生日期满足某个范围。再比如会员资格、权限等级、白名单成员都可以做成“只回答是/否”的凭证。这类系统的关键不是证明本身而是凭证的签发和撤销。凭证需要被签名证明里要绑定签发者公钥和撤销状态否则攻击者可以拿过期凭证生成证明。我在设计这类方案时会把“凭证有效期”和“撤销根”作为公开输入绑进电路验证者每次验证时检查最新状态。这样证明仍然零知识但业务上不会出现过期凭证蒙混过关。6.2 扩容与批量验证把计算搬到链下另一大类场景是扩容。思路很直接把大量计算搬到链下生成一个证明链上只验证这个证明。验证成本从“随交易数量线性增长”变成“常数级”这就是很多人说的批量验证。某类公链项目会用这种方式把成百上千笔状态转换压成一个证明链上合约只验一次。但这里有个现实问题证明生成很贵。电路越大生成时间越长内存占用越高。我见过一个团队把几百兆的电路直接丢到普通服务器上跑结果内存爆了。实际项目里要做电路拆分、并行生成、证明聚合还要考虑证明生成失败时的回退策略。扩容不是“上个零知识证明就完了”它是一整套工程体系。6.3 数据协作与模型推理数据协作场景里零知识证明可以用来证明“我的数据满足某个统计性质”而不交出原始数据。比如证明一组数据的平均值在某个区间或者证明某个模型在私有数据集上的输出等于公开结果。这类应用听起来很美好但边界要划清楚零知识证明不加密数据本身它只保证证明过程不泄露 witness。如果数据在传输或存储环节没加密证明再漂亮也没用。模型推理场景还要注意“证明的是什么”。你可以证明“输出等于模型对输入的计算结果”但很难证明“模型是某个公开模型”因为模型权重本身可能也是私有的。实际做法通常是把模型哈希或承诺作为公开输入证明者证明自己用的模型承诺匹配再证明计算结果正确。这套组合拳的复杂度不低适合对信任要求极高的场景不适合普通业务。6.4 边界延迟、证明大小与开发成本零知识证明不是免费的。证明生成延迟可能从几百毫秒到几分钟不等证明大小从几百字节到几百 KB电路开发周期可能是普通业务的数倍。我在评估一个需求是否值得上零知识证明时会问三个问题验证方是否真的不需要原始数据信任缺失是否严重到必须用数学证明业务能否接受证明生成的延迟和成本如果三个答案都是“是”再考虑技术选型。如果只是“不想让日志里出现明文”那加密和脱敏更合适如果只是“想让对方相信我们没作弊”那审计和签名可能更便宜。零知识证明的价值在于“在不泄露秘密的前提下建立信任”它的成本也在这里。想清楚这一点就不会为了用而用。我个人在实际项目里的体会是先做威胁模型再选协议最后才写电路。威胁模型里把“证明者可能作弊”“验证者可能重放”“凭证可能过期”这些场景列清楚比急着调库更重要。很多坑不是数学问题而是边界没想明白。