开奖引擎 / Draw Engine 三种实现 + 一个草案 2026

夺宝开奖
怎么做到 公平

一张彩票真正难的不是「开奖」——而是凭什么相信开奖结果。
我们把这件事做了三种实现:后端开奖、VRF、全链上;另有一个 ticketRoot 草案
看完这一页,你会知道它们各自怎么跑、各自适合什么。

阅读时间 · 约 8 分钟 · 给小白和产品看的
▸ 看了 30 秒还想看更深 先看 30 秒动画版?→ prototype-mechanics.html 01 开奖原理
先看总览 / Start Here

先用一张表,知道新版到底 怎么开奖

下面这张表不是技术细节,而是先帮你建立方向感:现在默认用什么、票号谁来分、钱和配置放在哪里、FULL_CHAIN 为什么会更重。

你最关心的事
我们怎么做
一句话解释
现在状态
v1.0 默认开奖
VRF_ONLY
随机数链上可验证,买票仍然零 gas
主网目标
本地 / 测试
BACKEND
不依赖外部 VRF,方便完整跑通本地测试流程
local 默认
票号分配
后端维护剩余号
用户买票不需要链上交易,体验和成本最好
v1.0 推荐
分账配置
后端配置 / Nacos / DB
新版 BACKEND / VRF_ONLY 不读旧版主网合约 rewardConfigId
v1.0 口径
FULL_CHAIN
旧版全链闭环
链上买票、合约分号、VRF 开奖、合约分账
高价值场预留
方案四草案
ticketRoot + Merkle proof + Vault
买票零 gas,但 VRF 前把票表指纹锁到链上,领奖时合约验票付款
草案 / draft
30 秒概念扫盲 / Pre-flight Primer

先认识 6 个词,后面就都看得懂。

下面的内容会反复用到这几个词。如果你已经熟,跳过;如果你不熟,花 30 秒看完——后面所有"链上"、"VRF"、"gas" 出现的地方就不会再卡你。

区块链
blockchain
一个对所有人公开、谁都改不了的电子账本。每写一条记录,全世界几千台电脑都会同步存一份副本。
小区门口贴的红纸告示——人人能看、撕了也立刻有备份。
智能合约
smart contract
写在区块链上的一段代码。规则一旦部署,谁都改不了——包括写它的人。投币、按按钮、出货,自动执行。
写好规则的自动售货机,钱投进去机器自动出货,店主管不了。
链上 vs 链下
on-chain / off-chain
链上=写在区块链账本里(公开、不可改、要付手续费)。链下=写在公司自己服务器里(私有、可改、免费)。
链上=去公证处盖章;链下=在自家账本上写一笔。
Gas
gas fee
在区块链上"做事"要交的手续费。每写一笔账都要付,付给维护账本的矿工/验证者。复杂操作付得多,简单操作付得少。
寄快递的运费——不管寄什么,都要付邮票钱才能进系统。
VRF
verifiable random function
Chainlink VRF 这类可验证随机数服务——出号的同时附上一段数学证明,谁都能验这个数是不是按这次链上请求生成、有没有被改过。BSC / ETH 主网都已上线 Chainlink VRF v2.5;TRON 没有 Chainlink,v1.0 在 TRON 走 BACKEND 模式。
摇号机出球的同时打印一张防伪鉴定书。
私钥 / 钱包
private key / wallet
区块链账户的"密码"。谁有私钥谁就是这个账户的主人。一旦泄露 = 资产被偷,没有客服能找回。
家门钥匙——丢了别人就能进,没有 110 能帮你换锁。
铺垫 / The Trust Problem

最难的不是「怎么开」,
是「凭什么相信」。

夺宝、彩票、抽奖——所有这类产品的核心矛盾都是同一句话:用户付了钱,怎么相信开出来的号是公平的?

这件事拆开就是三个问题。三种主方案和一个草案的差别,本质是它们对这三个问题的回答不一样。

问题 01

随机数从哪来?

是服务器自己生成?还是预言机给的?还是参与者一起算的?源头决定了信任根。

问题 02

有人能改它吗?

开奖之后能被回滚或调整吗?运营方、合约部署者、节点运营商——谁都能动,结果就不算公平。

问题 03

用户怎么验证?

用户能不能在结果公布后,自己拿到证据证明开奖是真的随机、真的没被改?还是只能选择相信?

历史回顾 / Legacy Edition

旧版的 玩法骨架

重写之前那一版已经在主网跑了一段时间——它的开奖用的就是方案二(VRF)。 把完整玩法拆三块来看:怎么参与、怎么开奖、奖池怎么分。 这里先只看旧版主网本身:用户链上买票,合约维护票号,售完后合约请求 VRF,VRF 回调后合约开奖并分账。

参与方式 / Participation

