一个 1 USDT 夺宝平台,资金要在用户外部钱包 → HD 充值地址 → 热钱包 → 活动奖池 → 中奖者账本 → 用户提现地址之间走一圈。
每一步都有它的链上设计和安全设计,本文逐章拆解。
A single 1 USDT ticket has to travel through six distinct stages — and every stage exists for a specific blockchain or security reason. This walkthrough explains all six.
数字为 v1.0 设计口径,详见 PRD-V1.0。
Figures reflect the v1.0 design spec — see PRD-V1.0.
用户充值之前,平台先给他生成一个**专属的 HD 派生地址**。 Before a user can deposit, the platform generates a dedicated HD-derived address for them — one per chain.
签名服务 sign-service 持有 master seed,根据 executionChain + uid 给每个用户每条链派生一个独立子地址 —— 这就是 HD wallet 的核心:同一个 master,任意 uid 都能确定性派生出唯一地址,无需在链上注册或预分配。地址落库到表 user_chain_addr,字段 derivation_index 记录派生位置,代码见 AddressDeriveService.java。
外部钱包把 USDT 转到这个 HD 地址 → 链上确认(BSC ~15 块 · TRON ~19 块) → HD 地址余额可见。**但此时账本尚未入账**,平台数据库里这个用户的可用余额仍是 0。这是个故意的设计:不靠被动扫链识别充值,而是要求用户主动声明 —— 见下一章「归集」。
HD 派生树 · HD derivation tree
归集是把 UNCLAIMED 的链上余额变成账本可用余额、并把币归集到热钱包的两阶段操作。 Sweep is the two-stage operation that turns un-credited on-chain balance into spendable ledger balance and moves the actual funds to the hot wallet.
2A · 认领归属 (manual claim):用户从 TG Mini App 提交那笔充值 tx 的 txHash → chain-service 调 POST /internal/v1/chain/deposit/verify-claim 校验「tx 是否存在 / to-addr 是否匹配 HD 地址 / 是否已被 claim」→ 通过则 trust-on-first-use 把 from-address 绑给当前 uid,后续从同一来源的转账自动认领 → wallet-service 写账本,发 Kafka deposit.claimed。
2B · 物理 sweep:HD 地址上的币要归集到热钱包统一调度。sign-service 用各个用户的 HD 子私钥(按 derivation_index 派生)签 sweep tx,同链多用户可以聚合成 batch tx 省 gas;chain-service 广播 → 资金到热钱包。整个过程私钥不出 sign-service 子网。
Claim → Bound 流程 · 6 steps
tx 存在 + to-addr 匹配 HD 地址 + 未被别人 claimuser_account.balance += 充值额 → 发 deposit.claimed 事件derivation_index 派生 · 同链多用户 batch · 私钥不出 subnet1 USDT 的票,上链一次 gas 就吃掉 30-80%。所以购票纯走内部账本,链上零 tx。 A 1 USDT ticket would lose 30-80% of its value to gas fees if every purchase hit the chain. So purchases run purely on an internal ledger — zero on-chain tx.
购票时 wallet-service 在数据库里做一次 double-entry:
user_account.balance -1,platform_account/game_id.balance +1。物理上 USDT 一直在热钱包,逻辑上奖池只是一个账本子账户。所有变动写 audit log + 发 Kafka ticket.bought(下游做风控 / 反刷 / 统计)。
为啥能这么做?因为充值环节已经验证过 用户的钱真的在我们这里(HD 地址 + claim 绑定 + 物理 sweep 到热钱包)。账本层的变动只是表示所有权在用户之间 / 用户和平台之间转移,不涉及真实资金物理移动。等用户提现时,平台再用热钱包发链上 tx 把币转出去 —— 这才是真正的链上 tx 时刻。
上链 vs 不上链 · on-chain vs ledger
wallet-service 是唯一可以扣账户的服务,其他服务通过 Feign + mTLS 调它,不能直接 UPDATE 数据库。idempotency_key 防重复扣款,version_no 乐观锁防并发覆盖,所有变动落 account_event 表用于对账。
开奖是 lottery 平台最敏感的一步,做错就上社交媒体。架构上做成 DrawEngine 接口 + 多个实现,通过 Nacos one.draw.mode 切换。
Drawing is the highest-risk step. We built it as a DrawEngine interface with multiple implementations, switched at runtime via Nacos config one.draw.mode.
三个引擎共享相同的输入输出契约,只是「随机数从哪来 / 可验证性多强」不同。本地开发跑 BACKEND(快 + 简单),测试网和线上跑 VRF_ONLY(链上可验证),FULL_CHAIN 保留扩展接口。Spring @ConditionalOnProperty 在启动时按配置选择实现。
中奖者拿到的奖,v1.0 支持两条出口:① USDT 奖直接进中奖者账本余额(用户提现时走第 06 步出款);② 实物奖进 fulfillment 队列,TG 客服联系用户拿到收件信息,后台运营录入并发货(v1.0 用户端不做地址表单)。BTC 奖(后续开放):触发 BTC 链上 tx(BIP84 P2WPKH)的设计保留为后续蓝本,v1.0 不启用。
3 个开奖引擎 · 3 draw engines
api-service commit hash 并签名公示;开奖时 reveal salt;用 commit + salt 计算中奖号。
所有人可以事后验证,但不上链。适合开发自测,不需要外部依赖。
DrawCoordinator.requestVRF() 上链请求 → Chainlink 返回 random + cryptographic proof → 链上 verify → off-chain decode 出中奖号。
链上可验证的随机源,平台无法操纵。这是 launch 默认。
平台资金分三层:HD 充值地址收钱 → 热钱包日常出款 → 冷钱包长期储备。每一层的攻击面和私钥保护强度都不一样。 Platform funds sit in three tiers: HD deposit addresses receive funds → hot wallet handles daily payouts → cold wallet holds long-term reserves. Each tier has a different attack surface and key-protection strength.
规则:热钱包目标余额 $10k(覆盖 ~24 小时高峰出款)。
超过 $15k → 自动推 $5k 到冷钱包;低于 $5k → 触发运维告警,从冷钱包人工回填。所有推冷 / 回填都要 sign-service 签,推冷自动可执行,回填需要冷钱包持有人多签离线签 —— 这是最后一道防线,攻击者拿到热钱包私钥也动不了冷钱包。
为啥冷钱包不直接合并到热钱包?因为攻击面分层。热钱包在线 → 业务面打穿即可达 → 必须配限额 + KMS + 子网隔离 + 白名单。冷钱包硬件多签 + 物理隔离 → 远程攻击面接近 0 → 即使 sign-service 整个被拿下,冷钱包资金都是安全的。两层目标不同,合并就失去意义。
钱包分层 · wallet tiers
出款是离链上最近的一步,也是攻击者最想打穿的目标。我们在私钥前面放了三道独立防线 —— 业务面被打穿不等于资金能被提走。 Withdrawal is the closest point to on-chain action, and the highest-value target. We placed three independent layers in front of the private key — breaching business services is not the same as draining funds.
流程 6 步:① wallet-service 验额度 + 锁余额(frozen_balance += amount)→ ② 默认全部走运营审核(admin-service 审批 + 二次 TOTP;不分金额,小额自动放行为后续可配置开关)→ ③ wallet-service 通过 mTLS 调 sign-service 让它签 raw tx → ④ sign-service 内部校验 nonce + 限额 + 白名单 + 用 KMS 解锁主私钥完成签名 → ⑤ chain-service 广播到目标链(USDT:BSC / ETH / TRON;BTC 后续开放) → ⑥ 区块确认后 wallet-service 释放 frozen_balance + Bot 推送用户。
三道防线设计的关键是每一道都独立:即使攻击者打穿了业务服务,他还要:伪造客户端 mTLS 证书才能调到 sign-service;即使能调,还要绕过限额白名单;即使绕过限额,还要 KMS 解锁 —— 而 KMS 的主私钥永远不出 KMS,sign-service 持有的只是临时解锁的内存副本。任何一道防线起作用,资金就守得住。
私钥三道防线 · 3 defense layers
mTLS · 服务间双向认证sign-service。
证书由内部 CA 签发,私钥不在业务服务进程里。
限额 + 白名单 · sign-service 内部校验sign-service 还会内部校验:
单笔上限 / 单日累计上限 / toAddr 白名单 / nonce 顺序。
异常请求拒签 + 触发告警。
KMS · 主私钥永不出 KMS从用户外部钱包到中奖者拿到提现,资金经历 6 个阶段。前两步(充值 + 归集)走链 + 账本两层,中间两步(购票 + 开奖)纯账本零链上 tx,后两步(冷热钱包 + 出款)又回到链上由 sign-service 把守。