公开抽奖怎么证明公平:承诺 + 揭示、区块链种子、VRF 三种方案

· 约 7 分钟 🎲 随机抽签

“我们用 Math.random() 公平抽的”——这话和”我以人格保证”在工程上等价,零外部可验证性。真正的公开抽奖必须做到任何参与者能独立验证结果、且主办方在抽签前不可能预知结果。这要么靠承诺-揭示模式,要么靠不可操纵的公共随机源,要么靠密码学的可验证随机函数。

截图 / 录屏抽奖为什么不算公平

朋友圈抽奖最常见的形式:

主办方截图:随机数 = 7,对应名单第 7 位 @张三 中奖

这种”证据”的问题:

  1. 截图 / 录屏无法证明”现场”:主办方可以提前抽 100 次,挑结果最满意的那次截图发出来——参与者看不到被丢弃的 99 次
  2. 代码可控:主办方电脑上可以提前改 Math.random() 让结果倾向自己人
  3. DOM 可改:直播抽奖网页 F12 把列表里某些名字的权重改成 0
  4. 没有时间锚:抽签时间不绑定任何外部事件,主办方可以”看到结果不满意就重抽”

形式公平 ≠ 可验证公平。“看起来很随机”和”任何人都能事后审计这个结果是不可操纵的”是两回事。

公平的两个核心要求

工程上”可证明公平”的抽奖必须同时满足:

  1. 可验证(Verifiable):抽签结果发布后,任何参与者用公开信息能独立算出同样的结果
  2. 不可预定(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);承诺要在名单冻结后发、种子要不依赖任何个人行为——这两条踩稳,小型抽奖到大型促销都能压住质疑。

❓ 常见问题

直播抽奖时主办方说"我现场用 Math.random() 抽的"算公平吗?

不算——这是零证明。理由:(1) F12 改 DOM 一行:在主办方电脑上抽奖前先把名单里"自己人"的概率改成 100%;(2) Math.random() 不可重现:参与者无法事后验证当时确实是这个种子;(3) 录屏可剪辑:直播看似"现场抽",实际可以预录或多次重抽到满意结果再播。真正公平的抽奖必须满足两条:(a) 可验证——任何参与者能事后独立算出同一个结果;(b) 不可预定——主办方在抽签前无法知道结果。这两条要靠"承诺-揭示"模式或公共随机源实现,不是嘴上说"我保证公平"。

什么叫"承诺-揭示"(commit-reveal)模式?

两步走:抽签前先公布承诺、抽签后揭示原始数据,参与者自己复算验证。具体:(1) 主办方生成一个随机盐 salt,把 hash(salt + 名单 + 规则) 公布出去(例如发到群里 / 贴在网站上)——这就是"承诺";(2) 抽签时间到了,揭示原始 salt 和算法(例如"按 hash 排序取前 3");(3) 任何人用揭示的 salt + 名单 + 规则,自己跑一遍 hash,能算出和主办方公布的相同结果——验证通过。关键安全性:承诺一旦发布就不能改(hash 是单向的),主办方无法在揭示阶段动 salt 来操纵结果;同时参与者抽签前看不到 salt,无法预测自己中签概率。前提:承诺必须在所有参与者确定后发布,否则主办方可以根据已知名单选 salt。

用比特币区块 hash 当种子是怎么回事?

用一个"未来才确定、谁也无法预测、全网公开可查"的事件作为种子,让主办方不可能预知结果。流程:(1) 抽签前公告"将以 2026-05-10 北京时间 00:00 后第一个比特币区块的 hash 作为种子";(2) 那个时刻到了,从区块浏览器(mempool.space、blockchain.com)查到该块 hash,例如 0000000000000000000abc123...;(3) 用这个 hash 做随机数种子,按公开算法(如 hash(seed + index) 取模)抽出中奖者。为什么不可操纵:比特币区块 hash 是全球矿工算力博弈的结果,主办方既无法预知也无法干预(除非他能改写比特币)。适用范围:抽奖金额大、参与者多、需要强公信力的场景;但需要等待区块确认(通常 10 分钟一个),不适合"按按钮立刻出结果"。中国区注意合规,可改用上证综指收盘价、彩票开奖号等同等性质的公开事件。

VRF(可验证随机函数)和承诺-揭示有什么区别?

VRF 用密码学一步到位输出"随机数 + 这个数确实由我私钥生成"的证明,不需要事前承诺。流程:(1) 主办方持有一对密钥(私钥 + 公钥,公钥事先公布);(2) 抽签时输入一个公开的种子(如本月销售总额、今天日期),主办方用私钥算出 (随机数, 证明);(3) 任何人用主办方公钥 + 种子 + 证明,能验证"这个随机数确实是用主办方私钥生成的、且唯一确定"。优点:(a) 单步完成不用等揭示;(b) 同一个种子永远输出同一个随机数(确定性),所以主办方不能"试运行"挑结果;(c) 在区块链 / 智能合约场景中常用(Chainlink VRF、Algorand)。门槛:参与者要懂密码学才能验证,不像承诺-揭示直接复算 hash 那么直观——所以主办方多在网页上提供"一键验证"工具。

🎲 打开 随机抽签 单签/多签 · 滚动动画 · 历史记录