@@ -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