4.1 承诺方案:封存与揭示的两幕剧 本节摘要:承诺方案(Commitment Scheme)是零知识证明的通用容器:先把值封进一个"藏不住看也改不动"的盒子里,事后再开箱验证。本节讲清绑定的两种打开方式(哈希承诺与 Pedersen 承诺)、两条性质(绑定性、隐藏性)的失效条件,并用暗标拍卖场景配可运行演算。承接第 3 章流水线视角,通往 4.2 的知识论证。 两幕剧的剧本结构 想象一场暗标拍卖:各家把报价写进标书、装入火漆封缄的匣子当众上交,此为第一幕;开标时刻当众拆匣、报出各家数字、比出赢家,此为第二幕。这套流程能成立,全靠火漆与匣子完成两项约束——拆封前没人能偷看数字(隐藏性),上交后也没人能换数字(绑定性)。
本节摘要:承诺方案(Commitment Scheme)是零知识证明的通用容器:先把值封进一个"藏不住看也改不动"的盒子里,事后再开箱验证。本节讲清绑定的两种打开方式(哈希承诺与 Pedersen 承诺)、两条性质(绑定性、隐藏性)的失效条件,并用暗标拍卖场景配可运行演算。承接第 3 章流水线视角,通往 4.2 的知识论证。
想象一场暗标拍卖:各家把报价写进标书、装入火漆封缄的匣子当众上交,此为第一幕;开标时刻当众拆匣、报出各家数字、比出赢家,此为第二幕。这套流程能成立,全靠火漆与匣子完成两项约束——拆封前没人能偷看数字(隐藏性),上交后也没人能换数字(绑定性)。承诺方案就是把这对约束数学化的原语:第一幕计算承诺值 C = commit(m, r) 并公开,其中 r 是持有者自选的随机数;第二幕公开 m 与 r,验证者重算 C 核对无误。
两条性质对应两种作弊,各有专门的攻法。绑定性防"翻供":持有者不能找到两个不同的 (m, r) 对应同一个 C,否则开标时想报哪个报哪个;隐藏性防"偷看":从 C 反推不出 m 的任何信息,包括"两个候选值哪个更可能"这种程度的信息都不行。注意两幕之间的时间顺序是安全性的一部分——承诺一旦公开即生效,"我先看看别人再决定"的空间必须为零。