一张票 = 一个号

每场("一期")有一个固定的总票数 ticketAll。每张票绑定 1 到 N 之间的一个唯一号码。买票时可以"随机抓号",也可以"手选号码"。

  • 两种买法:随机 / 手选
  • 旧版主网:合约用 Fisher-Yates 维护剩余号
  • 旧版主网:售完即开——不靠定时器、不靠人按按钮
  • 旧版主网:这一期开完,下一期由合约自动起
开奖逻辑 / Draw

VRF + 取模找中奖号

最后一张票卖出的瞬间,合约自动向 Chainlink VRF v2.5 请求一个 256-bit 随机数。VRF 回调后,合约先把随机数对总票数取模,得到 0 ~ ticketAll - 1 的下标;真正查中奖票时再用 winningIndex + 1,所以业务票号仍然是 1 ~ ticketAll,不会出现 0 号票。

  • RNG 100% 链上,没用 block.prevrandao
  • 真实随机数是 uint256,看起来像 0x9f3a...0011 这种 32-byte 大数
  • 旧版代码:winningIndex = randomWord % ticketAll,查票:numberOwner[winningIndex + 1]
  • 开奖延迟 1-3 分钟(等 VRF 回包)
  • 后端通过 Kafka LOTTERY_EVENT 同步
奖池分配 / Distribution

5 路分账,配置可调

一期所有买票钱进入同一个池子。开奖后合约按预设的 5 个比例分给 5 个角色——比例用 basis points 制,5 份必须加起来等于 100%。

  • 分母 DENOM = 100000(万分位精度)
  • 合约里 setRewardConfig() 可调
  • 没有独立代理层——只有"推荐人"
  • 剩余进滚动奖池,可下期叠加
奖池五份具体怎么分
中奖人
winnerReward · 中签号码的拥有者直接拿到。如果地址在黑名单,转入指定 collection 地址
大头
推荐人
referrerReward · 拉中奖人来玩的人拿到的回扣。没绑定推荐人时,进入 referrer collection 地址
分成
平台储备
platformReserve · 进入平台金库(treasury),用于运营、开发、流动性管理
运营
社区奖励
communityBonus · 进入社区基金,用于持币人激励、空投、生态活动
激励
滚动奖池
jackpotPool · 上面四份分完的余数。可被下期 append 倍率玩法叠加放大
滚动

ⓘ Fisher-Yates 只是“怎么从剩余号里公平拿一个号”的通用算法,不是合约专属技术。旧版看起来像“Fisher-Yates 是合约技术”,只是因为旧版完整业务闭环都在智能合约里完成。

旧版完整开奖过程 / Legacy full draw flow

旧版本完整走智能合约:用户买票、票号分配、售完触发、请求 VRF、VRF 回调开奖、奖池分账、下一期启动,都由链上合约状态驱动。

01
用户链上买票
USDT + gas
02
合约分配票号
Fisher-Yates
03
售完即开
sold out
04
合约请求 VRF
Chainlink
05
VRF 回调
randomWord
06
合约算中奖号
% ticketAll
07
合约分账
5-way split
08
下一期启动
contract state
小例子 / Tiny example

5 张票里,为什么最后能找到中奖人?

假设一期只有 5 张票。第 5 张票卖出后,合约请求 VRF。真实 VRF 回来的是一个 256-bit 的 uint256 randomWords[0],通常写成很长的十进制整数,或者 32-byte 十六进制,例如 0x9f3a...0011

为了让取模过程看得懂,下面把这个大数简化成演示值 17。合约算 17 % 5 = 2,这是从 0 开始的下标;再用 2 + 1 = 3 查票号,所以 3 号票中奖。

旧版合约对照:requestRandomWords(numWords: 1) 请求 1 个随机数;回调里存 randomWords[0];开奖时算 randomWord % ticketAll;最后用 numberOwner[winningIndex + 1] 找到中奖地址。
TICKETS SOLD ON-CHAIN 1 2 3 4 5 VRF 0x...11 randomWords[0] = uint256(0x9f3a...0011) demo value = 17 ticketAll = 5 winningIndex = 17 % 5 = 2 ticketNo = winningIndex + 1 = 3 WIN 3
新版 v1.0 实际怎么跑 / Actual v1.0 Flow

用户看到的是买票,系统背后是 8 步

v1.0 的核心目标是:尽可能简单、安全、可验证、公平。用户买票不被链上操作打断,开奖随机数也要能被链上验证。

