独立验证 · Verification

独立验证指南

三件事你都能自己验,不用相信我们:① 线上跑的游戏,就是这份公开源码(重新编译比对);② 每手牌结束自动跑 4 灯公平校验;③ 任何一手都能下载牌局记录、在本机重放复验。

研究项目声明:Fair Poker 是非营利、纯兴趣的开源学术研究项目,研究德州扑克如何用密码学实现可验证公平;它不是对公众运营的游戏平台,不提供任何游戏服务。在线演示向公众开放,用于协议自测与体验;欢迎 下载全部源码 在自己的电脑上搭建研究——我们提供思路与代码,不提供服务。

Verification

Independent verification guide

Three things you can verify yourself, no trust required: ① the live game is built from this public source (rebuild & compare); ② every hand auto-runs a four-light fairness audit; ③ any hand can be downloaded and replayed locally.

Research project notice: Fair Poker is a non-profit, hobbyist open-source academic research project on making Texas Hold’em verifiably fair with cryptography. It is not a game platform operated for the public and provides no gaming service; the live demo is open to the public for protocol self-testing. Download the full source and run your own instance — we share ideas and code, not a service.

发布事实Release facts

Official domain
https://fairpoker.app
Game client CID
Read from canonical release JSON
Source package CID
Read from canonical release JSON
Source package URL
Read from canonical release JSON
Source fingerprint
Read from canonical release JSON
Archive SHA256
Read from canonical release JSON

官方客户端开源了——但怎么证明「线上 IPFS 上跑的」就是这份源码?The client is open source — but how do you prove the code running on IPFS is this exact source?

先说破一个最常见的误会:上面「发布事实」里有两个不同的 CID,它们本来就不一样、也永远不该相等——别拿这两个互相比。

  • 牌局客户端 CID(Game client CID)=编译好、能直接玩的成品(好比烤好的蛋糕)。
  • 核心源码包 CID(Source package CID)=给人阅读、审查的源代码(好比面粉和鸡蛋)。

面粉的编号和蛋糕的编号永远不会一样,这很正常。证明「线上=源码」靠的是各自对上两次

  1. 比对 ①:你下载的源码包算出的 CID = 公布的 Source package CID → 说明你读的源码没被掉包
  2. 比对 ②:你用这份源码在固定 Docker 环境里重新编译出的 CID = 公布的 Game client CID → 说明线上跑的游戏正是这份源码编出来的

两个都对上 = 你能读的(源码)就是线上真在跑的(游戏)。官方没法「给你看一份干净代码、却偷偷跑一份作弊代码」——任何人重编都会当场对不上、立刻穿帮。

读取官方发布参数

curl -L -o release.json https://fairpoker.app/source/release.json
# 看:gameClientCid、currentSourceRelease.ipfsCid、archiveSha256,
#     以及 reproducibleGameClientBuild.buildEnv(含 SOURCE_DATE_EPOCH)

下载源码包并校验(比对 ①)

SOURCE_ARCHIVE_URL=$(node -e "console.log(require('./release.json').archiveUrl)")
curl -L -o fair-poker-source.tar.gz "$SOURCE_ARCHIVE_URL"

# (a) SHA256 应等于 release.json 的 archiveSha256
shasum -a 256 fair-poker-source.tar.gz

# (b) 源码包 CID 应等于 release.json 的 currentSourceRelease.ipfsCid  ← 比对 ①
ipfs add -rQ --cid-version=1 --raw-leaves --only-hash fair-poker-source.tar.gz

在固定 Docker 环境里重新编译

tar -xzf fair-poker-source.tar.gz   # → fair-poker-source/
cd fair-poker-source

# 用 release.json 的 reproducibleGameClientBuild.buildEnv 里的值:
export SOURCE_DATE_EPOCH=...                 # buildEnv.SOURCE_DATE_EPOCH
export REACT_APP_SOURCE_ARCHIVE_IPFS_CID=...
export REACT_APP_SOURCE_ARCHIVE_SHA256=...
export REACT_APP_SOURCE_ARCHIVE_URL=...

bash scripts/reproducible-game-build.sh ../fair-poker-source.tar.gz

比对结果(比对 ②)

脚本最后会打印一个 Game client CID。它应当等于上面「发布事实」里的 Game client CID,也等于你实际进入游戏时浏览器加载的那个 CID。

