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

lightningnetwork / lnd / 14277115115

05 Apr 2025 01:43AM UTC coverage: 58.056% (-11.0%) from 69.04%
14277115115

Pull #9670

github

web-flow
Merge a7e89c130 into f0ea5bf3b
Pull Request #9670: build: bump version to v0.19.0 rc2

96191 of 165688 relevant lines covered (58.06%)

1.22 hits per line

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

88.28
/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) {
2✔
79
        signDesc := &input.SignDescriptor{
2✔
80
                Output: &wire.TxOut{
2✔
81
                        PkScript: utxo.PkScript,
2✔
82
                        Value:    int64(utxo.Value),
2✔
83
                },
2✔
84
                HashType: txscript.SigHashAll,
2✔
85
        }
2✔
86

2✔
87
        var witnessType input.WitnessType
2✔
88
        switch utxo.AddressType {
2✔
89
        case lnwallet.WitnessPubKey:
2✔
90
                witnessType = input.WitnessKeyHash
2✔
91
        case lnwallet.NestedWitnessPubKey:
×
92
                witnessType = input.NestedWitnessKeyHash
×
93
        case lnwallet.TaprootPubkey:
2✔
94
                witnessType = input.TaprootPubKeySpend
2✔
95
                signDesc.HashType = txscript.SigHashDefault
2✔
96
        default:
×
97
                return nil, fmt.Errorf("unknown address type %v",
×
98
                        utxo.AddressType)
×
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)
2✔
104

2✔
105
        return input.NewBaseInput(
2✔
106
                &utxo.OutPoint, witnessType, signDesc, heightHint,
2✔
107
        ), nil
2✔
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 {
2✔
138
        // Sanity check the input slice to ensure it's non-empty.
2✔
139
        if len(inputs) == 0 {
2✔
140
                return errEmptyInputs
×
141
        }
×
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
2✔
147

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

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

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

169
        // Make sure the inputs share the same deadline height when there is
170
        // one.
171
        if inputDeadline != deadlineHeight {
2✔
172
                return fmt.Errorf("input deadline height not matched: want "+
×
173
                        "%d, got %d", deadlineHeight, inputDeadline)
×
174
        }
×
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) {
2✔
179
                return fmt.Errorf("duplicate inputs")
×
180
        }
×
181

182
        return nil
2✔
183
}
184

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

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

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

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

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

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

211
        return bi, nil
2✔
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 {
2✔
217
        extraBudget, err := fn.MapOptionZ(
2✔
218
                s, func(aux AuxSweeper) fn.Result[btcutil.Amount] {
2✔
219
                        return aux.ExtraBudgetForInputs(b.Inputs())
×
220
                },
×
221
        ).Unpack()
222
        if err != nil {
2✔
223
                return err
×
224
        }
×
225

226
        b.extraBudget = extraBudget
2✔
227

2✔
228
        return nil
2✔
229
}
230

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

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

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

247
// addWalletInput takes a wallet UTXO and adds it as an input to be used as
248
// budget for the input set.
249
func (b *BudgetInputSet) addWalletInput(utxo *lnwallet.Utxo) error {
2✔
250
        input, err := createWalletTxInput(utxo)
2✔
251
        if err != nil {
2✔
252
                return err
×
253
        }
×
254

255
        pi := SweeperInput{
2✔
256
                Input: input,
2✔
257
                params: Params{
2✔
258
                        DeadlineHeight: fn.Some(b.deadlineHeight),
2✔
259
                },
2✔
260
        }
2✔
261
        b.addInput(pi)
2✔
262

2✔
263
        log.Debugf("Added wallet input to input set: op=%v, amt=%v",
2✔
264
                pi.OutPoint(), utxo.Value)
2✔
265

2✔
266
        return nil
2✔
267
}
268