01
用户充值
USDT / BTC
02
形成平台余额
wallet-service
03
后端分配票号
Fisher-Yates
04
售完触发开奖
game-service
05
请求随机数
VRF_ONLY
06
后端算中奖者
word % soldCount
07
内账派奖
wallet-service
08
用户提现
audit + payout
票号在哪里
v1.0 在后端 game-service / DB 里维护,靠事务、锁和审计日志保证不重复发号
随机数在哪里
生产默认走 VRF_ONLY,随机数来自 Chainlink VRF;local 可切 BACKEND,方便离线跑通
奖金在哪里
v1.0 是平台余额,不是直接打到链上钱包;用户提现时再走审核和链上出款
链上证明什么
VRF_ONLY 证明随机数来源可信;它不证明后端票表天然可信。FULL_CHAIN 则把买票、票号和分账都放进合约
Mode 1 BACKEND

方案一 / 老板自己摇号

用人话讲 · Plain Talk

平台自己用电脑生成一个数当中奖号——但开奖之前就先把这个数的"指纹"挂出来;开奖之后再亮出原始的数。任何人都能算一下指纹对不对得上,平台想偷换数就会穿帮。

像店主自己摇号,但摇之前先把奖号封在透明信封里挂墙上、开奖时当众拆封——号没法换,但你还是得相信"摇号那一下没人帮店主作弊"。

✓ 适合
积分游戏、营销抽奖、内部测试。Gas 为零、UX 最快、用户零门槛。
! 注意
钱在平台账户里。平台跑路、账户被冻结、出款系统故障 = 用户拿不回。

最简单、最快、最便宜。
技术上:服务器调用 SecureRandom.nextLong(),把结果写进数据库,公布。

类比:你去街角小店参加店主办的抽奖。店主在柜台后摇了个奖,把中奖号写在小黑板上。整个流程——抽奖、记录、公布——都在店主的柜台里完成。你信任店主,所以你信任这个结果。

技术现实里,「店主」是后端服务,「柜台」是数据库。生成、存储、公布全部在公司内部系统完成。用户必须信任运营方不会作弊——这是这种方式最大的特点,也是最大的局限。

01
用户下单
POST /draw
02
后端生成随机数
SecureRandom
03
写入数据库
draw_results
04
公布结果
推送 / 查询接口
随机数来自
后端 SecureRandom.nextLong() 生成一个 64-bit signed long,随机值由后端独占
谁能改它
后端能换 randomValue——但 commit-reveal 兜底设计限制了"开奖后改"(见下方)
用户怎么验证
事后比对:拿到公开的 randomValue,本地算 sha256(randomValue + roundId) 看是否等于开奖前公布的 commitment
派奖路径
后端 wallet-service 内账(同 VRF_ONLY)
兜底设计 / commit-reveal off-chain

新版的 BACKEND 不是完全黑盒——开奖之前后端就先公布 commitment = sha256(randomValue + roundId) 并由 KMS 签名;开奖之后再 reveal 真正的 randomValue。任何人都能本地复算 hash 验证 reveal 的随机值是否就是当初承诺的那个——给"信任运营方"加了一道事后可审计的兜底。

兜底的边界:能防"开奖篡改随机值",但不能防"开奖选有利随机值"——后端理论上可以一直生成 randomValue 直到选到对己有利的那个,再公布 commitment。这点要靠场景信任弥补。

✓ 适合

积分游戏、营销抽奖、内部测试、品牌可信度高且金额小的场景。Gas 为零,UX 最丝滑。

✗ 不适合

真金白银、跨地域用户、用户互不认识的 PvP。一旦出现争议,运营方虽有 commit-reveal 自证空间,但仍弱于 VRF。

小例子 / Tiny example

服务器自己摇号,数字长什么样?

SecureRandom.nextLong() 返回的是一个 Java long:64-bit、有正有负,真实值可能长得像 -5128840312744306913L,不是只会返回 17 这种小数字。

假设一期有 5 张票,服务器拿这个 long 做取模。因为 long 可能是负数,代码里要用 Math.floorMod(randomValue, ticketAll),不要直接用 randomValue % ticketAll 当业务票号。

本例:Math.floorMod(-5128840312744306913L, 5) = 2,这是 0 基下标;业务票号仍然是 2 + 1 = 3
TICKETS IN DATABASE 1 2 3 4 5 SERVER nextLong() randomValue = -5128840312744306913L ticketAll = 5 winningIndex = floorMod(randomValue, 5) = 2 ticketNo = winningIndex + 1 = 3 winner = ticket[3].userId WIN 3
看代码 · Java 版 SecureRandom.nextLong() 可直接放进 BackendDrawEngine
import java.security.SecureRandom;

public final class BackendDrawEngine {
  private static final SecureRandom RNG = new SecureRandom();

  public DrawResult draw(long roundId, int ticketAll) {
    if (ticketAll <= 0) {
      throw new IllegalArgumentException("ticketAll must be positive");
    }

    long randomValue = RNG.nextLong();              // 64-bit signed long, may be negative
    int winningIndex = Math.floorMod(randomValue, ticketAll); // 0..ticketAll-1
    int ticketNo = winningIndex + 1;             // business ticket: 1..ticketAll

    return new DrawResult(roundId, randomValue, ticketAll, winningIndex, ticketNo);
  }
}

