• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 13566028875

27 Feb 2025 12:09PM UTC coverage: 49.396% (-9.4%) from 58.748%
13566028875

Pull #9555

github

ellemouton
graph/db: populate the graph cache in Start instead of during construction

In this commit, we move the graph cache population logic out of the
ChannelGraph constructor and into its Start method instead.
Pull Request #9555: graph: extract cache from CRUD [6]

34 of 54 new or added lines in 4 files covered. (62.96%)

27464 existing lines in 436 files now uncovered.

101095 of 204664 relevant lines covered (49.4%)

1.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.42
/sweep/aggregator.go
1
package sweep
2

3
import (
4
        "sort"
5

6
        "github.com/btcsuite/btcd/btcutil"
7
        "github.com/btcsuite/btcd/wire"
8
        "github.com/lightningnetwork/lnd/fn/v2"
9
        "github.com/lightningnetwork/lnd/input"
10
        "github.com/lightningnetwork/lnd/lntypes"
11
        "github.com/lightningnetwork/lnd/lnwallet"
12
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
13
)
14

15
// UtxoAggregator defines an interface that takes a list of inputs and
16
// aggregate them into groups. Each group is used as the inputs to create a
17
// sweeping transaction.
18
type UtxoAggregator interface {
19
        // ClusterInputs takes a list of inputs and groups them into input
20
        // sets. Each input set will be used to create a sweeping transaction.
21
        ClusterInputs(inputs InputsMap) []InputSet
22
}
23

24
// BudgetAggregator is a budget-based aggregator that creates clusters based on
25
// deadlines and budgets of inputs.
26
type BudgetAggregator struct {
27
        // estimator is used when crafting sweep transactions to estimate the
28
        // necessary fee relative to the expected size of the sweep
29
        // transaction.
30
        estimator chainfee.Estimator
31

32
        // maxInputs specifies the maximum number of inputs allowed in a single
33
        // sweep tx.
34
        maxInputs uint32
35

36
        // auxSweeper is an optional interface that can be used to modify the
37
        // way sweep transaction are generated.
38
        auxSweeper fn.Option[AuxSweeper]
39
}
40

41
// Compile-time constraint to ensure BudgetAggregator implements UtxoAggregator.
42
var _ UtxoAggregator = (*BudgetAggregator)(nil)
43

44
// NewBudgetAggregator creates a new instance of a BudgetAggregator.
45
func NewBudgetAggregator(estimator chainfee.Estimator,
46
        maxInputs uint32, auxSweeper fn.Option[AuxSweeper]) *BudgetAggregator {
3✔
47

3✔
48
        return &BudgetAggregator{
3✔
49
                estimator:  estimator,
3✔
50
                maxInputs:  maxInputs,
3✔
51
                auxSweeper: auxSweeper,
3✔
52
        }
3✔
53
}
3✔
54

55
// clusterGroup defines an alias for a set of inputs that are to be grouped.
56
type clusterGroup map[int32][]SweeperInput
57

