← 返回博客

· · 更新

德州扑克:零知识证明

德州默认发牌现已上线零知识证明验证:用 52 个不透明信封,讲清加密牌、玩家随机数与两类证明如何配合,以及如何在本机核对发牌,让未亮出的牌继续保密。

一手德州结束,你想确认发牌没有被动过手脚,却不想为了验证,把所有人的弃牌也翻出来。这两个愿望,能不能同时满足?

“加密牌 + 可验证洗牌”的思路是:把牌藏起来,把核对所需的证据交出来。玩家可以检查牌组完整、发牌顺序符合约定、亮出的牌确实来自相应位置,而不必拿到重建整副明文牌的秘密。

2026 年 10 月 31 日更新:德州默认发牌的 v2 可验证发牌已上线,采用加密牌组、可验证洗牌和解密证明。本文按当前实现更新,介绍玩家如何核对发牌,同时让未亮出的牌继续保密。独立的高级加密发牌模式不受这次替换影响。

现在怎么验证:一手默认发牌的德州结束后,本手拿到底牌的玩家可以从手牌历史的“公平验证”入口打开核对,也可以进入公平性验证页面,选择最近手牌或输入手牌 ID。请登录参与该手的账号,并尽量使用打这手牌时的同一浏览器和设备;只有证明与完整的本机见证都通过,才会显示“完全通过(本机亲历)”。

为什么不直接公开种子,让大家自己算?

随机种子可以理解为“重放这次洗牌所需的起点”。旧版默认发牌的种子复算机制,会在一手结束后向本手参与者公开服务器种子,让他们重算整副牌,检查发牌是否与开局前的承诺一致。这很直观,但代价也很直接:整副牌能被重算,没亮出的弃牌也会被还原。

现在的默认发牌不再公开服务器主种子、解密密钥和内部牌序种子。它改用两类证明:一类说明“这里确实是一副完整的标准牌”,另一类说明“这个位置打开后确实是这张牌”。公开的玩家随机数仍会保留,服务器用来打开整副牌的秘密则留在服务器。

想象桌上摆着 52 个不透明信封

服务器把标准 52 张牌分别放进不透明信封,秘密打乱,排成一列,再为这一列信封留下一个不能随意更换的“指纹”。在实际协议里,信封是密文,指纹是绑定公钥和全部 52 个密文的哈希承诺。

服务器知道信封里装着什么,玩家不知道。随后,玩家贡献的随机数算出一个公开顺序,例如“第 17 个信封去第一个发牌位置”。大家都能核对信封怎样移动,却看不到未打开的信封里是什么。

  1. 先封好并锁定:服务器在玩家提交随机数之前公布加密牌组的承诺。
  2. 再决定去向:玩家随机数与本手标识、牌组承诺一起,决定密文的公开重排顺序。
  3. 按规则打开:该发给你的底牌交给你;公共牌、摊牌和主动亮牌按各自规则公开。
  4. 事后核对:点击验证时,浏览器检查牌组、发牌位置和你有权看到的牌,并与本机留下的记录比对。

只让服务器排信封,服务器就有机会预先安排谁拿好牌。只公开洗一副明牌,所有人又都能算出彼此的底牌。把保密的牌组与玩家参与决定的分配顺序结合起来,才能同时处理这两个问题。

这里有一个前提:至少有一位诚实玩家贡献了服务器事先无法预测的随机数,并且没有人扣留已经承诺的随机数。

玩家的随机数,为什么要分两次交?

如果每个人直接报一个数,最后报数的人就可能先看别人的数,再挑选对自己有利的答案。当前实现沿用两轮交换:先交“答案的指纹”,等大家锁定答案后,再交答案本身。

每手开始,支持该协议的真人玩家客户端,用浏览器的 crypto.getRandomValues 生成 32 字节随机数。这是 Web Crypto 标准提供的密码学随机数接口,详见 W3C 的接口定义。客户端先提交这个随机数的哈希承诺,再按协议公开随机数原文。

浏览器收到牌组承诺后,先提交绑定本手标识和座位号的随机数哈希。第一轮结束,参与名单和各人的哈希固定下来;第二轮才公开随机数原文,原文必须与此前的哈希对得上。所有人的随机数按座位顺序汇总,参与算出最终的公开重排。

有人可能交过哈希,却拒绝交出原文。这叫“扣留”。它不等于可以悄悄换一个数,但可能破坏公平所需的前提,因此必须记录、公开,并影响验证结果。机器人不贡献这类玩家随机数,也不能充当那位诚实参与者。

对你自己而言,关键检查是:你的随机数确实被纳入了计算,而且没有人扣留。只有看到这些证据,才能把“我也参与决定了发牌顺序”落实到这一手。

