Skip to content

Commit a07df43

Browse files
committed
feat(tbtcpg): group deposit sweep candidates by vault address
Deposits targeting different vaults cannot be swept in the same transaction. This change groups unswept deposits by their vault address (with case-insensitive normalization) and selects the largest group to maximize per-transaction throughput. Vault=0x0 (nil-vault) deposits that are not selected are logged at Warn level for operator awareness, as they remain recoverable via later sweep cycles, depositor refunds, or reinitializer re-assignment. Also propagates the Vault field through DepositReference and Deposit structs so downstream consumers have access to it.
1 parent c535a07 commit a07df43

3 files changed

Lines changed: 831 additions & 2 deletions

File tree

pkg/tbtcpg/deposit_sweep.go

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"math"
66
"math/big"
77
"sort"
8+
"strings"
89
"time"
910

1011
"github.com/ipfs/go-log/v2"
1112
"go.uber.org/zap"
1213

1314
"github.com/keep-network/keep-core/internal/hexutils"
1415
"github.com/keep-network/keep-core/pkg/bitcoin"
16+
"github.com/keep-network/keep-core/pkg/chain"
1517
"github.com/keep-network/keep-core/pkg/tbtc"
1618
)
1719

@@ -102,6 +104,7 @@ type DepositReference struct {
102104
FundingTxHash bitcoin.Hash
103105
FundingOutputIndex uint32
104106
RevealBlock uint64
107+
Vault *chain.Address
105108
}
106109

107110
// Deposit holds some detailed data about a deposit.
@@ -113,6 +116,7 @@ type Deposit struct {
113116
IsSwept bool
114117
AmountBtc float64
115118
Confirmations uint
119+
Vault *chain.Address
116120
}
117121

118122
// FindDeposits finds deposits according to the given criteria. It always
@@ -266,6 +270,7 @@ func findDeposits(
266270
IsSwept: isSwept,
267271
AmountBtc: convertSatToBtc(float64(depositRequest.Amount)),
268272
Confirmations: confirmations,
273+
Vault: depositRequest.Vault,
269274
},
270275
)
271276
}
@@ -328,7 +333,96 @@ func (dst *DepositSweepTask) FindDepositsToSweep(
328333
return nil, err
329334
}
330335

331-
depositsToSweep := unsweptDeposits
336+
// Group unswept deposits by their target vault address. The
337+
// maxNumberOfDeposits cap is applied inside findDeposits() before
338+
// this grouping step, so the grouping operates on an already-capped
339+
// set. This is an acceptable trade-off: each vault group will
340+
// contain at least one deposit (a valid sweep batch), and because
341+
// vault=0x0 (nil-vault) deposits are rare in practice the
342+
// throughput impact of the cap reducing a minority group is
343+
// negligible.
344+
type vaultGroup struct {
345+
vaultLabel string
346+
deposits []*Deposit
347+
}
348+
349+
groups := make(map[string]*vaultGroup)
350+
351+
for _, deposit := range unsweptDeposits {
352+
var key string
353+
var label string
354+
355+
if deposit.Vault == nil {
356+
key = ""
357+
label = "vault=0x0 (nil)"
358+
} else {
359+
key = strings.ToLower(string(*deposit.Vault))
360+
label = string(*deposit.Vault)
361+
}
362+
363+
g, exists := groups[key]
364+
if !exists {
365+
g = &vaultGroup{vaultLabel: label}
366+
groups[key] = g
367+
}
368+
g.deposits = append(g.deposits, deposit)
369+
}
370+
371+
// Select the vault group with the most deposits. This
372+
// largest-group-first policy maximises the number of deposits
373+
// swept per transaction. A theoretical starvation risk exists for
374+
// minority vault groups when deposits arrive faster than the sweep
375+
// cadence can process them; monitoring via the Warn-level logs
376+
// emitted below for vault=0x0 deposits is the mitigation strategy
377+
// so operators can detect and act on stuck deposits.
378+
var selectedGroup *vaultGroup
379+
for _, g := range groups {
380+
if selectedGroup == nil || len(g.deposits) > len(selectedGroup.deposits) {
381+
selectedGroup = g
382+
}
383+
}
384+
385+
var depositsToSweep []*Deposit
386+
if selectedGroup != nil {
387+
depositsToSweep = selectedGroup.deposits
388+
}
389+
390+
if len(groups) > 1 {
391+
taskLogger.Infof(
392+
"multiple vault groups detected: [%d] groups, selecting [%s] with [%d] deposits",
393+
len(groups),
394+
selectedGroup.vaultLabel,
395+
len(selectedGroup.deposits),
396+
)
397+
398+
for _, g := range groups {
399+
taskLogger.Infof(
400+
"vault group [%s]: [%d] deposits",
401+
g.vaultLabel,
402+
len(g.deposits),
403+
)
404+
}
405+
406+
// Vault=0x0 deposits that are not selected for sweeping are
407+
// not at risk of fund loss. Three recovery paths exist:
408+
// 1. The deposit can still be swept to the wallet's Bank
409+
// balance in a later sweep cycle (normal sweep, delayed).
410+
// 2. After the deposit locktime expires, the depositor can
411+
// request a refund on-chain.
412+
// 3. A reinitializer can re-assign the deposit to a
413+
// different vault, making it eligible for a future sweep.
414+
// The Warn-level log below flags these deposits for operator
415+
// awareness and manual follow-up.
416+
if nilGroup, ok := groups[""]; ok {
417+
for _, deposit := range nilGroup.deposits {
418+
taskLogger.Warnf(
419+
"vault=0x0 deposit [%s] with wallet PKH [0x%x] requires manual follow-up",
420+
deposit.DepositKey,
421+
deposit.WalletPublicKeyHash,
422+
)
423+
}
424+
}
425+
}
332426

333427
if len(depositsToSweep) == 0 {
334428
return nil, nil
@@ -357,6 +451,7 @@ func (dst *DepositSweepTask) FindDepositsToSweep(
357451
FundingTxHash: deposit.FundingTxHash,
358452
FundingOutputIndex: deposit.FundingOutputIndex,
359453
RevealBlock: deposit.RevealBlock,
454+
Vault: deposit.Vault,
360455
}
361456
}
362457

0 commit comments

Comments
 (0)