“我们用 Math.random() 公平抽的”——这话和”我以人格保证”在工程上等价,零外部可验证性。真正的公开抽奖必须做到任何参与者能独立验证结果、且主办方在抽签前不可能预知结果。这要么靠承诺-揭示模式,要么靠不可操纵的公共随机源,要么靠密码学的可验证随机函数。
截图 / 录屏抽奖为什么不算公平
朋友圈抽奖最常见的形式:
主办方截图:随机数 = 7,对应名单第 7 位 @张三 中奖
这种”证据”的问题:
- 截图 / 录屏无法证明”现场”:主办方可以提前抽 100 次,挑结果最满意的那次截图发出来——参与者看不到被丢弃的 99 次
- 代码可控:主办方电脑上可以提前改
Math.random()让结果倾向自己人 - DOM 可改:直播抽奖网页 F12 把列表里某些名字的权重改成 0
- 没有时间锚:抽签时间不绑定任何外部事件,主办方可以”看到结果不满意就重抽”
形式公平 ≠ 可验证公平。“看起来很随机”和”任何人都能事后审计这个结果是不可操纵的”是两回事。
公平的两个核心要求
工程上”可证明公平”的抽奖必须同时满足:
- 可验证(Verifiable):抽签结果发布后,任何参与者用公开信息能独立算出同样的结果
- 不可预定(Unpredictable):主办方在抽签时刻之前无法知道结果是什么
这两条互不蕴含——可验证的不一定不可预定(主办方可以挑选有利的算法),不可预定的不一定可验证(用真随机但无法事后审计)。要同时达成,工程上有三种主流方案。
方案 A:承诺-揭示(commit-reveal)
核心思路:抽签前先公布一个”封锁的承诺”(hash),抽签后揭示原始数据,参与者复算验证。
标准流程
第一步:承诺(在所有参与者名单确定后)
salt = 随机生成 32 字节 (主办方私藏)
roster = ["张三", "李四", ..., "王五"] (名单,公开)
rules = "按 hash(salt + name) 排序取前 3" (规则,公开)
commitment = SHA-256(salt + JSON.stringify(roster) + rules)
主办方把 commitment(一个 64 位 hex 字符串)公开发布——发到群里、贴在网页上、推给所有参与者。
第二步:等待
参与者已经知道:
- 承诺值
commitment - 名单
roster - 规则
rules
不知道的:
- 盐
salt
第三步:揭示
抽签时刻,主办方公布 salt:
公布:salt = "a3f2c8de1b9..."
任何参与者自己跑:
import hashlib, json
salt = "a3f2c8de1b9..."
roster = ["张三", "李四", ..., "王五"]
rules = "按 hash(salt + name) 排序取前 3"
# 1. 验证承诺
recomputed = hashlib.sha256(
(salt + json.dumps(roster) + rules).encode()
).hexdigest()
assert recomputed == commitment # 验证主办方没改 salt
# 2. 复算结果
scored = [(hashlib.sha256((salt + name).encode()).hexdigest(), name)
for name in roster]
scored.sort()
winners = [name for _, name in scored[:3]]
所有人算出同一个 winners——这就是验证。
为什么这个方案安全
- 主办方无法操纵 salt:commitment 已经发布,hash 是单向的,改 salt 必然 commitment 变化,参与者立刻发现
- 主办方无法预知结果:salt 没揭示前主办方是知道的,但他不能改名单——名单已经在 commitment 里被锁定。唯一的攻击向量:主办方在抽签前选了一个对自家有利的 salt——所以承诺必须在名单确定后发布
实务细节
坑 1:commitment 发布时机——必须在名单完整确定之后。如果先发承诺、后接受报名,主办方可以用已知 salt 反推有利名单。
坑 2:salt 长度——32 字节(256 bit)随机串,太短的 salt 主办方可以暴力试 salt 值找到对自家有利的——salt 只有 4 位数字时主办方能秒级试 10000 次。
坑 3:算法不确定性——JSON.stringify 在不同语言里键的顺序可能不同,验证脚本必须明确序列化方式。安全做法是规则里写死”按字符串字典序排列后用 \n 拼接”。
方案 B:公共随机源(区块链 / 公开事件)
核心思路:用一个”未来才确定、主办方无法操纵、全网可查”的公开事件作为随机种子。
比特币区块 hash
公告:
抽签种子 = 2026-05-10 北京时间 00:00 后第一个比特币区块的 hash
到了那一刻,所有人去 mempool.space、blockchain.com 查这个块的 hash,作为种子算结果:
seed = "0000000000000000000abc123def..." # 区块 hash
roster = ["张三", "李四", ...]
scored = [(hashlib.sha256((seed + name).encode()).hexdigest(), name)
for name in roster]
scored.sort()
winners = [name for _, name in scored[:3]]
为什么不可操纵:比特币区块 hash 是全球矿工 PoW 博弈结果,主办方需要租用全球 30%+ 的算力才能影响结果——经济上不可行。
其他可用的公共种子
| 来源 | 优点 | 缺点 |
|---|---|---|
| 比特币区块 hash | 全球公认、不可操纵 | 国内合规风险 |
| 上证综指收盘价 | 国内合规 | 精度低(4 位数字),可枚举 |
| 中国福利彩票开奖号 | 公信力高 | 时间锚定难(每天 21:15 开) |
| 国家天文台报时秒值 | 中立 | 精度问题 |
| Chainlink VRF(链上) | 密码学验证 | 需要部署合约 |
| drand 信标网络 | 专门设计 | 知名度低 |
精度低的解法:把多个低精度源拼起来 hash,例如 SHA-256(沪指收盘 + 深成指收盘 + 五点天气 + ...)——把几个独立公开事件拼接,主办方需要同时操纵全部才能预测结果。
时机锁定
公共随机源方案的关键是时机锁定——公告里必须明确写死:
种子 = 某具体时刻 t 后第一个 / 第 N 个公开事件的值
不能写”今天某个比特币区块”——这给主办方在多个候选块之间挑选的空间。
方案 C:可验证随机函数(VRF)
核心思路:主办方持密钥,输入一个公开种子,密码学输出”随机数 + 由我私钥生成的证明”。
流程
抽签前:主办方公布公钥 pk。
抽签时:选一个公开输入 input(例如本月销售额、今天日期、上一个抽签结果),主办方用私钥 sk 算:
(rand, proof) = VRF_sign(sk, input)
公布 (rand, proof)。
验证:任何人用公开的 pk + input + proof 验证:
VRF_verify(pk, input, rand, proof) = true / false
如果通过,证明 rand 确实是由 pk 对应的私钥从 input 生成的、且对该 input 唯一——主办方不能”试一试不满意再换”。
为什么不可操纵
- 确定性:同一个
(sk, input)永远输出同一个rand——主办方不能挑结果 - 可验证:
proof只能由sk生成,外人验证 = 100% 信任 - 不可预测:不知道
sk的人无法从input算出rand
但有一个细节:input 必须不可被主办方操纵。如果 input 是”今天日期”,主办方等于完全可控(推迟一天 input 就变)。所以 VRF 通常和方案 B 结合——input = 区块链未来某区块的 hash,VRF 的 (rand, proof) 由主办方算出公布。
实战工具
- Chainlink VRF:链上抽奖事实标准,以太坊 / Polygon / BSC 都支持
- Algorand VRF:协议自带,每个区块都有 VRF
- drand:分布式随机信标,CloudFlare 等节点共同维护
三种方案对比
| 方案 | 不可预定来源 | 验证门槛 | 适用场景 |
|---|---|---|---|
| 承诺-揭示 | 主办方私藏的 salt | 低(一行 SHA-256) | 群抽奖、社区抽奖、内部活动 |
| 公共随机源 | 区块链 / 公开事件 | 中(要会查公开事件) | 大型 / 商业抽奖、需高公信力 |
| VRF | 密钥 + 公开 input | 高(需密码学库) | 链上 DApp、自动化抽奖系统 |
推荐:
- 个人 / 小型抽奖:承诺-揭示足够,技术门槛最低
- 公司年会 / 大型促销:承诺-揭示 + 公开 input(如
salt = 抽签当晚 8 点央视新闻联播开始时的实时分钟数),把”主办方挑 salt”的可能性也堵掉 - 链上 / DApp:直接用 Chainlink VRF,别自己造
几个失败案例
案例 A:承诺时机错了
某 App 内测公告说”开放报名 → 报名截止 → 抽奖”,但提前发了承诺。结果有人发现可以偷偷修改自己提交的昵称使 hash 落在中奖区间——主办方没意识到 salt 已知后用户名是可控的。
修法:承诺必须在所有用户输入冻结之后再发。
案例 B:公共种子被主办方控制
某直播抽奖公告”用某网红下条视频的点赞数当种子”——结果到点该网红没发视频,主办方临时改规则用其他源——失去信任。
修法:种子来源必须不依赖任何特定个人的行为——区块链、彩票、收盘价这种”按时一定发生”的事件才行。
案例 C:算法歧义
承诺里写”按 hash 排序”,但没说升序还是降序、字符串 hash 还是字节 hash——揭示时主办方按对自家有利的方向选。
修法:规则必须精确到能写成代码的程度。最好附上验证脚本的 GitHub 链接。
一句话总结
Math.random() 抽奖等于零证明;真正可验证公平靠承诺-揭示(发 hash 锁名单 + 揭示 salt 复算)、公共随机源(用未来某个区块 hash 当种子)、或 VRF(密钥+公开 input);承诺要在名单冻结后发、种子要不依赖任何个人行为——这两条踩稳,小型抽奖到大型促销都能压住质疑。