record DrawResult(
  long roundId,
  long randomValue,
  int ticketAll,
  int winningIndex,
  int ticketNo
) {}
注意:nextLong() 可能返回负数,所以这里用 Math.floorMod。不要写 Math.abs(randomValue) % ticketAll,因为 Long.MIN_VALUE 取绝对值仍然会溢出。
Mode 2 VRF_ONLY

方案二 / Chainlink VRF 出号 + 数学证明

用人话讲 · Plain Talk

Chainlink VRF 是区块链常用的"可验证随机数服务"。平台不是自己在服务器里偷偷摇号,而是让链上的合约去请求 Chainlink VRF;它返回随机数时,会同时带一份数学证明,证明这个数确实是它按这次请求生成的。

小白可以这样理解:普通后端开奖像"店主自己摇号";Chainlink VRF 像"摇号机自带防伪码"。数字出来后,链上合约会先验防伪码,验过才接受这个随机数;验不过就拒绝。

它解决的是随机数来源可信:平台不能提前知道随机数,也不能在随机数出来后偷偷改。但在 VRF_ONLY 里,奖金到账仍然走平台余额和提现流程,不是直接打进链上钱包。

✓ 适合
主网正式产品、奖金中等、要可验证又不想用户买票时付 gas。
! 注意
奖金到账走平台余额——不是直接到链上钱包;提现走平台流程。

VRF 全称是 Verifiable Random Function,意思是"可验证随机函数"。它不是人工审核,也不是平台自己生成随机数;它是一套链上可验证的随机数机制。

这件事分三步:合约发起请求,Chainlink VRF 返回 randomWord 和 proof,合约验证 proof 通过后才把这个随机数认作有效。VRF_ONLY 的关键设计是:只把"出随机数"这一步上链——票务管理、派奖结算都在后端 game-service / wallet-service 完成。用户买票零 gas,开奖结果链上可查。

类比:抽奖时不再让店主自己摇。合约把"我要给这一期开奖"这个请求发给 Chainlink VRF。Chainlink VRF 返回一个数字,同时附上防伪证明。任何人拿到结果,都能确认这个数字是不是这次请求对应出来的。

技术上,Chainlink VRF 是去中心化预言机网络提供的随机数服务。防伪证明更准确说是 ECVRF proof。链上合约只接受能通过 VRF 验证的随机数——不通过验证的,合约自己就拒绝,没有人工干预空间。

01
售完触发
game-service
02
链上请求 VRF
DrawCoordinator
03
VRF 链上回调
number + proof
04
后端算中奖号
word % soldCount
05
钱包内账派奖
wallet-service
随机数来自
VRF 预言机网络,用其私钥+合约请求 hash 派生
谁能改它
VRF 节点要作弊就必须伪造证明——数学上不可行(除非私钥泄露)
用户怎么验证
看链上 requestId / randomWord / 回调交易,再用 randomWord % soldCount 复算中奖票号
派奖路径
后端 wallet-service 内账——奖金以平台余额形式记录,用户按需提现
关键提醒 / VRF still needs a contract

VRF_ONLY 不是“没有智能合约”。Chainlink VRF 必须由链上的 consumer contract 发起请求并接收回调。后端不能直接找 Chainlink 要随机数;后端只能调用或协调 DrawCoordinator,再由合约请求 VRF、接收 randomWord、emit event,最后由 chain-service 监听事件同步给 game-service

✓ 适合

主网生产、有真实金额、要可验证又不想 gas 太高。Chainlink VRF 在 EVM 链上是事实标准。

▲ 注意

仍然要信任 VRF 网络的诚实性(节点 collusion)。响应有 1–3 分钟延迟,开奖瞬间感不如方案一即时。

用户如何验证 / User verification

小白不用理解 ECVRF 公式,按下面流程核对即可。核心思路是:先确认随机数真来自 Chainlink VRF,再用同一个公式复算中奖票号

