← 返回博客2026-07-06

高级加密发牌让服务器看不见什么?从源码讲 Mental Poker

Mental Poker 实现想解决“服务端自己能否看到未公开底牌”,但 ADR-090 已把客户路径安全封闭:当前 H5 不可开启,服务端创建与连接均 fail closed。本文同时解释设计和当前发布边界。

先把它和普通发牌分开

BluffKing 里有两类容易被混在一起的公平性机制:默认的可验证发牌,以及现已开放(ADR-101,“可信单一运营者”范围)的高级加密发牌

2026-07-25 更新(ADR-101):高级加密发牌现已开放:这一手服务器不持有你未公开的底牌明文。它在一个明确的“可信单一运营者”信任模型下上线——交付真实且密码学成立的底牌保密,但不是对抗运营者的无信任结算。必备披露:只要有玩家掉线,整桌会公开降级为普通(服务器可见)发牌;这份保密只在开启该模式的这一手成立;仅练习币。默认牌桌仍是服务器可见的 commit-reveal 发牌。

默认可验证发牌回答的是:服务端在一手牌开始后,能不能悄悄换 seed、换 deck、按剧情补牌。它的边界是 commit-reveal:发牌前承诺随机材料,发牌后公开材料,用户可以复算牌序。

开放的高级加密实现回答的是更强的设计问题:服务端自己在牌局进行中能不能看到未公开底牌。这条路径在代码里叫 engine_blind,它建立在 Mental Poker 真实密码学核心之上:服务器不再拿一副明文 deck 推进整手牌,而是协调多个客户端共同完成加密洗牌、发牌、公共牌打开和摊牌 reveal。(另有一个尽力而为的 Mental Poker transcript 模式跑在普通服务端可见牌桌上,它不是 server-blind。)

一句话:普通模式证明「牌没有被事后改」;高级加密发牌把边界推进到「未公开底牌不进入服务端明文状态」。

所以这篇不写「更安全」这种空话,而是按源码路径拆:哪条 HTTP 请求会把房间标记成 engine_blind、WebSocket attach 又会怎么把这个标记带进 session task,哪一个分支避开普通明文 deck,Mental Poker 协调器到底让服务端看到什么、看不到什么,最后落库、复盘和教练又怎么保持这个边界。

整篇文章只回答一个工程问题:代码路径里有没有一个地方,让服务端在高级加密手牌中拿到 folded 或摊牌前未公开的底牌明文。

开源边界:公开镜像 github.com/CisaSettle/bluffking 可复核 Mental Poker 加密核心、扑克规则引擎和相关可验证组件;生产房间编排和 H5 接入不是公开镜像承诺。下面只在解释流程时点到必要路径,不把源码清单当成前置阅读。

今天谁能开启

全真人、无 AI 的房间可由房主开启。资格边界不变:至少 2 名真人、无任何 bot(ADR-041/068)。服务端 bot 必须知道自己座位的底牌,因此 AI 房仍不符合条件。默认牌桌仍走服务器可见的 commit-reveal 发牌,不受影响。开启后,这份底牌保密只覆盖本模式的这一手;任一玩家掉线,整桌会当众降级为普通(服务器可见)发牌。

场景是否高级加密原因
默认普通牌局服务端可见牌,用于发牌、复盘、教练;可用 commit-reveal 复核未被改牌。
AI / bot 牌局服务端运行 bot,需要读取 bot 自己的底牌。
全真人、无 AI是——房主可开启在可信单一运营者范围内上线;这一手服务器不持有你未公开的底牌明文(练习币)。
engine-blind 房间的连接与路由创建 → WebSocket attach → 协调者,一路按 engine-blind 路由,不再被拦截。

开放后:创建 → attach → 协调者,一路走 engine-blind

高级加密发牌不是浏览器可以偷偷打开的本地开关,但它现在已在服务端开放。一个全真人、无 AI 的房间由房主选择开启后,三个环节会一路把它按 engine-blind 路由:

  1. HTTP 创建:房主的 POST /api/lobby/create-roomengine_blind: truehandlers/lobby.rs 校验资格(全真人、无 AI)后落一个 engine_blind 房间——不再返回 503
  2. WebSocket attach:GET /ws?session_id=... 连接时把这个房间类别带进 session task,并 spawn 出协调这手牌的实时协议,而不是把连接顶掉。
  3. 运行时路由:常量 ENGINE_BLIND_RELEASE_AVAILABLE = true(无 env 覆盖),session task 因此走 engine_blind_routes_blind_coordinatorrun_engine_blind_live_handrun_mp_deal_blind 的盲发牌路径。

这里的关键是:HTTP 负责「创建了一个什么样的房间」;WebSocket 负责「这手牌的实时协议怎么跑」。真正推进一手牌的仍然是后端 session task,而不是浏览器自己决定发牌——这一点在开放前后都没变。

engine-blind 的 session task 不走普通明文牌堆

普通牌局会走标准的服务端发牌路径:服务端生成或派生 deck,engine 用明文牌堆推进,最后持久化完整历史。

engine-blind 实现不进入这条路径。session task 看到 engine_blind=true 时,会把整手牌委托给 run_engine_blind_live_hand。这条协调器内部再调用 run_mp_deal_blind 完成 DKG、加密牌堆、可验证 re-encryption shuffle 和最终密文 deck commit。

