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

lightningnetwork / lnd / 13157733617

05 Feb 2025 12:49PM UTC coverage: 57.712% (-1.1%) from 58.82%
13157733617

Pull #9447

github

yyforyongyu
sweep: rename methods for clarity

We now rename "third party" to "unknown" as the inputs can be spent via
an older sweeping tx, a third party (anchor), or a remote party (pin).
In fee bumper we don't have the info to distinguish the above cases, and
leave them to be further handled by the sweeper as it has more context.
Pull Request #9447: sweep: start tracking input spending status in the fee bumper

83 of 87 new or added lines in 2 files covered. (95.4%)

19472 existing lines in 252 files now uncovered.

103634 of 179570 relevant lines covered (57.71%)

24840.31 hits per line

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

86.98
/sweep/tx_input_set.go
1
package sweep
2

3
import (
4
        "fmt"
5
        "math"
6
        "sort"
7

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

17
var (
18
        // ErrNotEnoughInputs is returned when there are not enough wallet
19
        // inputs to construct a non-dust change output for an input set.
20
        ErrNotEnoughInputs = fmt.Errorf("not enough inputs")
21

22
        // ErrDeadlinesMismatch is returned when the deadlines of the input
23
        // sets do not match.
24
        ErrDeadlinesMismatch = fmt.Errorf("deadlines mismatch")
25

26
        // ErrDustOutput is returned when the output value is below the dust
27
        // limit.
28
        ErrDustOutput = fmt.Errorf("dust output")
29
)
30

31
// InputSet defines an interface that's responsible for filtering a set of
32
// inputs that can be swept economically.
33
type InputSet interface {
34
        // Inputs returns the set of inputs that should be used to create a tx.
35
        Inputs() []input.Input
36

37
        // AddWalletInputs adds wallet inputs to the set until a non-dust
38
        // change output can be made. Return an error if there are not enough
39
        // wallet inputs.
40
        AddWalletInputs(wallet Wallet) error
41

42
        // NeedWalletInput returns true if the input set needs more wallet
43
        // inputs.
44
        NeedWalletInput() bool
45

46
        // DeadlineHeight returns an absolute block height to express the
47
        // time-sensitivity of the input set. The outputs from a force close tx
48
        // have different time preferences:
49
        // - to_local: no time pressure as it can only be swept by us.
50
        // - first level outgoing HTLC: must be swept before its corresponding
51
        //   incoming HTLC's CLTV is reached.
52
        // - first level incoming HTLC: must be swept before its CLTV is
53
        //   reached.
54
        // - second level HTLCs: no time pressure.
55
        // - anchor: for CPFP-purpose anchor, it must be swept before any of
56
        //   the above CLTVs is reached. For non-CPFP purpose anchor, there's
57
        //   no time pressure.
58
        DeadlineHeight() int32
59

60
        // Budget givens the total amount that can be used as fees by this
61
        // input set.
62
        Budget() btcutil.Amount
63

64
        // StartingFeeRate returns the max starting fee rate found in the
65
        // inputs.
66
        StartingFeeRate() fn.Option[chainfee.SatPerKWeight]
67

68
        // Immediate returns a boolean to indicate whether the tx made from
69
        // this input set should be published immediately.
70
        //
71
        // TODO(yy): create a new method `Params` to combine the informational
72
        // methods DeadlineHeight, Budget, StartingFeeRate and Immediate.
73
        Immediate() bool
74
}
75

76
// createWalletTxInput converts a wallet utxo into an object that can be added
77
// to the other inputs to sweep.
78
func createWalletTxInput(utxo *lnwallet.Utxo) (input.Input, error) {
4✔
79
        signDesc := &input.SignDescriptor{
4✔
80
                Output: &wire.TxOut{
4✔
81
                        PkScript: utxo.PkScript,
4✔
82
                        Value:    int64(utxo.Value),
4✔
83
                },
4✔
84
                HashType: txscript.SigHashAll,
4✔
85
        }
4✔
86

4✔
87
        var witnessType input.WitnessType
4✔
88
        switch utxo.AddressType {
4✔
89
        case lnwallet.WitnessPubKey:
3✔
90
                witnessType = input.WitnessKeyHash
3✔
91
        case lnwallet.NestedWitnessPubKey:
×
92
                witnessType = input.NestedWitnessKeyHash
×
UNCOV
93
        case lnwallet.TaprootPubkey:
×
UNCOV
94
                witnessType = input.TaprootPubKeySpend
×
UNCOV
95
                signDesc.HashType = txscript.SigHashDefault
×
96
        default:
1✔
97
                return nil, fmt.Errorf("unknown address type %v",
1✔
98
                        utxo.AddressType)
1✔
99
        }
100

101
        // A height hint doesn't need to be set, because we don't monitor these
102
        // inputs for spend.
103
        heightHint := uint32(0)
3✔
104

3✔
105
        return input.NewBaseInput(
3✔
106
                &utxo.OutPoint, witnessType, signDesc, heightHint,
3✔
107
        ), nil
3✔
108
}
109

110
// BudgetInputSet implements the interface `InputSet`. It takes a list of
111
// pending inputs which share the same deadline height and groups them into a
112
// set conditionally based on their economical values.
113
type BudgetInputSet struct {
114
        // inputs is the set of inputs that have been added to the set after
115
        // considering their economical contribution.
116
        inputs []*SweeperInput
117

118
        // deadlineHeight is the height which the inputs in this set must be
119
        // confirmed by.
120
        deadlineHeight int32
121

122
        // extraBudget is a value that should be allocated to sweep the given
123
        // set of inputs. This can be used to add extra funds to the sweep
124
        // transaction, for example to cover fees for additional outputs of
125
        // custom channels.
126
        extraBudget btcutil.Amount
127
}
128

129
// Compile-time constraint to ensure budgetInputSet implements InputSet.
130
var _ InputSet = (*BudgetInputSet)(nil)
131

132
// errEmptyInputs is returned when the input slice is empty.
133
var errEmptyInputs = fmt.Errorf("inputs slice is empty")
134

135
// validateInputs is used when creating new BudgetInputSet to ensure there are
136
// no duplicate inputs and they all share the same deadline heights, if set.
137
func validateInputs(inputs []SweeperInput, deadlineHeight int32) error {
24✔
138
        // Sanity check the input slice to ensure it's non-empty.
24✔
139
        if len(inputs) == 0 {
26✔
140
                return errEmptyInputs
2✔
141
        }
2✔
142

143
        // inputDeadline tracks the input's deadline height. It will be updated
144
        // if the input has a different deadline than the specified
145
        // deadlineHeight.
146
        inputDeadline := deadlineHeight
22✔
147

22✔
148
        // dedupInputs is a set used to track unique outpoints of the inputs.
22✔
149
        dedupInputs := fn.NewSet(
22✔
150
                // Iterate all the inputs and map the function.
22✔
151
                fn.Map(inputs, func(inp SweeperInput) wire.OutPoint {
56✔
152
                        // If the input has a deadline height, we'll check if
34✔
153
                        // it's the same as the specified.
34✔
154
                        inp.params.DeadlineHeight.WhenSome(func(h int32) {
50✔
155
                                // Exit early if the deadlines matched.
16✔
156
                                if h == deadlineHeight {
28✔
157
                                        return
12✔
158
                                }
12✔
159

160
                                // Update the deadline height if it's
161
                                // different.
162
                                inputDeadline = h
4✔
163
                        })
164

165
                        return inp.OutPoint()
34✔
166
                })...,
167
        )
168

169
        // Make sure the inputs share the same deadline height when there is
170
        // one.
171
        if inputDeadline != deadlineHeight {
25✔
172
                return fmt.Errorf("input deadline height not matched: want "+
3✔
173
                        "%d, got %d", deadlineHeight, inputDeadline)
3✔
174
        }
3✔
175

176
        // Provide a defensive check to ensure that we don't have any duplicate
177
        // inputs within the set.
178
        if len(dedupInputs) != len(inputs) {
20✔
179
                return fmt.Errorf("duplicate inputs")
1✔
180
        }
1✔
181

182
        return nil
18✔
183
}
184

185
// NewBudgetInputSet creates a new BudgetInputSet.
186
func NewBudgetInputSet(inputs []SweeperInput, deadlineHeight int32,
187
        auxSweeper fn.Option[AuxSweeper]) (*BudgetInputSet, error) {
24✔
188

24✔
189
        // Validate the supplied inputs.
24✔
190
        if err := validateInputs(inputs, deadlineHeight); err != nil {
30✔
191
                return nil, err
6✔
192
        }
6✔
193

194
        bi := &BudgetInputSet{
18✔
195
                deadlineHeight: deadlineHeight,
18✔
196
                inputs:         make([]*SweeperInput, 0, len(inputs)),
18✔
197
        }
18✔
198

18✔
199
        for _, input := range inputs {
44✔
200
                bi.addInput(input)
26✔
201
        }
26✔
202

203
        log.Tracef("Created %v", bi.String())
18✔
204

18✔
205
        // Attach an optional budget. This will be a no-op if the auxSweeper
18✔
206
        // is not set.
18✔
207
        if err := bi.attachExtraBudget(auxSweeper); err != nil {
18✔
208
                return nil, err
×
209
        }
×
210

211
        return bi, nil
18✔
212
}
213

214
// attachExtraBudget attaches an extra budget to the input set, if the passed
215
// aux sweeper is set.
216
func (b *BudgetInputSet) attachExtraBudget(s fn.Option[AuxSweeper]) error {
18✔
217
        extraBudget, err := fn.MapOptionZ(
18✔
218
                s, func(aux AuxSweeper) fn.Result[btcutil.Amount] {
24✔
219
                        return aux.ExtraBudgetForInputs(b.Inputs())
6✔
220
                },
6✔
221
        ).Unpack()
222
        if err != nil {
18✔
223
                return err
×
224
        }
×
225

226
        b.extraBudget = extraBudget
18✔
227

18✔
228
        return nil
18✔
229
}
230

231
// String returns a human-readable description of the input set.
232
func (b *BudgetInputSet) String() string {
18✔
233
        inputsDesc := ""
18✔
234
        for _, input := range b.inputs {
44✔
235
                inputsDesc += fmt.Sprintf("\n%v", input)
26✔
236
        }
26✔
237

238
        return fmt.Sprintf("BudgetInputSet(budget=%v, deadline=%v, "+
18✔
239
                "inputs=[%v])", b.Budget(), b.DeadlineHeight(), inputsDesc)
18✔
240
}
241

242
// addInput adds an input to the input set.
243
func (b *BudgetInputSet) addInput(input SweeperInput) {
30✔
244
        b.inputs = append(b.inputs, &input)
30✔
245
}
30✔
246

247
// NeedWalletInput returns true if the input set needs more wallet inputs.
248
//
249
// A set may need wallet inputs when it has a required output or its total
250
// value cannot cover its total budget.
251
func (b *BudgetInputSet) NeedWalletInput() bool {
9✔
252
        var (
9✔
253
                // budgetNeeded is the amount that needs to be covered from
9✔
254
                // other inputs. We start at the value of the extra budget,
9✔
255
                // which might be needed for custom channels that add extra
9✔
256
                // outputs.
9✔
257
                budgetNeeded = b.extraBudget
9✔
258

9✔
259
                // budgetBorrowable is the amount that can be borrowed from
9✔
260
                // other inputs.
9✔
261
                budgetBorrowable btcutil.Amount
9✔
262
        )
9✔
263

9✔
264
        for _, inp := range b.inputs {
24✔
265
                // If this input has a required output, we can assume it's a
15✔
266
                // second-level htlc txns input. Although this input must have
15✔
267
                // a value that can cover its budget, it cannot be used to pay
15✔
268
                // fees. Instead, we need to borrow budget from other inputs to
15✔
269
                // make the sweep happen. Once swept, the input value will be
15✔
270
                // credited to the wallet.
15✔
271
                if inp.RequiredTxOut() != nil {
21✔
272
                        budgetNeeded += inp.params.Budget
6✔
273
                        continue
6✔
274
                }
275

276
                // Get the amount left after covering the input's own budget.
277
                // This amount can then be lent to the above input.
278
                budget := inp.params.Budget
9✔
279
                output := btcutil.Amount(inp.SignDesc().Output.Value)
9✔
280
                budgetBorrowable += output - budget
9✔
281

9✔
282
                // If the input's budget is not even covered by itself, we need
9✔
283
                // to borrow outputs from other inputs.
9✔
284
                if budgetBorrowable < 0 {
10✔
285
                        log.Tracef("Input %v specified a budget that exceeds "+
1✔
286
                                "its output value: %v > %v", inp, budget,
1✔
287
                                output)
1✔
288
                }
1✔
289
        }
290

291
        log.Debugf("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
9✔
292
                budgetNeeded, budgetBorrowable)
9✔
293

9✔
294
        // If we don't have enough extra budget to borrow, we need wallet
9✔
295
        // inputs.
9✔
296
        return budgetBorrowable < budgetNeeded
9✔
297
}
298

299
// copyInputs returns a copy of the slice of the inputs in the set.
300
func (b *BudgetInputSet) copyInputs() []*SweeperInput {
4✔
301
        inputs := make([]*SweeperInput, len(b.inputs))
4✔
302
        copy(inputs, b.inputs)
4✔
303
        return inputs
4✔
304
}
4✔
305

306
// AddWalletInputs adds wallet inputs to the set until the specified budget is
307
// met. When sweeping inputs with required outputs, although there's budget
308
// specified, it cannot be directly spent from these required outputs. Instead,
309
// we need to borrow budget from other inputs to make the sweep happen.
310
// There are two sources to borrow from: 1) other inputs, 2) wallet utxos. If
311
// we are calling this method, it means other inputs cannot cover the specified
312
// budget, so we need to borrow from wallet utxos.
313
//
314
// Return an error if there are not enough wallet inputs, and the budget set is
315
// set to its initial state by removing any wallet inputs added.
316
//
317
// NOTE: must be called with the wallet lock held via `WithCoinSelectLock`.
318
func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error {
5✔
319
        // Retrieve wallet utxos. Only consider confirmed utxos to prevent
5✔
320
        // problems around RBF rules for unconfirmed inputs. This currently
5✔
321
        // ignores the configured coin selection strategy.
5✔
322
        utxos, err := wallet.ListUnspentWitnessFromDefaultAccount(
5✔
323
                1, math.MaxInt32,
5✔
324
        )
5✔
325
        if err != nil {
6✔
326
                return fmt.Errorf("list unspent witness: %w", err)
1✔
327
        }
1✔
328

329
        // Sort the UTXOs by putting smaller values at the start of the slice
330
        // to avoid locking large UTXO for sweeping.
331
        //
332
        // TODO(yy): add more choices to CoinSelectionStrategy and use the
333
        // configured value here.
334
        sort.Slice(utxos, func(i, j int) bool {
5✔
335
                return utxos[i].Value < utxos[j].Value
1✔
336
        })
1✔
337

338
        // Make a copy of the current inputs. If the wallet doesn't have enough
339
        // utxos to cover the budget, we will revert the current set to its
340
        // original state by removing the added wallet inputs.
341
        originalInputs := b.copyInputs()
4✔
342

4✔
343
        // Add wallet inputs to the set until the specified budget is covered.
4✔
344
        for _, utxo := range utxos {
8✔
345
                input, err := createWalletTxInput(utxo)
4✔
346
                if err != nil {
5✔
347
                        return err
1✔
348
                }
1✔
349

350
                pi := SweeperInput{
3✔
351
                        Input: input,
3✔
352
                        params: Params{
3✔
353
                                DeadlineHeight: fn.Some(b.deadlineHeight),
3✔
354
                        },
3✔
355
                }
3✔
356
                b.addInput(pi)
3✔
357

3✔
358
                log.Debugf("Added wallet input to input set: op=%v, amt=%v",
3✔
359
                        pi.OutPoint(), utxo.Value)
3✔
360

3✔
361
                // Return if we've reached the minimum output amount.
3✔
362
                if !b.NeedWalletInput() {
4✔
363
                        return nil
1✔
364
                }
1✔
365
        }
366

367
        // The wallet doesn't have enough utxos to cover the budget. Revert the
368
        // input set to its original state.
369
        b.inputs = originalInputs
2✔
370

2✔
371
        return ErrNotEnoughInputs
2✔
372
}
373

374
// Budget returns the total budget of the set.
375
//
376
// NOTE: part of the InputSet interface.
377
func (b *BudgetInputSet) Budget() btcutil.Amount {
23✔
378
        budget := btcutil.Amount(0)
23✔
379
        for _, input := range b.inputs {
60✔
380
                budget += input.params.Budget
37✔
381
        }
37✔
382

383
        // We'll also tack on the extra budget which will eventually be
384
        // accounted for by the wallet txns when we're broadcasting.
385
        return budget + b.extraBudget
23✔
386
}
387

388
// DeadlineHeight returns the deadline height of the set.
389
//
390
// NOTE: part of the InputSet interface.
391
func (b *BudgetInputSet) DeadlineHeight() int32 {
22✔
392
        return b.deadlineHeight
22✔
393
}
22✔
394

395
// Inputs returns the inputs that should be used to create a tx.
396
//
397
// NOTE: part of the InputSet interface.
398
func (b *BudgetInputSet) Inputs() []input.Input {
10✔
399
        inputs := make([]input.Input, 0, len(b.inputs))
10✔
400
        for _, inp := range b.inputs {
25✔
401
                inputs = append(inputs, inp.Input)
15✔
402
        }
15✔
403

404
        return inputs
10✔
405
}
406

407
// StartingFeeRate returns the max starting fee rate found in the inputs.
408
//
409
// NOTE: part of the InputSet interface.
UNCOV
410
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
×
UNCOV
411
        maxFeeRate := chainfee.SatPerKWeight(0)
×
UNCOV
412
        startingFeeRate := fn.None[chainfee.SatPerKWeight]()
×
UNCOV
413

×
UNCOV
414
        for _, inp := range b.inputs {
×
UNCOV
415
                feerate := inp.params.StartingFeeRate.UnwrapOr(0)
×
UNCOV
416
                if feerate > maxFeeRate {
×
UNCOV
417
                        maxFeeRate = feerate
×
UNCOV
418
                        startingFeeRate = fn.Some(maxFeeRate)
×
UNCOV
419
                }
×
420
        }
421

UNCOV
422
        return startingFeeRate
×
423
}
424

425
// Immediate returns whether the inputs should be swept immediately.
426
//
427
// NOTE: part of the InputSet interface.
UNCOV
428
func (b *BudgetInputSet) Immediate() bool {
×
UNCOV
429
        for _, inp := range b.inputs {
×
UNCOV
430
                // As long as one of the inputs is immediate, the whole set is
×
UNCOV
431
                // immediate.
×
UNCOV
432
                if inp.params.Immediate {
×
UNCOV
433
                        return true
×
UNCOV
434
                }
×
435
        }
436

UNCOV
437
        return false
×
438
}
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