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

lightningnetwork / lnd / 11216766535

07 Oct 2024 01:37PM UTC coverage: 57.817% (-1.0%) from 58.817%
11216766535

Pull #9148

github

ProofOfKeags
lnwire: remove kickoff feerate from propose/commit
Pull Request #9148: DynComms [2/n]: lnwire: add authenticated wire messages for Dyn*

571 of 879 new or added lines in 16 files covered. (64.96%)

23253 existing lines in 251 files now uncovered.

99022 of 171268 relevant lines covered (57.82%)

38420.67 hits per line

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

96.1
/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/input"
9
        "github.com/lightningnetwork/lnd/lntypes"
10
        "github.com/lightningnetwork/lnd/lnwallet"
11
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
12
)
13

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

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

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

36
// Compile-time constraint to ensure BudgetAggregator implements UtxoAggregator.
37
var _ UtxoAggregator = (*BudgetAggregator)(nil)
38

39
// NewBudgetAggregator creates a new instance of a BudgetAggregator.
40
func NewBudgetAggregator(estimator chainfee.Estimator,
41
        maxInputs uint32) *BudgetAggregator {
42

43
        return &BudgetAggregator{
44
                estimator: estimator,
45
                maxInputs: maxInputs,
46
        }
4✔
47
}
4✔
48

4✔
49
// clusterGroup defines an alias for a set of inputs that are to be grouped.
4✔
50
type clusterGroup map[int32][]SweeperInput
4✔
51

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

64
        // Create clusters to group inputs based on their deadline height.
65
        clusters := make(clusterGroup, len(filteredInputs))
66

1✔
67
        // exclusiveInputs is a set of inputs that are not to be included in
1✔
68
        // any cluster. These inputs can only be swept independently as there's
1✔
69
        // no guarantee which input will be confirmed first, which means
1✔
70
        // grouping exclusive inputs may jeopardize non-exclusive inputs.
1✔
71
        exclusiveInputs := make(map[wire.OutPoint]clusterGroup)
1✔
72

1✔
73
        // Iterate all the inputs and group them based on their specified
1✔
74
        // deadline heights.
1✔
75
        for _, input := range filteredInputs {
1✔
76
                // Get deadline height, and use the specified default deadline
1✔
77
                // height if it's not set.
1✔
78
                height := input.DeadlineHeight
1✔
79

1✔
80
                // Put exclusive inputs in their own set.
1✔
81
                if input.params.ExclusiveGroup != nil {
8✔
82
                        log.Tracef("Input %v is exclusive", input.OutPoint())
7✔
83
                        exclusiveInputs[input.OutPoint()] = clusterGroup{
7✔
84
                                height: []SweeperInput{*input},
7✔
85
                        }
7✔
86

7✔
87
                        continue
8✔
88
                }
1✔
89

1✔
90
                cluster, ok := clusters[height]
1✔
91
                if !ok {
1✔
92
                        cluster = make([]SweeperInput, 0)
1✔
93
                }
1✔
94

95
                cluster = append(cluster, *input)
96
                clusters[height] = cluster
6✔
97
        }
9✔
98

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

108
                // Split on locktimes if they are different.
109
                splitClusters := splitOnLocktime(sortedInputs)
1✔
110

4✔
111
                // Create input sets from the cluster.
3✔
112
                for _, cluster := range splitClusters {
3✔
113
                        sets := b.createInputSets(cluster, height)
3✔
114
                        inputSets = append(inputSets, sets...)
3✔
115
                }
3✔
116
        }
3✔
117

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

2✔
126
        return inputSets
2✔
127
}
1✔
128

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

141
        // sets holds the InputSets that we will return.
142
        sets := make([]InputSet, 0)
143

144
        // Copy the inputs to a new slice so we can modify it.
145
        remainingInputs := make([]SweeperInput, len(inputs))
8✔
146
        copy(remainingInputs, inputs)
8✔
147

8✔
148
        // If the number of inputs is greater than the max inputs allowed, we
8✔
149
        // will split them into smaller clusters.
