Skip to content

Commit 6f82494

Browse files
committed
staticaddr: use dp autoloop selector
Replace the recursive full-deposit autoloop selector with a bounded-memory DP implementation in staticaddr/loopin/autoloop_dp.go. The new selector keeps the existing no-change semantics, first finds the best reachable total, then applies the 25 percent band rule so earlier-expiring deposits can win inside that near-optimal range. The DP table is capped at 128 MiB and keeps exact satoshi sums alongside compressed bucket weights, so planning stays memory-bounded without allowing oversized candidates. The compressed weighting now rounds down with a minimum of one bucket, which avoids rejecting valid sums after multiple per-deposit rounding steps while leaving the exact-sum check as the real safety boundary.
1 parent 51e5a71 commit 6f82494

4 files changed

Lines changed: 1014 additions & 165 deletions

File tree

staticaddr/loopin/autoloop.go

Lines changed: 3 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package loopin
33
import (
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

Comments
 (0)