① 输入参数
1roundSeed — 本期开卖之前就生成并锁定的 32 位十六进制随机种子;卖票期间只公开它的承诺哈希 commit = SHA256(roundSeed : 期号)
2roundNo — 期号(第几期)
3blockHash / blockNumber — 开奖时刻 BSC 最新区块的哈希与高度(公开可查)
4total — 本期总份额(即最大号码,号码区间 1 ~ total)
② 计算步骤
A拼接原始串 R = roundSeed + ":" + roundNo + ":" + blockHash + ":" + blockNumber
B哈希运算 seed = SHA256(R)(固定 SHA-256,不随环境变化)→ 64 位十六进制
C把 seed 从前往后切成 15 位十六进制 的窗口(每窗口 60 位整数)
D取第一个「小于可整除上界」的窗口 v,得 winner = (v mod total) + 1 ← 即为幸运号码
③ 伪代码
R = roundSeed + ":" + roundNo + ":" + blockHash + ":" + blockNumber
seed = sha256(R) // 64 位十六进制
span = 2^60
limit = (span / total) * total // 可整除上界,丢掉不能整分的尾巴
for (i = 0; i + 15 <= 64; i += 15) {
v = parseInt(seed.substr(i, 15), 16)
if (v < limit) { winner = (v % total) + 1; break } // 落在 1 ~ total
}
④ 为什么这样最公平
✓承诺在先:种子在卖票前就定死并公布哈希,运营方无法在看到销量后「换一个对自己有利的种子」
✓区块锁结果:开奖时的区块哈希是全网公开、且开奖前无法预知的
✓无偏抽签:用 60 位窗口 + 丢弃尾巴,保证 1 ~ total 每个号码严格等概率(旧的「取末 12 位再取模」写法有极小偏差)
✓可验证:不再掺入 mt_rand() / microtime() 这类无法复算的输入,同一条公式跑两遍必然同号
⑤ 号码怎么发(随机 · 不重复 · 可复算)
1号码池 = 1 ~ total(例:1000 份 → 000001 ~ 001000)。一个号码只属于一个人,绝不重复售卖
2发号不用顺号,而是用本期种子派生一个 双射置换 P(4 轮 Feistel 网络,轮函数 SHA-256),把「第 i 个被卖出的份额」映射成一个号码:号码 = P(i)
3「双射」意味着 i 不同 ⇒ 号码必不同:无论谁先买、买几份,都不可能撞号;反过来 i 走完 1 ~ total 时号码池恰好被用尽,不漏号也不多号
4卖票期间种子保密(只公布承诺哈希)→ 任何人都无法预知下一个号码;开奖后种子揭示 → 整期的发号顺序同样可以被复算核对
✓兜底硬约束:数据库对 (商品, 期号, 号码) 建了唯一索引 —— 即使代码出 bug、并发竞态,同一个号码也不可能在同一期卖出两次
⑥ 算例(可用上面公式自行核对)
设 total = 80,期次种子 roundSeed = a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4,roundNo = 1
开奖区块 blockNumber = 12345678,blockHash = 0x3d0f5b8c1a2e4d6f7b9c0e1a3d5f7b9c2e4a6d8f0b1c3e5a7d9f1b3c5e7a9d1f
R = a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4:1:0x3d0f5b8c1a2e4d6f7b9c0e1a3d5f7b9c2e4a6d8f0b1c3e5a7d9f1b3c5e7a9d1f:12345678
seed = 56918a072557366c9c95c10a9469462234ba5e3fd49ae0e8008cc3ae1ad08cfb
首窗口 = seed[0:15] = 56918a072557366 → v = 389869920136295270(小于上界,采用)
winner = 389869920136295270 mod 80 + 1 = 70 + 1 = 71
→ 幸运号码为 71 号;本期承诺哈希 commit = fdd9db193f79dfbdd7d72e1793f39149