A Visual Deep Dive · 资金流转可视化拆解

一笔 1 USDT
走完的 6 段路 FROM DEPOSIT TO PRIZE PAYOUT · A 1 USDT TICKET'S SIX-STAGE JOURNEY

一个 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.

6
资金流阶段 / Stages
3
链范围 BSC / ETH / TRON
3
开奖引擎 / Draw Modes
$1
单票价格 / Ticket

数字为 v1.0 设计口径,详见 PRD-V1.0
Figures reflect the v1.0 design spec — see PRD-V1.0.

向下滚动 · scroll to explore
资金流转 6 阶段 · 6-STAGE OVERVIEW
01
充值外部钱包 → HD 地址
02
归集Claim + Sweep → 热钱包
03
购票账本 → 活动奖池
04
开奖奖池 → 中奖者账本
05
冷热钱包阈值检查 + 推冷钱包
06
出款热钱包 → 用户链上钱包
Chapter 01 · 充值

每用户每条链,
一个独立 HD 地址 EVERY USER GETS THEIR OWN HD ADDRESS — PER CHAIN

用户充值之前,平台先给他生成一个**专属的 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。这是个故意的设计:不靠被动扫链识别充值,而是要求用户主动声明 —— 见下一章「归集」。

Key Insight · 关键洞察 充值 ≠ 入账。链上 HD 地址收到 USDT 不代表平台账本里你有钱花。这套两阶段设计 (HD 派生 + manual claim) 让平台不依赖扫链识别归属,扛重组、扛批量灌水攻击。
Deposit ≠ credit. Funds arriving at the HD address don't mean your in-app balance went up. The two-stage design (HD derivation + manual claim) avoids dependence on passive chain scanning.

HD 派生树 · HD derivation tree

🔒
MASTER SEED sign-service · KMS · isolated subnet
BSC
U10x7a3e...8d2
U20x9c1f...4a8
U30xb529...c31
TRON
U4TX9fK...n2E
U5TM4a8...pL3
U6TZ1b4...wR7
// Per-user-per-chain HD derivation
deriveAddress(executionChain, uid)
  // master seed never leaves sign-service
  return subAddr(master, executionChain, uid)
Chapter 02 · 归集

Claim 验证 +
资金物理归集 VERIFY THE CLAIM, BIND THE SOURCE, THEN MOVE THE MONEY

归集是把 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 的 txHashchain-servicePOST /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 子网

Why manual claim · 为什么要 manual claim 被动扫链识别归属(passive scanner)看似简单,但要扛 trace 投毒、链重组、多签 from-addr 多义性 —— 工程复杂度极高。Manual claim 把识别责任交给用户(粘 txHash),平台只做验证,代码量少、出问题边界清晰。这是 TD-16 的决策;passive scanner 列为 TD-24 Phase 2 备选。

Claim → Bound 流程 · 6 steps

1
用户外部钱包发起转账
USDT → 自己的 HD 充值地址(链上 tx 上链确认)
2
TG Mini App 提交 txHash 申请认领
用户复制充值 tx 哈希,在「我的钱包 → 充值」粘贴 → 提交
3
chain-service verify-claim
校验 tx 存在 + to-addr 匹配 HD 地址 + 未被别人 claim
4
trust-on-first-use 绑定
from-address 第一次出现就绑给当前 uid,后续从同一来源自动认领
5
wallet-service 写账本 + Kafka
user_account.balance += 充值额 → 发 deposit.claimed 事件
6
sign-service 签 sweep tx → 热钱包
HD 子私钥按 derivation_index 派生 · 同链多用户 batch · 私钥不出 subnet
Chapter 03 · 购票

为啥购票
不上链 WHY TICKET BUYS DON'T HIT THE CHAIN

1 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 时刻。

Design Principle · 设计原则 Lottery 平台核心矛盾:大量小额内部转账 vs 链上 gas 经济学。解法是把小额转账留在账本,大额边界穿越(充值入金、提现出金)上链。这套设计在 v1.0 PRD 第 7 节有完整账本表结构。
The economic core of a lottery platform: many small internal transfers vs prohibitive on-chain gas. Solution: keep small transfers in the ledger, only cross the chain boundary at deposit and withdrawal.

上链 vs 不上链 · on-chain vs ledger

假设上链
买 1 张票 → 链上 tx
If every ticket buy was a chain tx
  • BSC gas $0.3 - $0.8 / 笔
  • 票价 $1 → gas 吃掉 30 - 80%
  • 用户体验:确认要等 15+ 块
  • 奖池里的钱也要上链组合 → tx 更多
  • 合约风险面变大
实际方案
买票走账本 · off-chain
Our actual design — internal ledger only
  • 0 gas, 0 链上 tx
  • double-entry + audit log + Kafka
  • 毫秒级确认 (DB transaction)
  • idempotency_key + version_no 防并发
  • 大额边界穿越才上链
账本规则 · ledger rules wallet-service唯一可以扣账户的服务,其他服务通过 Feign + mTLS 调它,不能直接 UPDATE 数据库。idempotency_key 防重复扣款,version_no 乐观锁防并发覆盖,所有变动落 account_event 表用于对账。
Chapter 04 · 开奖

DrawEngine
三种模式插件化 THREE PLUGGABLE DRAW ENGINES — SWITCHED VIA NACOS

开奖是 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 不启用。

为什么要插件化 · why pluggable 线上一定要 VRF 才能让玩家相信开奖公平;但开发自测时启动 Chainlink VRF 等链上回调太慢。插件化让两边都能跑同一套代码,环境切换不改业务逻辑。这也是 launch 前必须能上 testnet 验证 VRF 路径的原因。

3 个开奖引擎 · 3 draw engines

BACKEND
LOCAL · 开发自测
commit-reveal off-chain。开奖前 api-service commit hash 并签名公示;开奖时 reveal salt;用 commit + salt 计算中奖号。 所有人可以事后验证,但不上链。适合开发自测,不需要外部依赖。
VRF_ONLY
TESTNET / STAGING / PROD
Chainlink VRFDrawCoordinator.requestVRF() 上链请求 → Chainlink 返回 random + cryptographic proof → 链上 verify → off-chain decode 出中奖号。 链上可验证的随机源,平台无法操纵。这是 launch 默认。
FULL_CHAIN
预留 · v1.0 不启用
全链上抽奖逻辑。把中奖号计算、奖金分配整个流程都放到合约里。v1.0 留接口不启用,Phase 2 视用户对透明度的需求再开。
Chapter 05 · 冷热钱包

热钱包不囤钱,
冷钱包不联网 HOT WALLET STAYS LEAN, COLD WALLET STAYS OFFLINE

平台资金分三层: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 整个被拿下,冷钱包资金都是安全的。两层目标不同,合并就失去意义。

Operational practice · 运营实践 热钱包阈值告警接入运维 oncall(Grafana + PagerDuty),低于 $5k 必须 30 分钟内人工干预。冷钱包 m-of-n 多签持有人是公司高管(法律 + 财务 + CTO),任何回填都需要 ≥ m 人到场签名 —— 业务上是不便,但这就是 cold wallet 该有的不便。

钱包分层 · wallet tiers

📥
HD 充值地址
每用户每链一个 收钱用,余额按规则 sweep 进热钱包。被攻击影响面:单个用户一笔充值。
🔥
热钱包
目标 $10k · 上限 $15k 日常出款的工作账户,在线。sign-service KMS 签名 + 限额 + 白名单守护。被攻击影响面:不超过 $15k。
❄️
冷钱包
m-of-n 多签 · 离线 长期储备 + 历史利润。硬件钱包 + 物理隔离 + 多人多签。被攻击影响面:近 0。
Chapter 06 · 出款

私钥三道防线 THREE LAYERS OF DEFENSE BEFORE A PRIVATE KEY EVER SIGNS

出款是离链上最近的一步,也是攻击者最想打穿的目标。我们在私钥前面放了三道独立防线 —— 业务面被打穿不等于资金能被提走。 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 通过 mTLSsign-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 持有的只是临时解锁的内存副本。任何一道防线起作用,资金就守得住。

Threat model · 威胁模型 我们假设业务服务(gateway / api / pay)迟早会被打穿(SQL 注入、依赖漏洞、内鬼)—— 这个前提下设计才有意义。三道防线对应:横向移动 (mTLS) / 业务逻辑绕过 (限额白名单) / 私钥本身 (KMS)。任何一道都独立足以挡住单一类型攻击。

私钥三道防线 · 3 defense layers

01
mTLS · 服务间双向认证
业务面被打穿后,攻击者要伪造 mTLS 客户端证书才能调到 sign-service。 证书由内部 CA 签发,私钥不在业务服务进程里。

If business services are compromised, the attacker still needs to forge an mTLS client certificate (signed by internal CA, with private key not in business processes).
02
限额 + 白名单 · sign-service 内部校验
即使 mTLS 被绕过,sign-service 还会内部校验: 单笔上限 / 单日累计上限 / toAddr 白名单 / nonce 顺序。 异常请求拒签 + 触发告警。

Even past mTLS, sign-service internally enforces per-tx cap / daily cap / toAddr whitelist / nonce order.
03
KMS · 主私钥永不出 KMS
热钱包主私钥永远不离开 KMS。每次签名 KMS 解锁后,sign-service 持有的是临时内存副本(签完即丢)。 即使主机被入侵也抓不到长期可用的密钥

The master private key never leaves KMS. Each signing operation gets a transient in-memory copy that's discarded after use.
Summary · 总览

6 阶段一图看完 THE FULL FLOW IN ONE PICTURE

从用户外部钱包到中奖者拿到提现,资金经历 6 个阶段。前两步(充值 + 归集)走链 + 账本两层,中间两步(购票 + 开奖)纯账本零链上 tx,后两步(冷热钱包 + 出款)又回到链上由 sign-service 把守。

01
充值
DEPOSIT
02
归集
SWEEP
03
购票
BUY TICKET
04
开奖
DRAW
05
冷热钱包
REBALANCE
06
出款
WITHDRAW