@@ -3,8 +3,6 @@ package loopin
33import (
44 "context"
55 "errors"
6- "slices"
7- "sort"
86
97 "github.com/btcsuite/btcd/btcutil"
108 "github.com/lightninglabs/loop"
@@ -91,164 +89,8 @@ func selectNoChangeDeposits(maxAmount, minAmount btcutil.Amount,
9189 unfilteredDeposits []* deposit.Deposit , csvExpiry , blockHeight uint32 ,
9290 excludedOutpoints map [string ]struct {}) ([]* deposit.Deposit , error ) {
9391
94- // Filter out deposits that cannot safely participate in a loop-in or
95- // were already allocated to a larger suggestion earlier in the same
96- // planning pass.
97- deposits := make ([]* deposit.Deposit , 0 , len (unfilteredDeposits ))
98- for _ , deposit := range unfilteredDeposits {
99- if _ , ok := excludedOutpoints [deposit .OutPoint .String ()]; ok {
100- continue
101- }
102-
103- swappable := IsSwappable (
104- uint32 (deposit .ConfirmationHeight ), blockHeight ,
105- csvExpiry ,
106- )
107- if ! swappable {
108- continue
109- }
110-
111- if deposit .Value > maxAmount {
112- continue
113- }
114-
115- deposits = append (deposits , deposit )
116- }
117-
118- if len (deposits ) == 0 {
119- return nil , ErrNoAutoloopCandidate
120- }
121-
122- // Sort by value so the search finds large feasible totals early. The
123- // expiry tie-break keeps equal-value deposits deterministic and helps
124- // the later candidate comparison prefer sooner-expiring funds.
125- sort .SliceStable (deposits , func (i , j int ) bool {
126- if deposits [i ].Value == deposits [j ].Value {
127- return deposits [i ].ConfirmationHeight <
128- deposits [j ].ConfirmationHeight
129- }
130-
131- return deposits [i ].Value > deposits [j ].Value
132- })
133-
134- // Precompute a suffix sum so branches that cannot possibly beat the
135- // current best total can be pruned before exploring the expensive part
136- // of the search tree.
137- suffixSums := make ([]btcutil.Amount , len (deposits )+ 1 )
138- for i := len (deposits ) - 1 ; i >= 0 ; i -- {
139- suffixSums [i ] = suffixSums [i + 1 ] + deposits [i ].Value
140- }
141-
142- var (
143- bestSelection []int
144- bestTotal btcutil.Amount
92+ return selectNoChangeDepositsWithMemoryBudget (
93+ maxAmount , minAmount , unfilteredDeposits , csvExpiry ,
94+ blockHeight , excludedOutpoints , autoloopDPMaxMemoryBytes ,
14595 )
146-
147- // betterSelection applies the full-deposit ordering:
148- // 1. highest total not exceeding the target
149- // 2. fewer deposits
150- // 3. earlier-expiring deposits
151- betterSelection := func (candidate []int , total btcutil.Amount ) bool {
152- switch {
153- case total > bestTotal :
154- return true
155-
156- case total < bestTotal :
157- return false
158-
159- case bestSelection == nil :
160- return true
161-
162- case len (candidate ) < len (bestSelection ):
163- return true
164-
165- case len (candidate ) > len (bestSelection ):
166- return false
167- }
168-
169- // Use signed arithmetic here so an expired deposit cannot wrap
170- // the residual-life comparison if height updates race the
171- // earlier swappability filter.
172- left := make ([]int64 , len (candidate ))
173- for i , index := range candidate {
174- left [i ] = deposits [index ].ConfirmationHeight +
175- int64 (csvExpiry ) - int64 (blockHeight )
176- }
177-
178- right := make ([]int64 , len (bestSelection ))
179- for i , index := range bestSelection {
180- right [i ] = deposits [index ].ConfirmationHeight +
181- int64 (csvExpiry ) - int64 (blockHeight )
182- }
183-
184- slices .Sort (left )
185- slices .Sort (right )
186-
187- for i := range left {
188- if left [i ] == right [i ] {
189- continue
190- }
191-
192- return left [i ] < right [i ]
193- }
194-
195- return false
196- }
197-
198- // search explores include/exclude choices. The branch-and-bound checks
199- // are intentionally conservative: they only prune when no combination
200- // below the current node can beat the best known total or tie it with a
201- // smaller deposit count.
202- var search func (index int , total btcutil.Amount , selected []int )
203- search = func (index int , total btcutil.Amount , selected []int ) {
204- if total > maxAmount {
205- return
206- }
207-
208- if total >= minAmount && betterSelection (selected , total ) {
209- bestTotal = total
210- bestSelection = append ([]int (nil ), selected ... )
211- }
212-
213- if index == len (deposits ) {
214- return
215- }
216-
217- maxReachable := total + suffixSums [index ]
218- if maxReachable < bestTotal {
219- return
220- }
221-
222- if maxReachable == bestTotal && bestSelection != nil &&
223- len (selected ) >= len (bestSelection ) {
224-
225- return
226- }
227-
228- // The include branch must not reuse selected's backing array.
229- // Otherwise a later append can leak into the exclude branch
230- // when the slice still has spare capacity.
231- selectedWithIndex := make ([]int , len (selected )+ 1 )
232- copy (selectedWithIndex , selected )
233- selectedWithIndex [len (selected )] = index
234-
235- search (
236- index + 1 , total + deposits [index ].Value ,
237- selectedWithIndex ,
238- )
239- search (index + 1 , total , selected )
240- }
241-
242- search (0 , 0 , nil )
243-
244- if len (bestSelection ) == 0 {
245- return nil , ErrNoAutoloopCandidate
246- }
247-
248- selectedDeposits := make ([]* deposit.Deposit , 0 , len (bestSelection ))
249- for _ , index := range bestSelection {
250- selectedDeposits = append (selectedDeposits , deposits [index ])
251- }
252-
253- return selectedDeposits , nil
25496}
0 commit comments