8✔
150
        for uint32(len(remainingInputs)) > b.maxInputs {
8✔
151
                log.Tracef("Cluster has %v inputs, max is %v, dividing...",
8✔
152
                        len(inputs), b.maxInputs)
8✔
153

8✔
154
                // Copy the inputs to be put into the new set, and update the
8✔
155
                // remaining inputs by removing currentInputs.
8✔
156
                currentInputs := make([]SweeperInput, b.maxInputs)
10✔
157
                copy(currentInputs, remainingInputs[:b.maxInputs])
2✔
158
                remainingInputs = remainingInputs[b.maxInputs:]
2✔
159

2✔
160
                // Create an InputSet using the max allowed number of inputs.
2✔
161
                set, err := NewBudgetInputSet(
2✔
162
                        currentInputs, deadlineHeight,
2✔
163
                )
2✔
164
                if err != nil {
2✔
165
                        log.Errorf("unable to create input set: %v", err)
2✔
166

2✔
167
                        continue
2✔
168
                }
2✔
169

2✔
170
                sets = append(sets, set)
3✔
171
        }
1✔
172

1✔
173
        // Create an InputSet from the remaining inputs.
1✔
174
        if len(remainingInputs) > 0 {
175
                set, err := NewBudgetInputSet(
176
                        remainingInputs, deadlineHeight,
1✔
177
                )
178
                if err != nil {
179
                        log.Errorf("unable to create input set: %v", err)
180
                        return nil
16✔
181
                }
8✔
182

8✔
183
                sets = append(sets, set)
8✔
184
        }
8✔
UNCOV
185

×
UNCOV
186
        return sets
×
UNCOV
187
}
×
188

189
// filterInputs filters out inputs that have,
8✔
190
// - a budget below the min relay fee.
191
// - a budget below its requested starting fee.
192
// - a required output that's below the dust.
8✔
193
func (b *BudgetAggregator) filterInputs(inputs InputsMap) InputsMap {
194
        // Get the current min relay fee for this round.
195
        minFeeRate := b.estimator.RelayFeePerKW()
196

197
        // filterInputs stores a map of inputs that has a budget that at least
198
        // can pay the minimal fee.
199
        filteredInputs := make(InputsMap, len(inputs))
2✔
200

2✔
201
        // Iterate all the inputs and filter out the ones whose budget cannot
2✔
202
        // cover the min fee.
2✔
203
        for _, pi := range inputs {
2✔
204
                op := pi.OutPoint()
2✔
205

2✔
206
                // Get the size of the witness and skip if there's an error.
2✔
207
                witnessSize, _, err := pi.WitnessType().SizeUpperBound()
2✔
208
                if err != nil {
2✔
209
                        log.Warnf("Skipped input=%v: cannot get its size: %v",
17✔
210
                                op, err)
15✔
211

15✔
212
                        continue
15✔
213
                }
15✔
214

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

229
                // Skip inputs that has too little budget.
230
                minFee := minFeeRate.FeeForWeight(wu)
231
                if pi.params.Budget < minFee {
232
                        log.Warnf("Skipped input=%v: has budget=%v, but the "+
233
                                "min fee requires %v (feerate=%v), size=%v", op,
14✔
234
                                pi.params.Budget, minFee,
14✔
235
                                minFeeRate.FeePerVByte(), wu.ToVB())
14✔
236

14✔
237
                        continue
18✔
238
                }
4✔
239

4✔
240
                // Skip inputs that has cannot cover its starting fees.
4✔
241
                startingFeeRate := pi.params.StartingFeeRate.UnwrapOr(
4✔
242
                        chainfee.SatPerKWeight(0),
4✔
243
                )
4✔
244
                startingFee := startingFeeRate.FeeForWeight(wu)
245
                if pi.params.Budget < startingFee {
246
                        log.Errorf("Skipped input=%v: has budget=%v, but the "+
247
                                "starting fee requires %v (feerate=%v), "+
10✔
248
                                "size=%v", op, pi.params.Budget, startingFee,
10✔
249
                                startingFeeRate.FeePerVByte(), wu.ToVB())
10✔
250

10✔
251
                        continue
10✔
UNCOV
252
                }
×
UNCOV
253

×
UNCOV
254
                // If the input comes with a required tx out that is below
×
UNCOV
255
                // dust, we won't add it.
×
UNCOV
256
                //
×
UNCOV
257
                // NOTE: only HtlcSecondLevelAnchorInput returns non-nil
×
258
                // RequiredTxOut.
259
                reqOut := pi.RequiredTxOut()
260
                if reqOut != nil {
261
                        if isDustOutput(reqOut) {
262
                                log.Errorf("Rejected input=%v due to dust "+
263
                                        "required output=%v", op, reqOut.Value)
264

265
                                continue
10✔
266
                        }
11✔
267
                }
2✔
268

1✔
269
                filteredInputs[op] = pi
1✔
270
        }
1✔
271

1✔
272
        return filteredInputs
273
}
274

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