每个比对结果说明什么

比对 ① 对上
你正在读的源码=官方公布的源码,没被掉包。
比对 ② 对上
线上正在跑的游戏=这份源码编出来的,没换成别的。
① + ② 都对上
眼见(源码)即所得(线上游戏)。官方没有藏一手的空间。

不想编译?两个更轻量的快速自查:① 用 ipfs.iodweb.link 等不同网关打开同一个 Game client CID,应得到同一份文件(内容寻址,改一个字 CID 就变);② 牌桌「安全牌局」面板会在运行时自动比对你加载的客户端,显示「已锁定 / 不符 / 未固定」。

诚实边界:这套复现证明的是「玩家浏览器里跑的客户端」。后端中转服务器(worker)不在复现范围内——但你不需要信任它:上面公开的客户端源码已经证明 worker 只是一个「哑巴中转」,只看得到密文,既不能发牌、不能踢人、也不能改结果。完整说明见 REPRODUCIBLE_BUILD.md

First, the most common misconception: the «Release facts» above show two different CIDs, and they are different by design and never equal — do not compare these two to each other.

  • Game client CID = the compiled, ready-to-play build (the baked cake).
  • Source package CID = the readable source code you audit (the flour and eggs).

A bag of flour and a cake never share a label — that's normal. Proving «live = source» relies on matching each one to itself, twice:

  1. Check ①: the CID of the source package you download = the published Source package CID → the source you read was not swapped.
  2. Check ②: the CID you rebuild from that source in a pinned Docker toolchain = the published Game client CID → the live game is compiled from exactly this source.

Both matches = what you can read (source) is what actually runs (game). The operator cannot show clean code while secretly running cheating code — any independent rebuild would fail to match and expose it instantly.

Read the official release inputs

curl -L -o release.json https://fairpoker.app/source/release.json
# see: gameClientCid, currentSourceRelease.ipfsCid, archiveSha256,
#      and reproducibleGameClientBuild.buildEnv (incl. SOURCE_DATE_EPOCH)

Download the source package and verify (Check ①)

SOURCE_ARCHIVE_URL=$(node -e "console.log(require('./release.json').archiveUrl)")
curl -L -o fair-poker-source.tar.gz "$SOURCE_ARCHIVE_URL"

# (a) SHA256 must equal release.json archiveSha256
shasum -a 256 fair-poker-source.tar.gz

# (b) source package CID must equal release.json currentSourceRelease.ipfsCid  ← Check ①
ipfs add -rQ --cid-version=1 --raw-leaves --only-hash fair-poker-source.tar.gz

Rebuild in the pinned Docker toolchain

tar -xzf fair-poker-source.tar.gz   # → fair-poker-source/
cd fair-poker-source

# use the values from release.json reproducibleGameClientBuild.buildEnv:
export SOURCE_DATE_EPOCH=...                 # buildEnv.SOURCE_DATE_EPOCH
export REACT_APP_SOURCE_ARCHIVE_IPFS_CID=...
export REACT_APP_SOURCE_ARCHIVE_SHA256=...
export REACT_APP_SOURCE_ARCHIVE_URL=...

bash scripts/reproducible-game-build.sh ../fair-poker-source.tar.gz

Compare the result (Check ②)

The script prints a final Game client CID. It must equal the Game client CID in «Release facts» above — and the CID your browser actually loads when you enter the game.

What each comparison proves

Check ① matches
the source you're reading is the official source, not swapped.
Check ② matches
the live game is built from this exact source, nothing else.
① + ② both match
what you see (source) is what runs (game). No room for hidden code.

Don't want to compile? Two lighter quick checks: ① open the same Game client CID through different gateways (ipfs.io, dweb.link) — you should get identical files (content-addressed: change one byte and the CID changes); ② the table's «Secure Table» panel compares your loaded client at runtime and shows locked / mismatch / not pinned.

Honest boundary: this reproduction proves the client running in the player's browser. The backend relay server (worker) is out of scope — but you don't need to trust it: the public client source above already proves the worker is a dumb relay that only sees ciphertext and cannot deal cards, remove players, or change results. Full guide: REPRODUCIBLE_BUILD.md.

每手自带 4 灯公平校验(自动,无需操作)