工程上最常用的承诺有两款,性格互补。哈希承诺最省事:C = H(r, m)。绑定性由抗碰撞性直接给出——找到两个 (r, m) 映射到同一 C 就是构造哈希碰撞;隐藏性靠 r 的随机性——没有盐值时攻击者可以拿候选值字典逐一试哈希,所以 r 必须足够长且随机。Pedersen 承诺走群运算:C = g^m · h^r mod p,其中 g、h 是互不知离散对数关系的两个生成元。它的妙处在强度互换:随机数 r 均匀分布时,C 在群中均匀分布,完美隐藏(连算力无限的对手都偷看不到);绑定性则依赖离散对数假设(计算性绑定)。注意这组强度是互补分布的——完美隐藏与完美绑定被证明不可兼得,选择哪款承诺本质是在选"哪一侧要更强"。
跑一段演算把两款承诺都过一遍手:
# 两款承诺的玩具演示:哈希承诺与 Pedersen 承诺 import hashlib, secrets # —— 哈希承诺:暗标拍卖 —— def hash_commit(bid: int): r = secrets.token_bytes(16) # 盐:绑死随机性 C = hashlib.sha256(r + bid.to_bytes(8, "big")).hexdigest() return C, r C_a, r_a = hash_commit(700) # 甲出价 700 C_b, r_b = hash_commit(700) # 乙出价同样的 700 print("同价不同承诺:", C_a[:16], C_b[:16], "互不相等:", C_a != C_b) # True # 揭示:验证者用 (r, m) 重算哈希核对 print("揭示核对:", hashlib.sha256(r_a + (700).to_bytes(8, "big")).hexdigest() == C_a) # —— Pedersen 承诺:完美隐藏的手感 —— p = 10007 # 玩具素数域 g, h = 5, 17 # 两个生成元(真实协议需保证 dlog 关系未知) m1, m2 = 123, 456 r1 = secrets.randbelow(p) r2 = secrets.randbelow(p) C1 = pow(g, m1, p) * pow(h, r1, p) % p C2 = pow(g, m2, p) * pow(h, r2, p) % p # 完美隐藏:对任何两个消息,总存在 r 使承诺值恰好相等 # 反过来找:已知 C1 属于 m1,问 C1 也可能是 m2 的承诺吗? # 解 r' 满足 g^m2 h^r' = C1,即 r' = dlog_h(C1 / g^m2) —— 离散对数难题挡路 print(" Pedersen 承诺 C1 =", C1, "(单看承诺值猜不出 123 还是 456)") # 加法同态的演示:两个承诺的乘积 = 和的承诺 m3, r3 = 7, secrets.randbelow(p) C3 = pow(g, m3, p) * pow(h, r3, p) % p C_sum = C1 * C3 % p # 承诺相乘 manual = pow(g, m1 + m3, p) * pow(h, (r1 + r3) % p, p) % p # 直接承诺和值 print("同态成立:", C_sum == manual) # True —— 门限解密与匿名凭据的地基
最后一段同态演示值得圈出来:Pedersen 承诺满足"承诺之积等于和之承诺",这让验证方能在看不见数值的前提下对数值做代数运算与断言——第 6 章隐私交易里的金额平衡性检查、门限签名里的份额聚合,用的都是这条性质。
承诺的两种失效各有经典案例。翻供多死于随机数管理:盐太短被枚举、随机数复用导致两条承诺的相关性暴露、或把"承诺前可试错"的口子留在协议里(攻击者本地海选合适的 r 再公开承诺)。偷看多死于"承诺空间太小":对有限候选集(比如"同意/不同意"两个值)做哈希承诺而不加盐,攻击者直接把两个候选各哈希一遍对照即可。审查一份协议时,把承诺的所有输入列出来问两遍:随机部分够不够随机?候选空间之外的输入有没有被候选化?
另外留意承诺与哈希的常见混淆:哈希是"摘要"——公开内容本身的指纹;承诺是"容器"——公开的是混合了随机性的密闭值。把哈希当承诺用而忘了盐,是初学者代码里最高频的安全翻车点。
承诺解决了"锁住",下一节解决"验人":验证者接受的到底是"陈述为真",还是"证明者真的掌握背后知识"?两者的差别比看起来大。
**问:承诺与加密有本质区别吗?**有,而且区别是方向性的。加密是"带钥匙的隐藏"——持有解密钥的人能看到全部内容;承诺是"结构性的隐藏"——任何人都看不到内容,持有者自己也只能通过揭示阶段整体公开。需要一个可以部分解密、部分保留的容器时,两者才开始组合使用(如带查看键的隐私方案,见 6.2)。
**问:承诺可以叠加吗?**可以。对承诺值再做承诺、或把多个承诺通过同态性合并成一个,都是协议设计的常规动作——Merkle 树本质就是对哈希承诺的层层叠加。判断叠加是否安全,看每层承诺的隐藏与绑定性质是否被下一层继承。
**问:随机数 r 泄露了会怎样?**哈希承诺下,r 泄露等于内容可被字典攻击枚举(若候选集小);Pedersen 承诺下,r 泄露直接让隐藏性归零——承诺值与消息的绑定关系全部裸露。所以 r 的生成与保管纪律和秘密本身同级,备份策略里要一并考虑。
读协议文档的一个实用技巧:把其中每一个"锁住/封存/绑定"的动作都翻译成"这是某某承诺的某某性质"。看到"用户先对余额做承诺"—— Pedersen 或哈希承诺的隐藏性;看到"防作恶方中途换答案"—— 绑定性;看到"两份承诺相乘等于和的承诺"—— Pedersen 的同态性;看到"对多项式取一个承诺、以后任点开箱"—— 4.3 节的多项式承诺。翻译完一份协议,你会发现它不过是承诺性质清单的排列组合,陌生感随之消散。这个技巧反过来也是审计思路:文档里任何一个"锁住"动作若说不出用的是哪种承诺、靠哪条性质,那句话就是待补的洞。