269
// NeedWalletInput returns true if the input set needs more wallet inputs.
270
//
271
// A set may need wallet inputs when it has a required output or its total
272
// value cannot cover its total budget.
273
func (b *BudgetInputSet) NeedWalletInput() bool {
2✔
274
        var (
2✔
275
                // budgetNeeded is the amount that needs to be covered from
2✔
276
                // other inputs. We start at the value of the extra budget,
2✔
277
                // which might be needed for custom channels that add extra
2✔
278
                // outputs.
2✔
279
                budgetNeeded = b.extraBudget
2✔
280

2✔
281
                // budgetBorrowable is the amount that can be borrowed from
2✔
282
                // other inputs.
2✔
283
                budgetBorrowable btcutil.Amount
2✔
284
        )
2✔
285

2✔
286
        for _, inp := range b.inputs {
4✔
287
                // If this input has a required output, we can assume it's a
2✔
288
                // second-level htlc txns input. Although this input must have
2✔
289
                // a value that can cover its budget, it cannot be used to pay
2✔
290
                // fees. Instead, we need to borrow budget from other inputs to
2✔
291
                // make the sweep happen. Once swept, the input value will be
2✔
292
                // credited to the wallet.
2✔
293
                if inp.RequiredTxOut() != nil {
4✔
294
                        budgetNeeded += inp.params.Budget
2✔
295
                        continue
2✔
296
                }
297

298
                // Get the amount left after covering the input's own budget.
299
                // This amount can then be lent to the above input. For a wallet
300
                // input, its `Budget` is set to zero, which means the whole
301
                // input can be borrowed to cover the budget.
302
                budget := inp.params.Budget
2✔
303
                output := btcutil.Amount(inp.SignDesc().Output.Value)
2✔
304
                budgetBorrowable += output - budget
2✔
305

2✔
306
                // If the input's budget is not even covered by itself, we need
2✔
307
                // to borrow outputs from other inputs.
2✔
308
                if budgetBorrowable < 0 {
4✔
309
                        log.Tracef("Input %v specified a budget that exceeds "+
2✔
310
                                "its output value: %v > %v", inp, budget,
2✔
311
                                output)
2✔
312
                }
2✔
313
        }
314

315
        log.Debugf("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
2✔
316
                budgetNeeded, budgetBorrowable)
2✔
317

2✔
318
        // If we don't have enough extra budget to borrow, we need wallet
2✔
319
        // inputs.
2✔
320
        return budgetBorrowable < budgetNeeded
2✔
321
}
322

323
// hasNormalInput return a bool to indicate whether there exists an input that
324
// doesn't require a TxOut. When an input has no required outputs, it's either a
325
// wallet input, or an input we want to sweep.
326
func (b *BudgetInputSet) hasNormalInput() bool {
2✔
327
        for _, inp := range b.inputs {
4✔
328
                if inp.RequiredTxOut() != nil {
4✔
329
                        continue
2✔
330
                }
331

332
                return true
2✔
333
        }
334

335
        return false
2✔
336
}
337

338
// AddWalletInputs adds wallet inputs to the set until the specified budget is
339
// met. When sweeping inputs with required outputs, although there's budget
340
// specified, it cannot be directly spent from these required outputs. Instead,
341
// we need to borrow budget from other inputs to make the sweep happen.
342
// There are two sources to borrow from: 1) other inputs, 2) wallet utxos. If
343
// we are calling this method, it means other inputs cannot cover the specified
344
// budget, so we need to borrow from wallet utxos.
345
//
346
// Return an error if there are not enough wallet inputs, and the budget set is
347
// set to its initial state by removing any wallet inputs added.
348
//
349
// NOTE: must be called with the wallet lock held via `WithCoinSelectLock`.
350
func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error {
2✔
351
        // Retrieve wallet utxos. Only consider confirmed utxos to prevent
2✔
352
        // problems around RBF rules for unconfirmed inputs. This currently
2✔
353
        // ignores the configured coin selection strategy.
2✔
354
        utxos, err := wallet.ListUnspentWitnessFromDefaultAccount(
2✔
355
                1, math.MaxInt32,
2✔
356
        )
2✔
357
        if err != nil {
2✔
358
                return fmt.Errorf("list unspent witness: %w", err)
×
359
        }
×
360

361
        // Sort the UTXOs by putting smaller values at the start of the slice
362
        // to avoid locking large UTXO for sweeping.
363
        //
364
        // TODO(yy): add more choices to CoinSelectionStrategy and use the
365
        // configured value here.
366
        sort.Slice(utxos, func(i, j int) bool {
4✔
367
                return utxos[i].Value < utxos[j].Value
2✔
368
        })
2✔
369

370
        // Add wallet inputs to the set until the specified budget is covered.
371
        for _, utxo := range utxos {
4✔
372
                err := b.addWalletInput(utxo)
2✔
373
                if err != nil {
2✔
374
                        return err
×
375
                }
×
376

377
                // Return if we've reached the minimum output amount.
378
                if !b.NeedWalletInput() {
4✔
379
                        return nil
2✔
380
                }
2✔
381
        }
382

383
        // Exit if there are no inputs can contribute to the fees.
384
        if !b.hasNormalInput() {
4✔
385
                return ErrNotEnoughInputs
2✔
386
        }
2✔
387

388
        // If there's at least one input that can contribute to fees, we allow
389
        // the sweep to continue, even though the full budget can't be met.
390
        // Maybe later more wallet inputs will become available and we can add
391
        // them if needed.
392
        budget := b.Budget()
2✔
393
        total, spendable := b.inputAmts()
2✔
394
        log.Warnf("Not enough wallet UTXOs: need budget=%v, has spendable=%v, "+
2✔
395
                "total=%v, missing at least %v, sweeping anyway...", budget,
2✔
396
                spendable, total, budget-spendable)
2✔
397

2✔
398
        return nil
2✔
399
}
400