两种证明,分别回答什么问题?

证明能核对什么不会因此公开什么
可验证洗牌证明输出的 52 个密文,是标准 52 张牌经过重排和重新加密的结果,没有多牌、少牌或重复牌。秘密洗牌的对应关系,以及未亮出的牌面。
解密证明指定位置的密文,在承诺所绑定的公钥下,确实对应所展示的那张牌。解密密钥,以及其他位置的牌。

“零知识”的直觉是:证明一件事成立,不必交出证明者掌握的全部秘密。这里既要证明“这是一副正常的牌”,也要证明“刚才亮出的红桃 A 确实来自应该打开的位置”。

核对器不能照单全收服务器给的材料。它必须自己重建标准起始牌组,检查承诺中的公钥和密文,重新计算玩家随机数决定的顺序,并从冻结的座位顺序推出每张牌的位置。否则,一份本来有效的证明,也可能被配到错误的位置上。

当前实现复用仓库已有的可验证重加密洗牌和 DLEQ 解密证明代码,并由浏览器中的 WebAssembly 核对。

证明能对上,为什么还要本机留一份记录?

验证分两步:先检查服务器提供的证明,再把证明中的承诺、玩家随机数、发牌位置和牌面,与浏览器在这手牌进行时保存的记录逐项比对。

所以浏览器会留下自己的见证:发牌前收到的承诺、自己贡献的随机数、固定的座位顺序、是否有人扣留,以及本机第一次看到的每张牌和位置。核对时,服务器提供的证明必须与这些本机记录吻合,不能拿后来的记录覆盖先前看到的事实。

浏览器在本机存储中保留最近 500 手见证记录;存储不可用或已满时,记录可能无法保存。换设备、清理浏览器存储、中途断线漏消息,或者超出保留范围,都可能让本机证据不完整。因此验证结果明确分层:

  • 完全通过(本机亲历):证明有效;本机记录完整且一致;承诺在发牌前收到;各玩家随机数与各自承诺匹配;自己的随机数被纳入;无人扣留;本机见过的每张牌,都有正确位置、正确牌面的解密证明;本机检查没有失败。
  • 服务器记录有效,本机未完整亲历:证明有效,但本机缺少完整见证,只能确认所提供记录在协议内的一致性。
  • 失败或无法验证:证明无效、拿不到证明或本机已有失败记录,都不能显示为通过。不能用一份后来有效的证明抹掉本机已经发现的矛盾。

哪些牌可以核对,哪些牌继续保密?

当前验证覆盖公共牌、摊牌、主动亮出的牌,包括只亮一张;自己的底牌也可核对,但底牌解密证明只交给本人。别人的未亮弃牌继续保持密文,不会为了让你验证而自动翻开。

“追兔”展示的是提前结束后看看后面本来会发什么,它不影响输赢,当前实现也未将这些牌持久保存,因此不在核对范围内,验证页会明确提示。

更早的旧版本手牌也不能事后补成新协议:旧版默认发牌的手牌会标注“旧版本,不支持验证”,验证接口不再返回它们的种子。本文只讲已上线的德州默认发牌 v2;炸金花不在本文范围内,现有高级加密发牌模式不受这次替换影响。

每手都生成证明,会不会拖慢游戏?

当前实现按需生成证明。开局时只加密、洗牌和公布承诺;有人点击验证时,服务器才重建那一手的加密牌组,并生成洗牌证明和该用户有权查看的解密证明。浏览器随后在本地核对。

主种子可以确定性地重建同一副密文牌组,但生成证明使用的随机数必须另取新鲜的操作系统随机数,不能重复派生使用。每次证明的字节可以不同,所证明的牌组和牌面仍应相同。

证明计算在服务器的阻塞线程池中执行,验证接口按账号限制为每分钟 30 次请求。

服务器保存什么,验证时提供什么?

默认发牌由服务器持有每手的主种子,并据此重建密钥和加密牌组。玩家验证时,接口提供牌组承诺、玩家随机数、洗牌证明,以及该玩家有权查看的牌的解密证明;不提供服务器主种子和解密密钥。

服务器随手牌记录保存主种子,供以后生成证明;浏览器保存最近 500 手本机见证,用来核对这些证明与实际收到的牌局消息是否一致。

这套已上线的功能,让玩家核对看到的牌是否来自开局锁定的完整牌组、分配是否采用了约定的玩家随机数;验证过程只打开有权查看的牌,其余牌保持加密。与现有两种发牌模式的区别,可继续阅读德扑发牌公平与隐私和高级加密发牌;这些旧文描述的是各自注明版本的机制。