58
// ClusterInputs creates a list of input sets from pending inputs.
59
// 1. filter out inputs whose budget cannot cover min relay fee.
60
// 2. filter a list of exclusive inputs.
61
// 3. group the inputs into clusters based on their deadline height.
62
// 4. sort the inputs in each cluster by their budget.
63
// 5. optionally split a cluster if it exceeds the max input limit.
64
// 6. create input sets from each of the clusters.
65
// 7. create input sets for each of the exclusive inputs.
66
func (b *BudgetAggregator) ClusterInputs(inputs InputsMap) []InputSet {
3✔
67
        // Filter out inputs that have a budget below min relay fee.
3✔
68
        filteredInputs := b.filterInputs(inputs)
3✔
69

3✔
70
        // Create clusters to group inputs based on their deadline height.
3✔
71
        clusters := make(clusterGroup, len(filteredInputs))
3✔
72

3✔
73
        // exclusiveInputs is a set of inputs that are not to be included in
3✔
74
        // any cluster. These inputs can only be swept independently as there's
3✔
75
        // no guarantee which input will be confirmed first, which means
3✔
76
        // grouping exclusive inputs may jeopardize non-exclusive inputs.
3✔
77
        exclusiveInputs := make(map[wire.OutPoint]clusterGroup)
3✔
78

3✔
79
        // Iterate all the inputs and group them based on their specified
3✔
80
        // deadline heights.
3✔
81
        for _, input := range filteredInputs {
6✔
82
                // Get deadline height, and use the specified default deadline
3✔
83
                // height if it's not set.
3✔
84
                height := input.DeadlineHeight
3✔
85

3✔
86
                // Put exclusive inputs in their own set.
3✔
87
                if input.params.ExclusiveGroup != nil {
6✔
88
                        log.Tracef("Input %v is exclusive", input.OutPoint())
3✔
89
                        exclusiveInputs[input.OutPoint()] = clusterGroup{
3✔
90
                                height: []SweeperInput{*input},
3✔
91
                        }
3✔
92

3✔
93
                        continue
3✔
94
                }
95

96
                cluster, ok := clusters[height]
3✔
97
                if !ok {
6✔
98
                        cluster = make([]SweeperInput, 0)
3✔
99
                }
3✔
100

101
                cluster = append(cluster, *input)
3✔
102
                clusters[height] = cluster
3✔
103
        }
104

105
        // Now that we have the clusters, we can create the input sets.
106
        //
107
        // NOTE: cannot pre-allocate the slice since we don't know the number
108
        // of input sets in advance.
109
        inputSets := make([]InputSet, 0)
3✔
110
        for height, cluster := range clusters {
6✔
111
                // Sort the inputs by their economical value.
3✔
112
                sortedInputs := b.sortInputs(cluster)
3✔
113

3✔
114
                // Split on locktimes if they are different.
3✔
115
                splitClusters := splitOnLocktime(sortedInputs)
3✔
116

3✔
117
                // Create input sets from the cluster.
3✔
118
                for _, cluster := range splitClusters {
6✔
119
                        sets := b.createInputSets(cluster, height)
3✔
120
                        inputSets = append(inputSets, sets...)
3✔
121
                }
3✔
122
        }
123

124
        // Create input sets from the exclusive inputs.
125
        for _, cluster := range exclusiveInputs {
6✔
126
                for height, input := range cluster {
6✔
127
                        sets := b.createInputSets(input, height)
3✔
128
                        inputSets = append(inputSets, sets...)
3✔
129
                }
3✔
130
        }
131

132
        return inputSets
3✔
133
}
134

135
// createInputSet takes a set of inputs which share the same deadline height
136
// and turns them into a list of `InputSet`, each set is then used to create a
137
// sweep transaction.
138
//
139
// TODO(yy): by the time we call this method, all the invalid/uneconomical
140
// inputs have been filtered out, all the inputs have been sorted based on
141
// their budgets, and we are about to create input sets. The only thing missing
142
// here is, we need to group the inputs here even further based on whether
143
// their budgets can cover the starting fee rate used for this input set.
144
func (b *BudgetAggregator) createInputSets(inputs []SweeperInput,
145
        deadlineHeight int32) []InputSet {
3✔
146

3✔
147
        // sets holds the InputSets that we will return.
3✔
148
        sets := make([]InputSet, 0)
3✔
149

3✔
150
        // Copy the inputs to a new slice so we can modify it.
3✔
151
        remainingInputs := make([]SweeperInput, len(inputs))
3✔
152
        copy(remainingInputs, inputs)
3✔
153

3✔
154
        // If the number of inputs is greater than the max inputs allowed, we
3✔
155
        // will split them into smaller clusters.
3✔
156
        for uint32(len(remainingInputs)) > b.maxInputs {
3✔
UNCOV
157
                log.Tracef("Cluster has %v inputs, max is %v, dividing...",
×
UNCOV
158
                        len(inputs), b.maxInputs)
×
UNCOV
159

×
UNCOV
160
                // Copy the inputs to be put into the new set, and update the
×
UNCOV
161
                // remaining inputs by removing currentInputs.
×
UNCOV
162
                currentInputs := make([]SweeperInput, b.maxInputs)
×
UNCOV
163
                copy(currentInputs, remainingInputs[:b.maxInputs])
×
UNCOV
164
                remainingInputs = remainingInputs[b.maxInputs:]
×
UNCOV
165

×
UNCOV
166
                // Create an InputSet using the max allowed number of inputs.
×
UNCOV
167
                set, err := NewBudgetInputSet(
×
UNCOV
168
                        currentInputs, deadlineHeight, b.auxSweeper,
×
UNCOV
169
                )
×
UNCOV
170
                if err != nil {
×
UNCOV
171
                        log.Errorf("unable to create input set: %v", err)
×
UNCOV
172

×
UNCOV
173
                        continue
×
174
                }
175

UNCOV
176
                sets = append(sets, set)
×
177
        }
178

179
        // Create an InputSet from the remaining inputs.
180
        if len(remainingInputs) > 0 {
6✔
181
                set, err := NewBudgetInputSet(
3✔
182
                        remainingInputs, deadlineHeight, b.auxSweeper,
3✔
183
                )
3✔
184
                if err != nil {
3✔
185
                        log.Errorf("unable to create input set: %v", err)
×
186
                        return nil
×
187
                }
×
188

189
                sets = append(sets, set)
3✔
190
        }
191

192
        return sets
3✔
193
}
194