开奖结果页必须展示:roundIdchainIdDrawCoordinator 合约地址、requestTxHashfulfillTxHashrequestIdrandomWordsoldCount、平台公布的中奖票号。
  1. 打开开奖结果页:复制本期的 roundIdrequestIdrandomWordsoldCount 和两个交易 hash。
  2. 确认请求真的上链:requestTxHash 打开区块浏览器,确认交易调用的是项目公布的 DrawCoordinator,事件里能看到同一个 roundId / requestId
  3. 确认 VRF 已回调:fulfillTxHash 打开区块浏览器,确认回调来自 Chainlink VRFCoordinator,并且写入或 emit 的 requestId / randomWord 与开奖结果页一致。
  4. 自己复算中奖票号:先算 winningIndex = randomWord % soldCount,再算 ticketNo = winningIndex + 1。这里 +1 是因为业务票号从 1 开始。
  5. 对比平台结果:复算出的 ticketNo 必须等于平台公布的中奖票号;如果不一致,说明平台公布结果有问题。
  6. 核对自己的票:在平台订单/票号记录里查自己是否持有这个 ticketNo。如果持有,就应该中奖;如果没持有,就不是本期中奖人。
  7. 保存争议证据:如果发现不一致,保存开奖结果页截图、两个交易 hash、复算公式和自己的票号记录,用这些材料追责。
example: randomWord = 123456789, soldCount = 100
winningIndex = 123456789 % 100 = 89
ticketNo = 89 + 1 = 90

边界要讲清楚:VRF_ONLY 能验证"随机数没被平台编造或偷换";但票表归属和平台内账派奖仍主要依赖后端记录。要连票表和领奖都链上验证,需要方案三 FULL_CHAIN 或方案四草案这类更重的设计。

这版 VRF 跟 旧版 有什么不同
DIFF
维度
旧版主网
新版 VRF_ONLY
差异
随机源
Chainlink VRF v2.5
Chainlink VRF v2.5
票务管理
合约里 Fisher-Yates
后端 game-service
派奖路径
合约 5-way 链上转账
wallet-service 内账
用户买票 gas
每张都要
零(链下买票)
奖金到账
合约直接 USDT 转账
平台余额,按需提现
信任根(派奖)
无需信任运营方
需信任运营方兑付

ⓘ 一句话总结:随机数还是用 Chainlink VRF,但其他全部移到链下。换来的好处是用户买票零 gas、UX 更顺;代价是派奖那一段失去了"无需信任运营方"的强保障。

看代码 · VRF_ONLY 大概长这样 ~ 14 行伪代码
// ── 售完触发 / on sold-out (后端) ──
chainService.requestVrf(roundId)
  └→ DrawCoordinator.requestRandom(roundId)         // 链上调 Chainlink VRF
       └→ VRFCoordinatorV2Plus.requestRandomWords(...)

// ── 1-3 分钟后 VRF 链上回调 (合约) ──
DrawCoordinator.fulfillRandomWords(reqId, [word])
  └→ emit RandomWordsFulfilled(roundId, word)        // 链上事件

// ── chain-service 监听到事件 → Kafka → game-service ──
gameService.onVrfFulfilled(roundId, word):
  winNo  = (BigInteger(word) % soldCount) + 1
  winner = ticket[winNo].uid
  walletService.credit(winner, prize)                // 后端钱包派奖
  publish(game.draw.completed{roundId, winner})
伪代码——略了 VRF subscription 充值、回调 gas 上限、超时重试。真代码:DrawCoordinator.sol + chain-service Kafka consumer + game-service event handler。
Mode 3 FULL_CHAIN 旧版一致

方案三 / 旧版全链闭环

用人话讲 · Plain Talk

方案三改成和旧版一致:用户直接在链上买票,合约自己维护票号、奖池、开奖和分账。售完后合约请求 Chainlink VRF,VRF 回调后合约算中奖票号并直接分账。

小白可以理解成:整个抽奖摊位都搬到区块链上。买票、票号、开奖、奖池怎么分,全都按合约规则走;平台不能在开奖后改票,也不能跳过合约私下分账。

✓ 适合
高价值场、用户都是 web3 老玩家、追求和旧版一样的"全流程链上可验证"。
! 注意
每次买票都是链上交易,用户要钱包、USDT 和 gas;体验和成本都比 BACKEND / VRF_ONLY 重。

方案三就是旧版主网那种纯链上玩法:链上买票、合约分号、售完即开、VRF 回调、合约分账、下一期自动启动

这个模式不追求买票零 gas,而是追求"全流程都在合约里"。票号归属在合约状态里,随机数来自 Chainlink VRF,中奖号和分账都由合约执行。

类比:不是平台先收钱、记票、开奖、再发奖;而是每个用户都直接把买票交易发到合约。合约像一个公开自动运行的抽奖机,谁买了哪张票、什么时候售完、随机数是什么、奖金怎么分,都能在链上查。

技术上:合约用 Fisher-Yates 维护剩余票号;最后一张票卖出时,合约请求 Chainlink VRF;VRF 回调后,合约用 randomWord % ticketAll 得到中奖下标,再用 +1 查业务票号和中奖地址,最后按合约配置分账。

取舍:这是最接近旧版的 FULL_CHAIN,但用户每次买票都要付 gas,也要理解钱包、授权和链上交易确认。