engine 侧也不是拿一个明文 Deck 开局,而是使用 blind hand 结构:底牌位置是 opaque,公共牌在每条街通过已验证的 threshold open 注入,摊牌时才把合法 reveal 的底牌注入。这个边界让「服务端拿到完整明文 deck 后再假装不看」这类实现没有空间。

engine-blind 实现设计为隐藏什么

engine-blind 实现的核心不是「把牌藏在前端变量里」,而是密码学上的访问结构。

  • 每个真人客户端参与 DKG,持有自己的 secret share。
  • 牌堆是 ElGamal 密文,经过各方可验证 re-encryption shuffle。
  • 服务端是协调者,转发消息、校验证明、维护 session 状态,但不持有任一玩家的 secret share。
  • 打开一张牌需要 n-of-n 的解密贡献。公共牌本来就是公开信息,所以每条街按规则打开给所有人。
  • 底牌只在两个地方打开:发到本人客户端时由本人本地打开;摊牌时由仍有资格争夺底池的玩家按规则 reveal。
玩家客户端 份额 x_i 玩家客户端 份额 x_i 玩家客户端 份额 x_i 服务端协调者 无份额;转发消息、校验证明 ElGamal 密文牌堆 底牌 x₂ x₃ x₁ 仅 owner 本地合并 公共牌 x₁ x₂ x₃ P₁ P₂ P₃ 向所有人打开 弃牌未摊 永不揭示

engine-blind 设计:每类牌的解密份额由谁合并。协调者只转发份额,自己不持有份额,也从不合并底牌索引。

因此,服务端可以看到公共牌、行动、筹码变化、摊牌时已经公开的底牌;但对 folded 或尚未摊牌公开的底牌,它持有密文、可验证证明以及转发中的非持有者解密份额——但从不持有 owner 自己的份额,也没有任何合并路径,因此无法还原明文。

开启后,为什么要关闭 AI、教练、实时胜率和完整历史

这不是产品克制,而是同一个安全边界的必然代价。

功能高级加密发牌里的处理原因
AI / bot关闭服务端 bot 要读自己的底牌,和 server-blind 冲突。
教练 / solver 输入关闭服务端没有未公开底牌明文,不能把它送进教练。
实时胜率 / hand strength关闭或不由服务端计算这些计算依赖 hero 明文底牌。
完整明文历史不保存folded / 未 reveal 的底牌本来就不应该出现在服务端历史里。

对应到持久化,hands.engine_blind=true 是权威标记;dealing_provider 写入 mental_poker_engine_blinddeck_seed 不写明文可重放 seed;seats_log 里的底牌用空哨兵;每个玩家的 row-level hole_cards 只会来自合法 showdown reveal。coach.rs 会在读牌前先检查这个标记并直接拒绝教练分析。当前还有一个限制:engine-blind 的签名 transcript 不随手牌落库,所以这条记录只保住了保密性,还不是可独立重放的证明工件。

掉线、超时,以及为什么这不是“对抗运营者”的无信任结算

这套实现需要每个参与方在线配合。n-of-n 的好处是保密边界强;代价是 liveness 更脆:缺少必须的 share,某些公共牌或摊牌 reveal 就无法完成。掉线时的降级路径是本模式的稳定性设计,不是缺陷。

这里最重要的原则是:绝不静默降级。对可归类的诚实掉线——玩家离线——本模式会按规则作废当前手牌并退回筹码,然后服务端广播 mp_mode_downgraded,界面显示「本手起已切换为普通模式 · 服务器可见牌」。只有在这个持久提示发出之后,下一手才会走普通可验证发牌。检测到的恶意行为或密码学错误不会给服务端打开「改回可见牌继续发」的捷径,而是走同一条 void / teardown 路径。所以,降级不是偷偷换牌,也不是把当前手牌改造成普通手牌;它是公开且持续生效的模式切换,并且从下一手开始才生效。

真正未解的是选择性投递——这正是为什么本模式不是“对抗运营者”的无信任结算(一份公开披露的残余,ADR-101 §8):所有动作都经过唯一协调者,同一协调者的 ACK 无法区分恶意扣留 reveal 的玩家和被协调者审查的诚实玩家。既然无法区分,本 ADR 不改结算代码——凡是无法确证为恶意隐瞒的缺失,一律作废退还筹码(保护诚实玩家),代价是一个有界且软的退款残余(每桌至多一次、练习币、reveal 前通常不知作废是否有利)。要彻底消除这道歧义,得等独立认证的动作投递或结果前 reveal 托管——那是 ADR-090 保留的 v2 出口,与本次开放无关。

服务器能看到哪些牌?

当前产品答案:默认牌桌仍由服务端持有牌面(用于发牌、复盘、Coach);但在开启高级加密发牌的这一手,服务器不持有你未公开的底牌明文——folded 且从未摊牌的手牌也不进入服务端明文。普通玩家和未授权 API 在任何模式下都收不到别人的未公开底牌。

必备披露:这份保密只在开启该模式的这一手成立;只要有玩家掉线,整桌会公开降级为普通(服务器可见)发牌;底牌保密依赖运营者运行的是已审计的开源版本——远端无法验证正在运行的二进制;仅练习币。它不是“对抗运营者”的无信任结算:被攻破或作恶的运营者仍可能审查交付或操纵结算(已披露残余,ADR-101 §8)。

边界:这不是第三方安全审计,也不是「绝对安全」承诺。

可复用的规则:先定访问结构,再让它决定功能清单和故障行为——并对每个明文入口(创建、连接、路由、落库、分析)都显式路由,掉线时公开降级,绝不静默。