195
// filterInputs filters out inputs that have,
196
// - a budget below the min relay fee.
197
// - a budget below its requested starting fee.
198
// - a required output that's below the dust.
199
func (b *BudgetAggregator) filterInputs(inputs InputsMap) InputsMap {
3✔
200
        // Get the current min relay fee for this round.
3✔
201
        minFeeRate := b.estimator.RelayFeePerKW()
3✔
202

3✔
203
        // filterInputs stores a map of inputs that has a budget that at least
3✔
204
        // can pay the minimal fee.
3✔
205
        filteredInputs := make(InputsMap, len(inputs))
3✔
206

3✔
207
        // Iterate all the inputs and filter out the ones whose budget cannot
3✔
208
        // cover the min fee.
3✔
209
        for _, pi := range inputs {
6✔
210
                op := pi.OutPoint()
3✔
211

3✔
212
                // Get the size of the witness and skip if there's an error.
3✔
213
                witnessSize, _, err := pi.WitnessType().SizeUpperBound()
3✔
214
                if err != nil {
3✔
UNCOV
215
                        log.Warnf("Skipped input=%v: cannot get its size: %v",
×
UNCOV
216
                                op, err)
×
UNCOV
217

×
UNCOV
218
                        continue
×
219
                }
220

221
                //nolint:ll
222
                // Calculate the size if the input is included in the tx.
223
                //
224
                // NOTE: When including this input, we need to account the
225
                // non-witness data which is expressed in vb.
226
                //
227
                // TODO(yy): This is not accurate for tapscript input. We need
228
                // to unify calculations used in the `TxWeightEstimator` inside
229
                // `input/size.go` and `weightEstimator` in
230
                // `weight_estimator.go`. And calculate the expected weights
231
                // similar to BOLT-3:
232
                // https://github.com/lightning/bolts/blob/master/03-transactions.md#appendix-a-expected-weights
233
                wu := lntypes.VByte(input.InputSize).ToWU() + witnessSize
3✔
234

3✔
235
                // Skip inputs that has too little budget.
3✔
236
                minFee := minFeeRate.FeeForWeight(wu)
3✔
237
                if pi.params.Budget < minFee {
6✔
238
                        log.Warnf("Skipped input=%v: has budget=%v, but the "+
3✔
239
                                "min fee requires %v (feerate=%v), size=%v", op,
3✔
240
                                pi.params.Budget, minFee,
3✔
241
                                minFeeRate.FeePerVByte(), wu.ToVB())
3✔
242

3✔
243
                        continue
3✔
244
                }
245

246
                // Skip inputs that has cannot cover its starting fees.
247
                startingFeeRate := pi.params.StartingFeeRate.UnwrapOr(
3✔
248
                        chainfee.SatPerKWeight(0),
3✔
249
                )
3✔
250
                startingFee := startingFeeRate.FeeForWeight(wu)
3✔
251
                if pi.params.Budget < startingFee {
3✔
252
                        log.Errorf("Skipped input=%v: has budget=%v, but the "+
×
253
                                "starting fee requires %v (feerate=%v), "+
×
254
                                "size=%v", op, pi.params.Budget, startingFee,
×
255
                                startingFeeRate.FeePerVByte(), wu.ToVB())
×
256

×
257
                        continue
×
258
                }
259

260
                // If the input comes with a required tx out that is below
261
                // dust, we won't add it.
262
                //
263
                // NOTE: only HtlcSecondLevelAnchorInput returns non-nil
264
                // RequiredTxOut.
265
                reqOut := pi.RequiredTxOut()
3✔
266
                if reqOut != nil {
6✔
267
                        if isDustOutput(reqOut) {
3✔
UNCOV
268
                                log.Errorf("Rejected input=%v due to dust "+
×
UNCOV
269
                                        "required output=%v", op, reqOut.Value)
×
UNCOV
270

×
UNCOV
271
                                continue
×
272
                        }
273
                }
274

275
                filteredInputs[op] = pi
3✔
276
        }
277

278
        return filteredInputs
3✔
279
}
280