01
用户链上买票
USDT + gas
02
合约分配票号
Fisher-Yates
03
售完请求 VRF
Chainlink
04
VRF 链上回调
randomWord
05
合约开奖分账
winner + split
随机数来自
同方案二,Chainlink VRF v2.5
谁能改它
票号和奖池都在合约状态里;平台不能在 VRF 回来后改票表或改中奖地址
用户怎么验证
查自己的买票交易、合约票号记录、VRF 回调交易、randomWord % ticketAll 结果和链上分账事件
派奖路径
合约按分账规则直接转账——绕过 wallet-service,链上事件可查
✓ 适合

高 TVL 场、Web3 原生玩家、愿意为强可验证性接受 gas 成本和链上操作复杂度的玩法。

✗ 缺点

买票体验最重:每次买票都要链上交易、授权和 gas;合约逻辑复杂,升级和安全审计成本高。

=
方案三和 旧版主网 保持一致
LEGACY
维度
旧版主网
方案三 FULL_CHAIN
结论
随机源
Chainlink VRF v2.5
Chainlink VRF v2.5
买票方式
用户链上买票
用户链上买票
票务管理
合约里 Fisher-Yates
合约里 Fisher-Yates
开奖触发
售完即开
售完即开
分账模式
5-way 一键分给 5 个角色
5-way 一键分给 5 个角色
下一期
合约自动启动
合约自动启动

ⓘ 一句话总结:方案三回到旧版的完整链上闭环;代价是用户买票必须发链上交易。

看代码 · FULL_CHAIN 大概长这样 旧版一致 · 全链闭环
// ── 用户链上买票 / every purchase is on-chain ──
TreasureContract.buy(ticketCount, optionalNumber)
  └→ transfer USDT into contract
  └→ assign ticketNo by Fisher-Yates
  └→ store numberOwner[ticketNo] = buyer

// ── 售完即开 / sold out triggers VRF ──
if remainingTicket == 0:
  requestId = VRFCoordinatorV2Plus.requestRandomWords(...)

// ── VRF 回调后合约开奖和分账 / fulfill on-chain ──
rawFulfillRandomWords(requestId, randomWords):
  randomWord = randomWords[0]
  winningIndex = randomWord % ticketAll
  ticketNo = winningIndex + 1
  winner = numberOwner[ticketNo]
  settleDraw(winner, rewardConfig)        // 中奖人、推荐人、平台、社区、滚动奖池
  launchNextRoundIfNeeded()
伪代码按旧版主网思路写:票号、随机数、中奖者、分账都由合约状态驱动。实际实现还要补充授权、退款、重试 VRF、黑名单和奖池配置。
Mode 4 DRAFT ticketRoot

方案四(草案)/ ticketRoot + Merkle proof + Vault

用人话讲 · Plain Talk

方案四是折中方案:用户买票仍然走后端,所以买票可以零 gas;但售完后,后端必须先把整场票表做成一个不可篡改的指纹 ticketRoot 放到链上,然后才能请求 Chainlink VRF。

小白可以理解成:开奖前先把所有票根拍成一张"防伪名单指纹"交给链上保管。随机数出来后,中奖人拿自己的票根和证明去合约验,验得过,合约金库才付款。

✓ 想解决
保留买票零 gas,同时让平台不能在看到随机数后偷偷改票表。
! 草案边界
这不是旧版全链闭环;票仍由后端卖。它强在"开奖前锁票表 + 链上验票领奖"。

方案四不是替代方案三,而是另一个未来草案:后端售票负责体验,链上 ticketRoot 负责防篡改,Vault 负责奖金可达

关键规则只有一条:合约必须强制 ticketRoot 在请求 VRF 之前上链。如果随机数出来后才允许上传 ticketRoot,后端仍然可以换票表,那就不可信。

票表怎么变成 root:每张票做成一个 leaf,例如 hash(roundId, ticketNo, userId, wallet);同一期内 ticketNo 必须唯一。所有 leaf 组成 Merkle tree,树根就是 ticketRoot。只要任何一张票被改,root 就会变。

中奖怎么验:VRF 回来后得到中奖票号。中奖者提交自己的票据 leaf 和 merkleProof,合约验证这个 leaf 确实属于链上锁定的 ticketRoot,并且票号等于中奖号,才从 Vault 付款到 leaf 里的 wallet

01
后端零 gas 售票
DB tickets
02
票表 root 上链
ticketRoot
03
链上请求 VRF
after root
04
锁定中奖票号
word % soldCount
05
验 proof 后付款
Vault.payout
它和方案三差在哪
方案三链上买票;方案四后端卖票,但售完后把票表指纹锁到链上
用户怎么验证
核对 ticketRoot 是否早于 VRF 请求上链,再用 ticketLeaf + merkleProof 验自己的票是否在锁定票表里
派奖路径
Vault 链上付款——proof 验证通过后,合约直接转给中奖钱包
最大风险
proof 分发、重复 claim 防护、claim 超时、代领 gas、Vault 余额和合约升级权都要单独设计清楚
看代码 · 方案四草案大概长这样 ticketRoot before VRF
// ── 售完触发 / on sold-out (后端) ──
tickets = loadTickets(roundId)
ticketRoot = merkleRoot(tickets)              // leaf = hash(roundId, ticketNo, userId, wallet)
DrawCoordinator.commitTickets(roundId, ticketRoot, soldCount)

// ── root 上链后才能请求 VRF ──
DrawCoordinator.requestRandom(roundId)
  require(ticketRoot[roundId] != 0)
  require(vrfRequestId[roundId] == 0)

// ── VRF 回调锁定中奖票号 ──
fulfillRandomWords(reqId, [word]):
  winNo = (word % soldCount) + 1
  store(roundId, winNo)

// ── 中奖者或代领服务提交 proof,合约验票后付款 ──
claim(roundId, ticketNo, userId, wallet, proof):
  leaf = hash(roundId, ticketNo, userId, wallet)
  require(ticketNo == winNo)
  require(MerkleProof.verify(proof, ticketRoot, leaf))
  require(!claimed[roundId])
  claimed[roundId] = true
  Vault.payout(wallet, prize)
这是草案,不是当前主线。上线前必须补齐 proof 查询、重复 claim 防护、claim 超时、代领授权、Vault 余额预校验、合约升级权限和失败补偿流程。
五维对比 / Side by Side

把三种方式放一起 掂一掂

没有「最好」的答案——只有「适合什么场景」。下表是同一把尺子下的相对比较(条越长越强)。

维度 / Dimension
BACKEND
VRF_ONLY
FULL_CHAIN
可信度
仅靠对运营方的信任
数学证明 + 预言机网络
买票到分账全在合约里
可验证性
可验开奖后未换 seed,不能验开奖前是否挑 seed
链上合约自动验证 VRF proof
票号、VRF、分账都可链上复算
Gas 成本
零(无链上交易)
2 次链上调用 + 订阅费
每次买票都要 gas
UX 友好度
即时开奖,零等待
1–3 分钟出号
买票和开奖都要等链上确认
实现复杂度
一行 Java 代码
合约 + 订阅配置
完整售票、开奖、分账合约
安全性对比 / Security Posture

安全这件事 具体看什么

"哪个最安全"不是一个有意义的问题——安全是一束维度。一种方案可能在「随机不可预测」上 10 分满分、但在「奖金可达性」上是 3 分。把每个维度拆开看,才能挑对场景。

下表比较的是旧版主网 + 三种主方案;方案四仍是草案,暂不纳入安全评级。 = 强保障, = 有兜底但需信任某方, = 这个维度是这个方案的弱点。

安全维度
旧版主网
BACKEND
VRF_ONLY
FULL_CHAIN
随机不可预测
VRF 出号,平台无法预知
SecureRandom,平台知道种子;commit-reveal 限制了"开奖后改"
同 VRF
同 VRF
结果不可篡改
出号到分账全在合约里
commitment 签名 + KMS 保管;只能防"开奖后改",不能完全防"开奖前选有利 seed"
出号在链上,无人可改
买票、票号、出号、分账都在合约里
奖金可达性
合约直转,平台跑路也能拿到
钱全在平台账户,平台跑路 = 用户拿不回
同 BACKEND,依赖平台兑付
合约按分账规则直转,绕过平台账户
第三方依赖
依赖 Chainlink VRF 网络的诚实性
不依赖任何外部预言机
同旧版,依赖 Chainlink
依赖 Chainlink + USDT 合约
私钥泄露影响
合约 owner 私钥泄露 = 可改奖池比例参数
KMS 签名密钥泄露 = 可伪造 commitment 篡改种子
不掌握随机数私钥;wallet-service 私钥泄露仅影响代付
合约 owner 私钥泄露 = 可升级合约改逻辑
抗审查(开奖能否被强行延迟/拒绝)
VRF 节点全部挂 = 无法开奖;但合约自动重试
运营方完全控制开奖触发
VRF 30 min 超时 → 运营人工选择继续等 / 切 BACKEND / 取消
同旧版,依赖 VRF 回调;售完后合约状态驱动开奖

ⓘ 「奖金可达性」是 BACKEND 和 VRF_ONLY 最大的弱点——这两种方案下,奖金以平台余额形式存在,平台跑路或被冻 = 用户拿不回。FULL_CHAIN 走旧版全链闭环,能解决兑付信任问题,但买票体验和合约复杂度都会明显上升。

当前状态 / Implementation Status

哪些能跑,哪些还只是 设计要求