每局结束的瞬间,浏览器自动跑 4 项校验并在桌面亮一个覆盖层:① 牌堆完整(52 张唯一、无重复)·② 全员参与洗牌(每位玩家都完成 shuffle + lock)·③ 记录指纹一致(canonicalHandHash 可与其他玩家比对)·④ 签名齐全。通过盖"已验证"印,异常红灯并提示下载证据。无需任何命令行;只在看到红灯或想自己再确认时再走下面的复验流程。

下载并在本机重放一局牌

  1. 在牌桌左上角打开「安全牌局」面板。
  2. 点「下载」,保存本局 transcript JSON。
  3. 在核心源码目录安装依赖,运行 verifier。
npm ci
npm run verify:transcript -- /path/to/transcript.json

输出通过,即代表本局的 hash-chain、事件顺序、签名格式、下注、奖池与结果都能在你本机重放复验。篡改 transcript 中任意关键字段,verifier 会报错或给出警告。

Every hand auto-runs a four-light fairness audit (no action needed)

The moment a hand ends, the browser automatically runs four checks and shows an overlay on the table: ① deck integrity (52 unique, no duplicates) · ② all players shuffled and locked · ③ matching record fingerprint (canonicalHandHash, exportable for cross-player comparison) · ④ signatures complete. A pass stamps "verified"; a warn lights red and offers the evidence for download. No command line required; only follow the steps below when you see a red light or want to confirm a hand yourself.

Download and replay a hand on your own machine

  1. Open the Secure Table panel in the upper-left table tools.
  2. Click Download to save the hand transcript JSON.
  3. In the core source directory, install dependencies and run the verifier.
npm ci
npm run verify:transcript -- /path/to/transcript.json

A passing result means the hand's hash-chain, event order, signature format, bets, pot, and outcome can all be replayed on your own machine. Tampering with any important transcript field makes the verifier fail or warn.

为什么官方踢不了人Why the operator can't remove a player

这是「轮流加密」扑克:开局时,牌桌上每个人都用自己的私钥,对整副牌轮流加了一层锁。要翻开任何一张牌,必须每个人都用各自的钥匙解开自己那层——少一个人,那张牌就永远解不开。

因此「谁在座、这手牌能不能继续」完全由各玩家浏览器里的密钥决定,不由服务器决定:

  1. 服务器(worker)只是一个「哑巴中转」:它只转发各方已签名的消息,自己既没有任何人的私钥,也无权改动牌局状态。你在上面第 1 步校验过的公开源码里就能确认这一点。
  2. 服务器断开某个连接,最多只能触发对方「自动重连」;它无法占据座位、无法替谁解牌、也无法把谁判负——这些都需要对应玩家的私钥,而私钥只存在各自的浏览器里。
  3. 所以当有人彻底离开、缺了他那把钥匙时,这手牌在数学上就无法继续——只能整桌作废、各自重开。这不是官方踢人,而是密码学的硬限制,对官方和玩家一视同仁。

一句话:官方就算想踢人或顶替你,技术上也做不到——它手里没有你的钥匙。上面第 1 步证明了「线上代码 = 这份公开源码」,你可以亲自读这份源码,确认服务器确实只是中转。

This is “take-turns encryption” poker: at the start of a hand, every player locks the whole deck with their own private key, one layer each. To reveal any card, every player must peel off their own layer with their own key — if even one player is missing, that card can never be opened.

So “who is seated, and whether the hand can continue” is decided entirely by the keys in each player's browser, not by the server:

  1. The server (worker) is only a dumb relay: it forwards each side's signed messages and holds nobody's private key and no authority to change game state. You can confirm this in the public source you verified in Step 1.
  2. If the server drops a connection, the most it can do is trigger that player's auto-reconnect. It cannot seize a seat, decrypt cards for anyone, or declare anyone the loser — all of that needs the relevant player's private key, which lives only in their browser.
  3. That's why, when someone leaves for good and their key is gone, the hand mathematically cannot continue — the table is voided and everyone restarts. This is not the operator kicking anyone; it's a hard cryptographic limit that applies equally to the operator and the players.

In short: even if the operator wanted to kick or impersonate you, it technically cannot — it does not hold your key. Step 1 above proves “the live code = this public source,” so you can read that source yourself and confirm the server is only a relay.