287
        // Copy the inputs.
288
        sortedInputs = append(sortedInputs, inputs...)
4✔
289

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

4✔
299
                // Make sure forced inputs are always put in the front.
4✔
300
                leftForce := sortedInputs[i].params.Immediate
4✔
301
                rightForce := sortedInputs[j].params.Immediate
12✔
302

8✔
303
                // If both are forced inputs, we return the one with the higher
8✔
304
                // budget. If neither are forced inputs, we also return the one
8✔
305
                // with the higher budget.
8✔
306
                if leftForce == rightForce {
8✔
307
                        return left > right
8✔
308
                }
8✔
309

8✔
310
                // Otherwise, it's either the left or the right is forced. We
8✔
311
                // can simply return `leftForce` here as, if it's true, the
8✔
312
                // left is forced and should be put in the front. Otherwise,
13✔
313
                // the right is forced and should be put in the front.
5✔
314
                return leftForce
5✔
315
        })
316

317
        return sortedInputs
318
}
319

320
// splitOnLocktime splits the list of inputs based on their locktime.
3✔
321
//
322
// TODO(yy): this is a temporary hack as the blocks are not synced among the
323
// contractcourt and the sweeper.
4✔
324
func splitOnLocktime(inputs []SweeperInput) map[uint32][]SweeperInput {
325
        result := make(map[uint32][]SweeperInput)
326
        noLocktimeInputs := make([]SweeperInput, 0, len(inputs))
327

328
        // mergeLocktime is the locktime that we use to merge all the
329
        // nolocktime inputs into.
330
        var mergeLocktime uint32
5✔
331

5✔
332
        // Iterate all inputs and split them based on their locktimes.
5✔
333
        for _, inp := range inputs {
5✔
334
                locktime, required := inp.RequiredLockTime()
5✔
335
                if !required {
5✔
336
                        log.Tracef("No locktime required for input=%v",
5✔
337
                                inp.OutPoint())
5✔
338

5✔
339
                        noLocktimeInputs = append(noLocktimeInputs, inp)
19✔
340

14✔
341
                        continue
24✔
342
                }
10✔
343

10✔
344
                log.Tracef("Split input=%v on locktime=%v", inp.OutPoint(),
10✔
345
                        locktime)
10✔
346

10✔
347
                // Get the slice - the slice will be initialized if not found.
10✔
348
                inputList := result[locktime]
349

350
                // Add the input to the list.
4✔
351
                inputList = append(inputList, inp)
4✔
352

4✔
353
                // Update the map.
4✔
354
                result[locktime] = inputList
4✔
355

4✔
356
                // Update the merge locktime.
4✔
357
                mergeLocktime = locktime
4✔
358
        }
4✔
359

4✔
360
        // If there are locktime inputs, we will merge the no locktime inputs
4✔
361
        // to the last locktime group found.
4✔
362
        if len(result) > 0 {
4✔
363
                log.Tracef("No locktime inputs has been merged to locktime=%v",
4✔
364
                        mergeLocktime)
365
                result[mergeLocktime] = append(
366
                        result[mergeLocktime], noLocktimeInputs...,
367
                )
368
        } else {
6✔
369
                // Otherwise just return the no locktime inputs.
1✔
370
                result[mergeLocktime] = noLocktimeInputs
1✔
371
        }
1✔
372

1✔
373
        return result
1✔
374
}
5✔
375

4✔
376
// isDustOutput checks if the given output is considered as dust.
4✔
377
func isDustOutput(output *wire.TxOut) bool {
4✔
378
        // Fetch the dust limit for this output.
379
        dustLimit := lnwallet.DustLimitForSize(len(output.PkScript))
5✔
380

381
        // If the output is below the dust limit, we consider it dust.
382
        return btcutil.Amount(output.Value) < dustLimit
383
}
1✔
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