能力 / Capability
当前状态
说明
不能误说
BACKEND
local 可跑
用于本地开发、测试、压测和无外部依赖的完整流程
不要说它和 VRF 一样强可信
VRF_ONLY
v1.0 主网目标
随机数链上可验证,票务和派奖仍在后端 / wallet-service
不要说奖金绕过平台账户
FULL_CHAIN
方案预留
按旧版全链闭环设计:链上买票、合约分号、VRF 开奖、合约分账
不要说它买票零 gas
旧版全链能力
复用设计口径
Fisher-Yates、售完即开、Chainlink VRF、5-way 分账都在合约内完成
上线前必须重新审计合约
技术取舍 / Why not 100% on-chain

为什么不让所有场都用 FULL_CHAIN

看完安全对比,你大概会问:FULL_CHAIN 在「票号链上记录」「奖金可达」「合约分账」几个维度都更强——为什么不所有场都用它?

这里只看技术安全:FULL_CHAIN 的信任边界更硬,但它把风险集中到智能合约和链上依赖里。一旦合约、VRF、代币转账或升级权限出问题,修复成本比后端方案高很多。

1
合约漏洞 / Smart Contract Bugs

一个合约 bug 就可能直接动到钱

FULL_CHAIN 里买票、票号、开奖、分账都在合约里。只要有重入、权限判断、取模边界、数组/映射状态、转账失败处理这类 bug,影响的不是页面显示错误,而是链上资金和开奖结果。

技术后果 BACKEND / VRF_ONLY 出 bug,通常还能暂停、回滚数据库、人工补偿。FULL_CHAIN 出 bug,交易已经确认在链上,除非合约预留暂停/升级/补救路径,否则很难纠正。
2
升级权 / Upgrade Risk

可升级太危险,不可升级也危险

如果合约可升级,owner / multisig / timelock 就是高价值攻击面:私钥泄露或治理失误可能改掉核心逻辑。如果合约完全不可升级,发现漏洞后又很难修。两边都不是免费午餐。

设计要求 至少要有权限分层、multisig、timelock、pause 开关、升级公告窗口和审计记录。否则 FULL_CHAIN 只是把"信任后端"换成"信任合约 owner"。
3
外部依赖 / Oracle & Token Risk

链上也依赖外部系统

FULL_CHAIN 仍依赖 Chainlink VRF 回调、USDT 合约行为、目标链稳定性和 gas 市场。如果 VRF 长时间不回调、USDT transfer 行为异常、链拥堵,合约必须有明确的 timeout、retry、refund 或 pause 策略。

必须写清楚 VRF 超时怎么重试?重试几次?如果一直失败,是否退款?USDT 转账失败怎么处理?这些都要写进合约状态机,不能靠人工临时判断。
4
状态机复杂 / State Machine

状态越多,边界越容易漏

FULL_CHAIN 至少有售票中、售完待请求、等待 VRF、开奖中、已分账、退款、重试、暂停等状态。每个状态都要限制谁能调用什么函数,重复调用、乱序调用、跨期调用都要防住。

常见坑 最后一张票同时被多人抢买、VRF 回调重复、退款和开奖并发、下一期初始化失败、配置切换跨期污染,这些都必须靠合约状态机兜住。
5
资金锁定 / Locked Funds

钱进合约后,补救空间很小

FULL_CHAIN 下奖池资金锁在合约里,只能按合约规则流动。如果分账配置写错、黑名单逻辑误伤、收款地址不可用、某个 token 行为不兼容,资金可能卡在合约状态里。

上线前检查 必须做余额预校验、转账失败处理、紧急暂停、受限 rescue 方案和完整测试网演练。否则"平台不能动钱"会变成"谁都救不了钱"。

所以技术策略不是"永远不用 FULL_CHAIN",而是:低风险场景先用 BACKEND / VRF_ONLY 跑通;FULL_CHAIN 只在合约审计、权限治理、VRF 超时、退款、分账和资金救援方案都完整后,再用于高价值场。

我们怎么选 / Our Strategy

三种都做 ,按场景切换。

引擎层我们抽象了一个统一的 DrawEngine 接口,三种实现作为可插拔的 strategy。运行时通过 Nacos 配置项 one.draw.mode 切换——不改代码、不重启上下文,就能把同一个游戏在不同环境用不同方式开奖。

env × chain 正交:环境(dev / staging / prod)和链类型(Sepolia / BSC / Base / Solana)是两个独立维度,开奖模式跟着场景需要选,不绑死。

one.draw.mode = BACKEND→ 营销活动 / 积分游戏 / dev 环境
one.draw.mode = VRF_ONLY主网默认 / 中等价值 / 标准夺宝
one.draw.mode = FULL_CHAIN→ 高价值 PvP / 链上原生场 / 测试链探索