401
// Budget returns the total budget of the set.
402
//
403
// NOTE: part of the InputSet interface.
404
func (b *BudgetInputSet) Budget() btcutil.Amount {
2✔
405
        budget := btcutil.Amount(0)
2✔
406
        for _, input := range b.inputs {
4✔
407
                budget += input.params.Budget
2✔
408
        }
2✔
409

410
        // We'll also tack on the extra budget which will eventually be
411
        // accounted for by the wallet txns when we're broadcasting.
412
        return budget + b.extraBudget
2✔
413
}
414

415
// DeadlineHeight returns the deadline height of the set.
416
//
417
// NOTE: part of the InputSet interface.
418
func (b *BudgetInputSet) DeadlineHeight() int32 {
2✔
419
        return b.deadlineHeight
2✔
420
}
2✔
421

422
// Inputs returns the inputs that should be used to create a tx.
423
//
424
// NOTE: part of the InputSet interface.
425
func (b *BudgetInputSet) Inputs() []input.Input {
2✔
426
        inputs := make([]input.Input, 0, len(b.inputs))
2✔
427
        for _, inp := range b.inputs {
4✔
428
                inputs = append(inputs, inp.Input)
2✔
429
        }
2✔
430

431
        return inputs
2✔
432
}
433

434
// inputAmts returns two values for the set - the total input amount, and the
435
// spendable amount. Only the spendable amount can be used to pay the fees.
436
func (b *BudgetInputSet) inputAmts() (btcutil.Amount, btcutil.Amount) {
2✔
437
        var (
2✔
438
                totalAmt     btcutil.Amount
2✔
439
                spendableAmt btcutil.Amount
2✔
440
        )
2✔
441

2✔
442
        for _, inp := range b.inputs {
4✔
443
                output := btcutil.Amount(inp.SignDesc().Output.Value)
2✔
444
                totalAmt += output
2✔
445

2✔
446
                if inp.RequiredTxOut() != nil {
2✔
447
                        continue
×
448
                }
449

450
                spendableAmt += output
2✔
451
        }
452

453
        return totalAmt, spendableAmt
2✔
454
}
455

456
// StartingFeeRate returns the max starting fee rate found in the inputs.
457
//
458
// NOTE: part of the InputSet interface.
459
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
2✔
460
        maxFeeRate := chainfee.SatPerKWeight(0)
2✔
461
        startingFeeRate := fn.None[chainfee.SatPerKWeight]()
2✔
462

2✔
463
        for _, inp := range b.inputs {
4✔
464
                feerate := inp.params.StartingFeeRate.UnwrapOr(0)
2✔
465
                if feerate > maxFeeRate {
4✔
466
                        maxFeeRate = feerate
2✔
467
                        startingFeeRate = fn.Some(maxFeeRate)
2✔
468
                }
2✔
469
        }
470

471
        return startingFeeRate
2✔
472
}
473

474
// Immediate returns whether the inputs should be swept immediately.
475
//
476
// NOTE: part of the InputSet interface.
477
func (b *BudgetInputSet) Immediate() bool {
2✔
478
        for _, inp := range b.inputs {
4✔
479
                // As long as one of the inputs is immediate, the whole set is
2✔
480
                // immediate.
2✔
481
                if inp.params.Immediate {
4✔
482
                        return true
2✔
483
                }
2✔
484
        }
485

486
        return false
2✔
487
}
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