281
// sortInputs sorts the inputs based on their economical value.
282
//
283
// NOTE: besides the forced inputs, the sorting won't make any difference
284
// because all the inputs are added to the same set. The exception is when the
285
// number of inputs exceeds the maxInputs limit, it requires us to split them
286
// into smaller clusters. In that case, the sorting will make a difference as
287
// the budgets of the clusters will be different.
288
func (b *BudgetAggregator) sortInputs(inputs []SweeperInput) []SweeperInput {
3✔
289
        // sortedInputs is the final list of inputs sorted by their economical
3✔
290
        // value.
3✔
291
        sortedInputs := make([]SweeperInput, 0, len(inputs))
3✔
292

3✔
293
        // Copy the inputs.
3✔
294
        sortedInputs = append(sortedInputs, inputs...)
3✔
295

3✔
296
        // Sort the inputs based on their budgets.
3✔
297
        //
3✔
298
        // NOTE: We can implement more sophisticated algorithm as the budget
3✔
299
        // left is a function f(minFeeRate, size) = b1 - s1 * r > b2 - s2 * r,
3✔
300
        // where b1 and b2 are budgets, s1 and s2 are sizes of the inputs.
3✔
301
        sort.Slice(sortedInputs, func(i, j int) bool {
6✔
302
                left := sortedInputs[i].params.Budget
3✔
303
                right := sortedInputs[j].params.Budget
3✔
304

3✔
305
                // Make sure forced inputs are always put in the front.
3✔
306
                leftForce := sortedInputs[i].params.Immediate
3✔
307
                rightForce := sortedInputs[j].params.Immediate
3✔
308

3✔
309
                // If both are forced inputs, we return the one with the higher
3✔
310
                // budget. If neither are forced inputs, we also return the one
3✔
311
                // with the higher budget.
3✔
312
                if leftForce == rightForce {
6✔
313
                        return left > right
3✔
314
                }
3✔
315

316
                // Otherwise, it's either the left or the right is forced. We
317
                // can simply return `leftForce` here as, if it's true, the
318
                // left is forced and should be put in the front. Otherwise,
319
                // the right is forced and should be put in the front.
UNCOV
320
                return leftForce
×
321
        })
322

323
        return sortedInputs
3✔
324
}
325

326
// splitOnLocktime splits the list of inputs based on their locktime.
327
//
328
// TODO(yy): this is a temporary hack as the blocks are not synced among the
329
// contractcourt and the sweeper.
330
func splitOnLocktime(inputs []SweeperInput) map[uint32][]SweeperInput {
3✔
331
        result := make(map[uint32][]SweeperInput)
3✔
332
        noLocktimeInputs := make([]SweeperInput, 0, len(inputs))
3✔
333

3✔
334
        // mergeLocktime is the locktime that we use to merge all the
3✔
335
        // nolocktime inputs into.
3✔
336
        var mergeLocktime uint32
3✔
337

3✔
338
        // Iterate all inputs and split them based on their locktimes.
3✔
339
        for _, inp := range inputs {
6✔
340
                locktime, required := inp.RequiredLockTime()
3✔
341
                if !required {
6✔
342
                        log.Tracef("No locktime required for input=%v",
3✔
343
                                inp.OutPoint())
3✔
344

3✔
345
                        noLocktimeInputs = append(noLocktimeInputs, inp)
3✔
346

3✔
347
                        continue
3✔
348
                }
349

350
                log.Tracef("Split input=%v on locktime=%v", inp.OutPoint(),
3✔
351
                        locktime)
3✔
352

3✔
353
                // Get the slice - the slice will be initialized if not found.
3✔
354
                inputList := result[locktime]
3✔
355

3✔
356
                // Add the input to the list.
3✔
357
                inputList = append(inputList, inp)
3✔
358

3✔
359
                // Update the map.
3✔
360
                result[locktime] = inputList
3✔
361

3✔
362
                // Update the merge locktime.
3✔
363
                mergeLocktime = locktime
3✔
364
        }
365

366
        // If there are locktime inputs, we will merge the no locktime inputs
367
        // to the last locktime group found.
368
        if len(result) > 0 {
6✔
369
                log.Tracef("No locktime inputs has been merged to locktime=%v",
3✔
370
                        mergeLocktime)
3✔
371
                result[mergeLocktime] = append(
3✔
372
                        result[mergeLocktime], noLocktimeInputs...,
3✔
373
                )
3✔
374
        } else {
6✔
375
                // Otherwise just return the no locktime inputs.
3✔
376
                result[mergeLocktime] = noLocktimeInputs
3✔
377
        }
3✔
378

379
        return result
3✔
380
}
381

382
// isDustOutput checks if the given output is considered as dust.
383
func isDustOutput(output *wire.TxOut) bool {
3✔
384
        // Fetch the dust limit for this output.
3✔
385
        dustLimit := lnwallet.DustLimitForSize(len(output.PkScript))
3✔
386

3✔
387
        // If the output is below the dust limit, we consider it dust.
3✔
388
        return btcutil.Amount(output.Value) < dustLimit
3✔
389
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc