合约部署 / Owner 钱包
负责部署合约、初始化参数、授予/撤销角色和 pause/unpause,不参与日常充值、归集或用户提现。
- 部署后高权限迁 Safe/Timelock。
- 禁止和出款钱包共用。
- 不要长期由开发个人钱包控制。
1U夺宝是一个用 USDT 参与的夺宝/抽奖产品:用户充值、购票,活动售罄后开奖,中奖者领取实物或 USDT 奖励。 区块链在这里不是全部业务系统,而是负责资金进出、可验证开奖、链上签名与资金托管边界。
这不是一个“纯链上游戏”,而是一个平台托管的夺宝产品。链上能力服务于资金、开奖可信度和审计边界。
用户把 USDT 充值到平台分配的链上地址,在平台内用余额购票参与活动。活动售罄或到达开奖条件后,系统按 VRF_ONLY 模式开奖,并把奖品履约、USDT 派奖、退款或提现继续走平台账本和链上执行。
用户从交易所或外部钱包转 USDT 到自己的托管充值地址。
余额进入平台账本后,用户用 1U 单价参与活动。
活动售罄后按 VRF_ONLY 模式开奖。
中奖、退款和提现在账本里确认,再按需要发起链上出款。
用户看到的可用余额来自 wallet-service 的账本;链上 USDT 只是资金真实流入、归集和提现的外部结算层。账本负责用户体验和幂等,链上负责资产最终转移和公开可验证记录。
因此,系统必须同时管理两层事实:平台内部余额,以及链上地址、交易、确认数和钱包分层。
区块链不是替代所有后端服务;它负责外部资产流转、开奖可信增强和资金托管安全边界。
用户充值 USDT 到独立 HD 地址;提现审核通过后从平台 Payout wallet 出款到用户外部地址。
产品口径采用 VRF_ONLY:随机数来源走 VRF,业务账本和履约仍由后端服务承接。
合约部署、运行调用、HD 充值、Gas、Collection、Payout、Operations Hot 和 Cold Wallet 分开控制。
v1.0 主资产,用于充值、购票余额、派奖和提现。
ETH / BNB / TRX 只作为 gas 或 energy 成本,不作为用户主余额。
登录、活动配置、用户余额账本、订单状态、风控审核、客服与运营后台仍由后端服务和数据库承担。
按生产安全边界,每条链、每个环境都要拆分钱包职责。不要用一把私钥贯穿部署、充值、归集、提现和冷储备。
每个 env × chain 至少准备 8 类业务钱包角色。平台资金钱包登记使用 COLLECTION / HOT / WARM / COLD;其中 HOT 通过 label/SOP 区分 Payout Hot 和 Operations Hot。
部署合约、初始化参数、授予/撤销角色;部署后高权限迁 Safe/Timelock。
后端运行时调用合约、VRF request 和链上操作;不能复用 deployer。
派生每个用户每条链的充值地址;归集时用 child key 签名。
给用户充值地址补 native gas;TRON 还要处理 energy/bandwidth。
接收用户充值地址 sweep 来的 USDT;不直接给用户出款。
用户提现审核通过后从这里出款;可以 1 个或 N 个。
保存少量平台运营流动性,给 Payout 补库存、收 Collection 调拨、转利润。
老板/财务长期储备;离线/硬件/多签保管,不接普通后台自动出金。
这部分再进入细节:谁使用、控制方式、主要动作和禁止动作。平台资金钱包登记使用 COLLECTION / HOT / WARM / COLD 作为资金层级,业务角色靠 label、keyId 和 SOP 区分。
负责部署合约、初始化参数、授予/撤销角色和 pause/unpause,不参与日常充值、归集或用户提现。
后端运行时调用合约方法、VRF request 和链上操作,用 runtime operator key 签名。
一套 HD seed 派生大量用户充值地址,不为每个用户单独创建 KMS key。
用户充值地址只有 USDT 时,不能自己支付链上转账成本,需要 gas wallet 补少量 native token 或 TRON energy。
用户 HD 地址里的 USDT 应归集到 Collection Wallet,再按 SOP 调拨到运营热钱包或出款钱包。
用户提现审核通过后,从 Payout Wallet 给用户外部地址出款。Treasury tier 使用 HOT,key label 使用 withdraw-hot-*。
保存少量平台运营资金和短期流动性:接收 Collection 调拨、给 Payout 补库存、向老板/财务转利润、前期接收运营拨款。
Cold wallet 是老板/财务长期储备和大额资金保管层,必须离线/人工保管,不能被普通业务服务直接调用。
上面先讲业务和钱包分工;这里再看运行路径:keyId 从配置进入 sign-service,KMS 只签 digest,HD 地址由 master seed 派生。
chain-service 保存逻辑名,sign-service 才保存 KMS ARN。
chain.evm.BSC.operator-key-id = operator-hot-evm-bsc-1 sign.keys[0].id = operator-hot-evm-bsc-1 sign.keys[0].kms-key-arn = arn:aws:kms:...:key/... sign.keys[0].chain-id = 56
KmsSigner.resolveArn() 按 duobao.sign.keys[] 查找;找不到就失败,不允许默认 key。
private String resolveArn(String keyId) {
return props.keys().stream()
.filter(k -> keyId.equals(k.id()))
.findFirst()
.map(KeyEntry::kmsKeyArn)
.orElseThrow(SIGN_KEY_NOT_FOUND);
}
KmsSigner 传 32-byte digest,使用 MessageType.DIGEST,KMS 不再 hash。
SignResponse resp = kms.sign(SignRequest.builder() .keyId(kmsArn) .messageType(MessageType.DIGEST) .message(SdkBytes.fromByteArray(messageHash)) .signingAlgorithm(ECDSA_SHA_256) .build()); (r, s) = decodeDer(resp.signature()); s = canonicalizeLowS(s); v = recoverV(r, s, messageHash, kmsPublicKey);
EvmSignService 解 raw tx,算 EIP-155 / EIP-1559 preimage,再把签名装回 RLP。
RawTransaction tx = TransactionDecoder.decode(rawTxHex); byte[] preimage = legacy ? TransactionEncoder.encode(tx, chainId) : TransactionEncoder.encode(tx); byte[] digest = Hash.sha3(preimage); SignatureData sig = signer.signEcdsa(keyId, digest); String signedTx = hex(TransactionEncoder.encode(tx, sig));
TronSignService 只签 32-byte txId,返回 r + s + recId。
byte[] txId = parseTxId(txIdHex); SignatureData sig = signer.signEcdsa(keyId, txId); signatureHex = hex(sig.r) + hex(sig.s) + tronRecoveryId(sig.v);
AddressDeriveService 从 master seed 派生 EVM/TRON 用户充值地址。
master = seedProvider.getMasterSeedBytes(); seed = HMAC_SHA512(master, "duobao:hd:seed:v1"); priv = derive(seed, "m/44'/60'/<uid>'"); address = Keys.toChecksumAddress(pubkeyToAddress(priv)); return address, seedVersion, derivationPath, keyId;
这不是单独一个“钱包服务”能做完的事情;区块链能力被拆在账本、链上执行、签名、合约和运维配置里。
wallet-service余额账本、充值入账、提现冻结、充值地址记录、平台资金钱包登记。
不签名、不广播、不保存私钥。
wallet_deposit_address、wallet_treasury
chain-service充值 txHash 验证、构造交易、nonce、广播、确认轮询、VRF 请求和合约调用。
不保存私钥、不直接改用户余额。
operator-key-id、operator-address、RPC、USDT contract。
sign-serviceKMS 在线钱包签名、HD 地址派生、签名审计、keyId 解析。
不审核提现、不判断余额、不广播交易。
duobao.sign.keys[]、KMS ARN、HD seed ciphertext。
VRF_ONLY 模式下的 VRF request、callback 和相关链上事件。
不承担中心化账本职责。
ABI、contract address、ChainAdapter。
读链、查交易、确认数、event log、自动充值扫块。
不作为用户余额唯一事实源。
RPC URL、confirmation depth、block cursor。
提现审核、资金面板、treasury movement SOP、KMS/IAM/CloudTrail。
普通后台不直接控制冷钱包出金。
runbook、IAM role、KMS audit、Safe/multisig SOP。
这些词要在 docs、prototype、代码和运维里保持同一个意思。
业务服务只传 operator-hot-evm-1,不接触真实 KMS ARN 或私钥。
由 sign-service 解析项目 keyId 后传给 KMS。
只放在 sign-service 配置里,用来调用 AWS KMS Sign。
chain-service 用它分配 nonce、广播出款或发起合约调用。
生产由 KMS-wrapped ciphertext 解密,用于派生用户地址。
种子轮换后,旧用户地址仍能按旧版本恢复。
标记用户充值地址如何由 HD seed 派生。
把用户充值地址里的 USDT 转到平台 Collection wallet。
Cold/Warm/大额调拨使用的多签控制能力;按人工 SOP 执行和留痕。
只用于 HD seed 的对称 Encrypt/Decrypt,不用于 asymmetric Sign。
chain-service 只知道 operator-hot-evm-sepolia-1,不知道 KMS ARN,更不知道私钥。sign-service 把这个别名解析成 AWS KMS 可识别的 KeyId,然后调用 KMS 签名。
operator-hot-evm-sepolia-1,业务服务可见的逻辑名。arn:aws:kms:...:key/abcd 或 alias,AWS SDK 请求参数。0xabc...,公开链上地址,用于 nonce、余额、广播。chain-service。.env 或 Nacos。keyId 同时表示出款钱包、Gas 钱包和 HD seed。wallet-service 调出款签名接口。keyId 不是私钥。它是跨服务传递的逻辑名,最终只在 sign-service 内解析到 KMS key ARN。
校验余额、提现白名单、24h 冷却,创建提现单并冻结余额。
读取出款 operator-key-id 和 operator-address,分配 nonce,构造 unsigned tx。
从 duobao.sign.keys[] 找到 KMS ARN。项目 keyId 在这里变成 AWS KeyId。
调用 Sign(MessageType=DIGEST),返回 DER 编码签名。
sign-service 恢复 v 后返回 signed tx;chain-service 广播并回调 wallet-service。
启动阶段只做配置绑定、安全保护、KMS/Local signer 选择、master seed 加载和内部接口注册。
Spring 从 env、Nacos、application.yml 绑定 SignProperties 和 KmsProperties。
SignBootValidator 拒绝非 local profile 使用 local-mode=true。
local 启用 LocalSigner;测试和生产启用 KmsSigner 与 KmsClient。
KMS 模式启动时 decode ciphertext,并调用 kms:Decrypt + EncryptionContext,明文 seed 只留在内存里。
只暴露 /internal/v1/sign/**、/actuator/* 和 error dispatch。
local-mode=true,不依赖 KMS,只能用于开发。local-mode=false,走 KMS 或 LocalStack。local-mode=false,用 AWS KMS 和 workload IAM role。chain-service 启动时读取 operator-key-id 和 operator-address,调用 GET /operator-addr/{keyId}。sign-service 从 KMS public key 恢复地址,返回给 chain-service 比对;不一致时不能进入可出款状态。
chain-service 启动后不直接拥有私钥;它只校验 operator address、注册链适配器,并启动确认轮询。
读取 duobao.chain.evm、duobao.chain.tron、RPC、USDT contract、confirmations、gas policy。
EVM 用 web3j adapter;TRON 用 full-node HTTP adapter;未配置链返回 stub 并 fail fast。
调用 sign-service /operator-addr/{keyId},确认配置地址等于 KMS public key 恢复地址。
BroadcastConfirmationPoller 处理提现;SweepConfirmationPoller 处理归集确认。
处理广播、查询余额、查询交易、VRF request 和 sweep 请求。
生产和测试环境里,sign-service 必须先就绪;chain-service 需要用它校验 operator 地址,之后才允许提现或归集。
application.yml 提供默认值,Nacos 或 env 注入 RPC、KMS ARN、wrapped seed 和 service token。
加载 KmsClient、解 HD master seed、注册 CallerAuthFilter,只开放内部签名接口。
加载 EVM/TRON adapter,然后用 operator-key-id 向 sign-service 查询公开地址。
operator-address 与 KMS public key 恢复地址一致时,提现、广播、sweep 和轮询器进入可用状态。
sign-service 只接内部服务请求。它不审核提现、不查余额、不分配 nonce、不广播交易。
调用方带 X-Caller-Service 和 service token。只读地址查询按内部只读策略放行。
用 duobao.sign.allowed-callers 校验 token,成功后写入 sign.caller。
SignInternalController 按 endpoint 调用 EVM、TRON、derive 或 address service。
生产走 KmsSigner 调 kms:Sign(MessageType=DIGEST);本地走 LocalSigner。
写请求记录 sign_audit。成功审计失败时不释放签名。
解 raw tx、计算 digest、用 payout/operator keyId 签名并返回 signed tx。
校验 32-byte txId,返回 r+s+recId 签名。
按 uid、executionChain、seedVersion 派生地址,返回 address/path/keyId。
按 uid/path/seedVersion 重建 HD child key,校验 fromAddress 后签 unsigned tx。
按用户地址派生 child key,签 32-byte txId,返回 r+s+recId。
从 KMS public key 恢复地址,供 chain-service 启动时比对配置。
/main-addr 等只读地址查询只返回公开地址,不返回 key material。
chain-service 是链上执行层。它接收 wallet-service、game-service 或运维内部请求,然后选择 adapter、sign-service 和 poller 完成链上动作。
sign-service 的合理设计是“签名边界”,不是钱包业务服务。
需要按 endpoint 做授权,不能只要 token 对就能调所有签名接口。
| Endpoint | Allowed caller | Policy | Purpose |
|---|---|---|---|
/derive-addr | wallet-service | derive only | 派生用户充值地址。 |
/evm | chain-service | operator | 平台 Payout / Runtime EVM 签名。 |
/tron | chain-service | operator | TRON 出款签名。 |
/operator-addr/{keyId} | chain-service startup | readiness | 启动时校验 operator-address 与 KMS public key 恢复地址一致。 |
/main-addr | internal read-only | read-only | 返回配置的主地址,不返回私钥材料。 |
/evm-derived | chain-service | sweep only | 用户充值地址 EVM sweep 签名。 |
/tron-derived | chain-service | sweep only | 用户充值地址 TRON sweep 签名。 |
sweep planner、gas station、签名、广播和确认状态推进必须形成闭环;所有用户充值地址归集目标都是 active Collection Wallet,TRON 按 energy/bandwidth 资源模型处理。
chain-service 查询用户充值地址,找到 USDT 超过阈值的地址。
如果用户地址没有 BNB/ETH/TRX,由 gas station 先小额补 gas。
写入 chain_sweep_record,归集目标为 active COLLECTION 钱包。
sign-service 根据 uid/path/seedVersion 派生临时 child key 签名。
chain-service 广播,确认后标记 CONFIRMED,失败进入人工队列。
真实资金上线前,资金闭环、签名权限和 KMS context 必须按最终方案通过门禁。
必须满足 自动充值扫块、SweepPlanner、gas station、chain_sweep_record、derived signing endpoint、EVM/TRON broadcast 和 confirmation poller 形成闭环;sweep 目标固定为 active Collection Wallet。
必须满足 endpoint 级 caller policy:wallet-service 只能 derive,chain-service 只能链上签名和 derived sweep;sign-service 只允许内部网络、service token 和 IP allowlist 访问。
必须满足 HD master seed Encrypt/Decrypt 使用完整 encryption context,包含 duobao-env、duobao-service、duobao-purpose 和 duobao-seed-version;它不用于 KMS Sign。
测试入口按资金托管门禁组织;smoke 只证明主流程,真实资金还必须通过 KMS、HD、资金钱包配置和 sweep 门禁。
| Command / Test | What it proves | Coverage |
|---|---|---|
./duobao-backend/scripts/test-kms-e2e.sh | KMS decrypt/sign path works through LocalStack. | KMS |
./duobao-backend/scripts/test-hd-address-allocation.sh | HD deposit address stability, uniqueness, chain isolation. | HD |
./duobao-backend/scripts/withdraw-wallets.sh check | operator key/address and platform wallet config alignment. | withdraw |
./duobao-backend/scripts/smoke-e2e.sh | Main app flow: login, buy, draw, withdraw review. | app |
POST /internal/v1/chain/evm/sweep | Signs with HD child key, broadcasts, records chain_sweep_record, then poller confirms. | core |