Files
pangolin/docs/superpowers/plans/2026-07-09-pay-address-pool.md
T
wangjia 5055afdbd7 docs(pay): 地址池收款模型 plan(折中方案,#34/34A)
从每单唯一 HD 地址改为地址池复用(池=峰值并行度、复用摊薄激活/归集)。规则:最小编号
idle 绑单/无 idle 派生/15min 超时释放/同用户单订单。核心安全点:地址复用迟到付款错配
→ 匹配按 tx+金额+时间戳(晚于建单)、每单唯一金额、孤儿付款记录。双产物 md+html,登记 index。
待用户确认后执行(tier-1)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:44:22 +08:00

5.8 KiB
Raw Blame History

pangolin-pay 地址池收款模型(折中方案,取代"每单唯一 HD 地址")

#34/34A 收款模型修订。Context:每单唯一 HD 地址 → 每地址激活(~1 TRX)+ 归集(每笔一次转账)成本随 订单总数线性增长(#34 暴露的严峻成本)。改为地址池复用:池大小 = 峰值并行度(初始 10,不够动态派生), 地址复用摊薄激活/归集;仍是多地址,避免并发订单金额撞、对账精确。这是"单地址(省钱但撞单)"与 "每单唯一(精确但贵)"的折中。

归集成本从"O(订单数)"降到"O(池大小)";激活成本一次性(每个池地址激活一次即长期复用)。

本 plan = 用户定的规则 + 补齐的安全点(核心:地址复用的迟到付款错配)。已实现的 Phase B(钱包派生)、 tron(TronGrid 读到账/建/广播/签名)、cmd/sweep 复用;主要改 store / pay(建单)/ watcher(匹配)。

设计(规则 + 安全点)

  • 地址池:HD 派生地址(路径 m/44'/195'/0'/0/i,与现有一致),各有状态 idle | busy。初始池 10。
  • 建单:绑最小编号的 idle 地址 → 置 busy;无 idle → 派生下一个新地址(编号递增)入池并绑之。
  • 每单唯一金额 必需:基准价 + 唯一尾数(如按订单序号映射的微分),保证当前活跃订单之间金额不撞, 且复用地址上"旧订单迟到付款"不会误配"新订单"。
  • 15 分钟超时:超时 → expired → 地址回 idle(可选冷却期再重绑,降低迟到错配)→ 提示用户重建订单、用新订单地址付款。
  • 付款完成:watcher 确认到账 → 订单 paid → 地址回 idle注意:此时地址里仍有 USDT(待归集); idle 只表示"可重绑",不代表钱清了。
  • 匹配规则(核心):watcher 找到该地址、金额 == 该单唯一金额(或 ≥,带容差)、且到账区块时间晚于订单创建 的 TRC20 转入 tx → 匹配。绝不按余额、不只按地址(地址会累积余额、会复用)。
  • 同用户单订单:同一用户同时只能有一个活跃(pending)订单。
  • 孤儿付款:到账 tx 匹配不到任何活跃订单(迟到 / 欠额 / 误差)→ 记 orphan_payments,人工对账/补发。
  • 归集:定期扫池内有余额的地址 → 冷钱包(能量租赁 ~$0.11/笔);池 ~10 地址,归集笔数有界; 归集与 busy 无关(随时可扫,不影响待付订单——到账是未来的独立 tx)。
  • 池增长上限(可选):无空闲就新建,设上限防滥用无限增长(超限则排队/拒单)。

Phase A — 数据模型改造

  • addresses 表:id / addr_index(唯一) / address / state('idle'|'busy') / activated(bool) / created_at;初始不预建,按需派生入池。
  • pay_orders 改:加 user_ref(同用户单订单约束用)、address_id(绑定的池地址)、expect_amount(唯一金额,micro-USDT)、matched_tx_id;保留 status/created_at/expires_at。
  • orphan_payments 表:tx_id(唯一) / address / value / block_ts / created_at / handled(bool)
  • 复用现有 addr_cursor(HD 派生游标)派生新池地址。迁移脚本 + 索引(status、address、user_ref)。

Phase B — 建单(pay 服务)

  • CreateOrder(userRef, sku):① 校验同用户无活跃订单(有则返回现有/拒);② 原子选最小编号 idle 地址(事务 + 行锁)置 busy;③ 无 idle → NextAddrIndex 派生新地址(AddressFromAccountXpub)入池并绑;④ 分配唯一金额;⑤ 写 pending 单(TTL 15min)。
  • 唯一金额分配:基准金额 + 唯一尾数,保证与当前所有活跃订单不撞(可用订单序号 → 尾数微分,micro-USDT 级);记录基准+尾数便于展示。
  • GetOrder;释放地址的公共方法 releaseAddress(addrID)(paid/expired 复用)。

Phase C — watcher 改造(匹配 + 释放 + 孤儿)

  • 匹配:对每个 pending 单,查其绑定地址的 TRC20 转入;筛 value 命中唯一金额(==,或 ≥ 带小容差)block_ts > order.created_atpaid + 记 matched_tx_id;释放地址 → idle
  • 超时:pending 过期 → expired + 释放地址 → idle(可选冷却期后才允许重绑)。
  • 孤儿:遍历池地址的到账 tx,凡匹配不到任何活跃订单的转入 → 写 orphan_payments(去重 by tx_id)。
  • 幂等(tx_id 全局去重,含 orphan)、崩溃恢复(从 DB pending + 池状态重建)。TronGrid 需能取到账时间戳(block_timestamp)用于时间过滤。

Phase D — 归集(复用 cmd/sweep)

  • 扫池内有余额地址 → 冷钱包;接能量租赁(先手动 runbook,后续自动化);归集与订单状态解耦。
  • 归集不清空 busy 语义;只搬余额。runbook 写清"哪些地址、多久扫一次、能量怎么租"。

Phase E — 测试 + 真链验证

  • 单测:并发建单不抢同址(原子性)、地址复用后旧订单迟到付款不误配新订单(唯一金额+时间戳)、唯一金额不撞、超时释放、孤儿记录、幂等、崩溃恢复、同用户单订单约束。
  • 真链:两并发订单各绑不同地址各自付款到 paid;一单超时释放后新单复用同址,再对旧地址补一笔"旧金额"付款 → 应成孤儿、不误配新单;归集池地址到冷钱包。

Verification / 判据

  • go test ./...(store/pay/watcher 新逻辑全绿,重点覆盖复用错配)。
  • 真链:并发 + 复用 + 迟到付款三场景表现正确;归集笔数 = 有余额的池地址数(有界)。
  • 成本:激活一次/地址、归集 O(池大小);对小额高频订单不再被激活费线性拖累。

不在本轮 / 取舍

  • GasFree(#35,免 TRX 但每笔固定 1 USDT,更适合少地址批量,另议)。
  • 能量租赁自动化(先手动)。
  • 池增长上限/排队(先不设,或设一个宽上限)。
  • 发货侧(独角数卡 + /internal/codes/mint)另排。