fix(v2): crypto 预留键改按地址(对齐链上匹配维度)——防共享地址跨账户同额碰撞

- allocateAmount 中解析地址后改用 address+'/'+amount 作为预留键,而非 accountID
- 同一地址的不同账户不能同时占用同金额,链上唯一性保证对齐 Query 的 to==addr 维度
- 新增 GetReserved() 测试方法暴露预留表副本,验证键格式
- 新增 TestReservationKeyByAddress 测试:两个账户共享地址,验证预留键基于地址

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-10 14:43:21 +08:00
parent fbd3bd6f36
commit 527de1c93e
2 changed files with 85 additions and 2 deletions
+20 -2
View File
@@ -51,7 +51,7 @@ type Provider struct {
now func() time.Time
mu sync.Mutex
reserved map[string]time.Time // "<accountID>/<amount>" → 预留到期(canonical AmountRecentlyUsed 的进程内等价)
reserved map[string]time.Time // "<address>/<amount>" → 预留到期(链上匹配维度,对齐 Query 的 to==addr)(canonical AmountRecentlyUsed 的进程内等价)
}
type Option func(*Provider)
@@ -99,9 +99,27 @@ func (p *Provider) apiKey(accountID string) string {
return os.Getenv("TRONGRID_API_KEY")
}
// GetReserved 仅用于测试,返回预留表副本。
func (p *Provider) GetReserved() map[string]time.Time {
p.mu.Lock()
defer p.mu.Unlock()
m := make(map[string]time.Time, len(p.reserved))
for k, v := range p.reserved {
m[k] = v
}
return m
}
// allocateAmount 移植 canonical pay/service.go:随机尾数 [1,tailMax] + 冷却预留,
// 保证同(地址,金额)在冷却窗内唯一——迟到付款绝不可能匹配到新单。64 次重试。
// 预留键基于地址(链上匹配维度),防止共享地址的不同账户产生同金额碰撞。
func (p *Provider) allocateAmount(accountID string, base int64) (amount, tail int64, err error) {
// 解析地址(同 Query 逻辑,作为链上匹配维度的真相源)
addr, err := p.address(accountID)
if err != nil {
return 0, 0, err
}
p.mu.Lock()
defer p.mu.Unlock()
now := p.now()
@@ -116,7 +134,7 @@ func (p *Provider) allocateAmount(accountID string, base int64) (amount, tail in
return 0, 0, rerr
}
amt := base + t
key := accountID + "/" + strconv.FormatInt(amt, 10)
key := addr + "/" + strconv.FormatInt(amt, 10)
if _, used := p.reserved[key]; used {
continue
}