TG 抽奖公开算法与在线验算
TG 抽奖公开算法与在线验算
Telegram 群抽奖公开算法与在线验算
RonanCat 官方 Telegram 群抽奖采用 SHA-256 承诺种子可验证公平算法(Provably Fair)。任何参与者均可在开奖后通过下方浏览器端验算器或本地命令行独立复算中奖签号,计算全程在您的本地设备内执行,不向服务器发送任何请求。
1. 在线独立验算器
输入开奖公示参数核验结果
| 排名 | 签号 | 状态 | 哈希输入与 SHA-256 得分 |
|---|
2. 算法流程与防作弊机制
| 阶段 | 执行逻辑 | 公平性保障 |
|---|---|---|
| 发起活动(Commit) | 系统通过操作系统加密安全随机数生成器(PHP random_bytes(32),256-bit CSPRNG)生成随机种子 seed,立即计算并在群活动卡片公布承诺哈希 commit = SHA256(seed)。 |
原始 seed 开奖前严格保密(管理员在 Telegram 界面亦不可见),承诺哈希公开锁定,开奖后无法更换或挑选种子。 |
| 私聊报名(一人一签) | 数据库通过 (lottery_id, user_id) 唯一索引限制每个已绑定官网账号每期仅限报名 1 次,按报名先后顺序依次分配连续签号 #1 至 #N,并在私聊报名回执与 /lottery 页面展示本人签号。 |
杜绝单账号重复入池,每位参与者权重完全相等。 |
| 到期开奖(Reveal) | 开奖公示同时披露原始 seed、总报名人数 N 与全部中奖签号,并提供带参数的在线核验链接;在机器人发送 /lottery 活动编号 可随时调取往期核验档案。 |
哈希输入同时绑定 seed 与截止时的最终总人数 N,报名期间 N 动态变化,任何人均无法在报名阶段预判中奖签号。 |
3. 确定性排序公式
设当期抽奖活动编号为 lottery_id,截止开奖时的总报名人数为 N,设定的奖品份数为 M(实际中奖人数 W = min(N, M)),开奖公开的原始种子为 seed。
- 校验承诺哈希:计算
SHA256(seed)(小写十六进制),核对其与活动发布时公开的承诺哈希commit完全一致。 - 计算每签得分:对每个签号
k = 1, 2, ..., N,按 UTF-8 拼接字符串并计算 SHA-256 小写十六进制摘要:
input(k) = "{seed}:{lottery_id}:{N}:{k}"
score(k) = SHA256(input(k))
- 升序截取中奖签号:将全部签号
1..N按score(k)的 ASCII 字典序从小到大排序(若极小概率出现哈希相同,则按签号k从小到大排序),排名前W位的签号即为当期中奖签号。
4. 本地终端复算命令
无需依赖任何网页,您也可以直接在本地电脑终端运行以下命令独立验算(替换开头的参数即可):
Python 3
import hashlib
lottery_id = 8
total_n = 5
winner_m = 2
commit = "a8ae6e6ee929abea3afcfc5258c8ccd6f85273e0d4626d26c7279f3250f77c8e"
seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
calc_commit = hashlib.sha256(seed.encode("utf-8")).hexdigest()
assert calc_commit == commit, f"承诺哈希不匹配: {calc_commit}"
ranked = sorted(
(hashlib.sha256(f"{seed}:{lottery_id}:{total_n}:{k}".encode("utf-8")).hexdigest(), k)
for k in range(1, total_n + 1)
)
print("承诺哈希校验通过:", calc_commit)
print("中奖签号:", [f"#{k}" for _, k in ranked[: min(total_n, winner_m)]])
for idx, (score, k) in enumerate(ranked, 1):
tag = "中奖" if idx <= min(total_n, winner_m) else "未中"
print(f"第 {idx} 名 | 签号 #{k} | {tag} | {score}")
Node.js
const crypto = require("crypto");
const lotteryId = 8, totalN = 5, winnerM = 2;
const commit = "a8ae6e6ee929abea3afcfc5258c8ccd6f85273e0d4626d26c7279f3250f77c8e";
const seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const sha256 = (s) => crypto.createHash("sha256").update(s, "utf8").digest("hex");
if (sha256(seed) !== commit) throw new Error("承诺哈希不匹配");
const ranked = Array.from({ length: totalN }, (_, i) => {
const k = i + 1;
return { k, score: sha256(`${seed}:${lotteryId}:${totalN}:${k}`) };
}).sort((a, b) => (a.score < b.score ? -1 : a.score > b.score ? 1 : a.k - b.k));
console.log("中奖签号:", ranked.slice(0, Math.min(totalN, winnerM)).map((r) => `#${r.k}`));
console.table(ranked);
5. #1 首期开奖(Legacy 历史算法)说明
在升级为上述 SHA-256 承诺种子可验证算法前,历史场次 #1(首期「PLUS不限时 100G 幸运抽奖」)及 #2(「5GB 套餐流量 幸运抽奖」)使用的是第一代 Legacy v0 等概率无放回洗牌算法。现将当时实际执行的完整服务端代码与开奖数据公开如下:
5.1 历史场次实际执行的 PHP 开奖核心代码
// 1. 数据库唯一索引 uniq_lottery_user (lottery_id, user_id) 强制一人一签,无任何人工权重
$entries = DB::table('v2_telegram_lottery_entries')
->where('lottery_id', $lotteryId)
->get()
->all();
// 2. 在数据库行级排他锁事务(SELECT ... FOR UPDATE)内执行 PHP 原生 Fisher-Yates 洗牌
shuffle($entries);
// 3. 无放回截取打乱后的前 winner_count 名作为中奖者
$winners = array_slice($entries, 0, min(count($entries), max(1, (int) $lot->winner_count)));
5.2 #1 首期开奖实盘数据与公平性说明
| 项目 | #1 首期活动实盘记录 |
|---|---|
| 活动名称 | PLUS不限时 100G 幸运抽奖(消耗 10 积分) |
| 开奖时间 | 2026-11-11 11:11:33(到期由后台计划任务自动触发) |
| 有效报名池 | 共 71 人报名(uniq_lottery_user 唯一约束,每人严格 1 签) |
| 抽取名额与概率 | 无放回抽出 10 名,每位报名者中奖概率严格相等,均为 10 / 71 ≈ 14.08% |
| 洗牌抽中序号顺序 | 按 shuffle($entries) 打乱后的前 10 位报名序号依次为:#33、#66、#61、#45、#19、#47、#11、#42、#39、#26(群友可私聊机器人发送 /lottery 1 查看本人当期签号与完整中奖名单) |
同构过渡场次 #2 |
5GB 套餐流量 幸运抽奖(2026-11-11 13:23:04 开奖,共 11 人报名抽 3 名,抽中报名序号依次为 #9、#6、#8,可发送 /lottery 2 查询) |
5.3 为什么从 Legacy v0 升级到 SHA-256 承诺种子算法
- Legacy v0 的优点与局限:PHP 标准库
shuffle()底层采用经典的 Fisher-Yates 洗牌算法,对数组所有排列等概率采样,无任何人工干预或账号权重倾斜;但由于其使用进程内即时随机数状态,洗牌完成后无法将随机过程导出供外部重放复算。 - 升级后的可验证性:自
#3期及之后发起的所有新抽奖活动,全面启用本页第 1 至第 4 节所述的 SHA-256 承诺种子算法(Commit-Reveal),发起即锁定承诺哈希、开奖即公开原始种子,任何群友均可事后 100% 独立复算验证。