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

lightningnetwork / lnd / 12312390362

13 Dec 2024 08:44AM UTC coverage: 57.458% (+8.5%) from 48.92%
12312390362

Pull #9343

github

ellemouton
fn: rework the ContextGuard and add tests

In this commit, the ContextGuard struct is re-worked such that the
context that its new main WithCtx method provides is cancelled in sync
with a parent context being cancelled or with it's quit channel being
cancelled. Tests are added to assert the behaviour. In order for the
close of the quit channel to be consistent with the cancelling of the
derived context, the quit channel _must_ be contained internal to the
ContextGuard so that callers are only able to close the channel via the
exposed Quit method which will then take care to first cancel any
derived context that depend on the quit channel before returning.
Pull Request #9343: fn: expand the ContextGuard and add tests

101853 of 177264 relevant lines covered (57.46%)

24972.93 hits per line

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

90.34
/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

69
// createWalletTxInput converts a wallet utxo into an object that can be added
70
// to the other inputs to sweep.
71
func createWalletTxInput(utxo *lnwallet.Utxo) (input.Input, error) {
4✔
72
        signDesc := &input.SignDescriptor{
4✔
73
                Output: &wire.TxOut{
4✔
74
                        PkScript: utxo.PkScript,
4✔
75
                        Value:    int64(utxo.Value),
4✔
76
                },
4✔
77
                HashType: txscript.SigHashAll,
4✔
78
        }
4✔
79

4✔
80
        var witnessType input.WitnessType
4✔
81
        switch utxo.AddressType {
4✔
82
        case lnwallet.WitnessPubKey:
3✔
83
                witnessType = input.WitnessKeyHash
3✔
84
        case lnwallet.NestedWitnessPubKey:
×
85
                witnessType = input.NestedWitnessKeyHash
×
86
        case lnwallet.TaprootPubkey:
×
87
                witnessType = input.TaprootPubKeySpend
×
88
                signDesc.HashType = txscript.SigHashDefault
×
89
        default:
1✔
90
                return nil, fmt.Errorf("unknown address type %v",
1✔
91
                        utxo.AddressType)
1✔
92
        }
93

94
        // A height hint doesn't need to be set, because we don't monitor these
95
        // inputs for spend.
96
        heightHint := uint32(0)
3✔
97

3✔
98
        return input.NewBaseInput(
3✔
99
                &utxo.OutPoint, witnessType, signDesc, heightHint,
3✔
100
        ), nil
3✔
101
}
102

103
// BudgetInputSet implements the interface `InputSet`. It takes a list of
104
// pending inputs which share the same deadline height and groups them into a
105
// set conditionally based on their economical values.
106
type BudgetInputSet struct {
107
        // inputs is the set of inputs that have been added to the set after
108
        // considering their economical contribution.
109
        inputs []*SweeperInput
110

111
        // deadlineHeight is the height which the inputs in this set must be
112
        // confirmed by.
113
        deadlineHeight int32
114

115
        // extraBudget is a value that should be allocated to sweep the given
116
        // set of inputs. This can be used to add extra funds to the sweep
117
        // transaction, for example to cover fees for additional outputs of
118
        // custom channels.
119
        extraBudget btcutil.Amount
120
}
121

122
// Compile-time constraint to ensure budgetInputSet implements InputSet.
123
var _ InputSet = (*BudgetInputSet)(nil)
124

125
// errEmptyInputs is returned when the input slice is empty.
126
var errEmptyInputs = fmt.Errorf("inputs slice is empty")
127

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

136
        // inputDeadline tracks the input's deadline height. It will be updated
137
        // if the input has a different deadline than the specified
138
        // deadlineHeight.
139
        inputDeadline := deadlineHeight
22✔
140

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

153
                                // Update the deadline height if it's
154
                                // different.
155
                                inputDeadline = h
4✔
156
                        })
157

158
                        return inp.OutPoint()
34✔
159
                })...,
160
        )
161

162
        // Make sure the inputs share the same deadline height when there is
163
        // one.
164
        if inputDeadline != deadlineHeight {
25✔
165
                return fmt.Errorf("input deadline height not matched: want "+
3✔
166
                        "%d, got %d", deadlineHeight, inputDeadline)
3✔
167
        }
3✔
168

169
        // Provide a defensive check to ensure that we don't have any duplicate
170
        // inputs within the set.
171
        if len(dedupInputs) != len(inputs) {
20✔
172
                return fmt.Errorf("duplicate inputs")
1✔
173
        }
1✔
174

175
        return nil
18✔
176
}
177

178
// NewBudgetInputSet creates a new BudgetInputSet.
179
func NewBudgetInputSet(inputs []SweeperInput, deadlineHeight int32,
180
        auxSweeper fn.Option[AuxSweeper]) (*BudgetInputSet, error) {
24✔
181

24✔
182
        // Validate the supplied inputs.
24✔
183
        if err := validateInputs(inputs, deadlineHeight); err != nil {
30✔
184
                return nil, err
6✔
185
        }
6✔
186

187
        bi := &BudgetInputSet{
18✔
188
                deadlineHeight: deadlineHeight,
18✔
189
                inputs:         make([]*SweeperInput, 0, len(inputs)),
18✔
190
        }
18✔
191

18✔
192
        for _, input := range inputs {
44✔
193
                bi.addInput(input)
26✔
194
        }
26✔
195

196
        log.Tracef("Created %v", bi.String())
18✔
197

18✔
198
        // Attach an optional budget. This will be a no-op if the auxSweeper
18✔
199
        // is not set.
18✔
200
        if err := bi.attachExtraBudget(auxSweeper); err != nil {
18✔
201
                return nil, err
×
202
        }
×
203

204
        return bi, nil
18✔
205
}
206

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

219
        b.extraBudget = extraBudget
18✔
220

18✔
221
        return nil
18✔
222
}
223

224
// String returns a human-readable description of the input set.
225
func (b *BudgetInputSet) String() string {
18✔
226
        inputsDesc := ""
18✔
227
        for _, input := range b.inputs {
44✔
228
                inputsDesc += fmt.Sprintf("\n%v", input)
26✔
229
        }
26✔
230

231
        return fmt.Sprintf("BudgetInputSet(budget=%v, deadline=%v, "+
18✔
232
                "inputs=[%v])", b.Budget(), b.DeadlineHeight(), inputsDesc)
18✔
233
}
234

235
// addInput adds an input to the input set.
236
func (b *BudgetInputSet) addInput(input SweeperInput) {
30✔
237
        b.inputs = append(b.inputs, &input)
30✔
238
}
30✔
239

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

9✔
252
                // budgetBorrowable is the amount that can be borrowed from
9✔
253
                // other inputs.
9✔
254
                budgetBorrowable btcutil.Amount
9✔
255
        )
9✔
256

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

269
                // Get the amount left after covering the input's own budget.
270
                // This amount can then be lent to the above input.
271
                budget := inp.params.Budget
9✔
272
                output := btcutil.Amount(inp.SignDesc().Output.Value)
9✔
273
                budgetBorrowable += output - budget
9✔
274

9✔
275
                // If the input's budget is not even covered by itself, we need
9✔
276
                // to borrow outputs from other inputs.
9✔
277
                if budgetBorrowable < 0 {
10✔
278
                        log.Tracef("Input %v specified a budget that exceeds "+
1✔
279
                                "its output value: %v > %v", inp, budget,
1✔
280
                                output)
1✔
281
                }
1✔
282
        }
283

284
        log.Debugf("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
9✔
285
                budgetNeeded, budgetBorrowable)
9✔
286

9✔
287
        // If we don't have enough extra budget to borrow, we need wallet
9✔
288
        // inputs.
9✔
289
        return budgetBorrowable < budgetNeeded
9✔
290
}
291

292
// copyInputs returns a copy of the slice of the inputs in the set.
293
func (b *BudgetInputSet) copyInputs() []*SweeperInput {
4✔
294
        inputs := make([]*SweeperInput, len(b.inputs))
4✔
295
        copy(inputs, b.inputs)
4✔
296
        return inputs
4✔
297
}
4✔
298

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

322
        // Sort the UTXOs by putting smaller values at the start of the slice
323
        // to avoid locking large UTXO for sweeping.
324
        //
325
        // TODO(yy): add more choices to CoinSelectionStrategy and use the
326
        // configured value here.
327
        sort.Slice(utxos, func(i, j int) bool {
5✔
328
                return utxos[i].Value < utxos[j].Value
1✔
329
        })
1✔
330

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

4✔
336
        // Add wallet inputs to the set until the specified budget is covered.
4✔
337
        for _, utxo := range utxos {
8✔
338
                input, err := createWalletTxInput(utxo)
4✔
339
                if err != nil {
5✔
340
                        return err
1✔
341
                }
1✔
342

343
                pi := SweeperInput{
3✔
344
                        Input: input,
3✔
345
                        params: Params{
3✔
346
                                DeadlineHeight: fn.Some(b.deadlineHeight),
3✔
347
                        },
3✔
348
                }
3✔
349
                b.addInput(pi)
3✔
350

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

3✔
354
                // Return if we've reached the minimum output amount.
3✔
355
                if !b.NeedWalletInput() {
4✔
356
                        return nil
1✔
357
                }
1✔
358
        }
359

360
        // The wallet doesn't have enough utxos to cover the budget. Revert the
361
        // input set to its original state.
362
        b.inputs = originalInputs
2✔
363

2✔
364
        return ErrNotEnoughInputs
2✔
365
}
366

367
// Budget returns the total budget of the set.
368
//
369
// NOTE: part of the InputSet interface.
370
func (b *BudgetInputSet) Budget() btcutil.Amount {
23✔
371
        budget := btcutil.Amount(0)
23✔
372
        for _, input := range b.inputs {
60✔
373
                budget += input.params.Budget
37✔
374
        }
37✔
375

376
        // We'll also tack on the extra budget which will eventually be
377
        // accounted for by the wallet txns when we're broadcasting.
378
        return budget + b.extraBudget
23✔
379
}
380

381
// DeadlineHeight returns the deadline height of the set.
382
//
383
// NOTE: part of the InputSet interface.
384
func (b *BudgetInputSet) DeadlineHeight() int32 {
22✔
385
        return b.deadlineHeight
22✔
386
}
22✔
387

388
// Inputs returns the inputs that should be used to create a tx.
389
//
390
// NOTE: part of the InputSet interface.
391
func (b *BudgetInputSet) Inputs() []input.Input {
10✔
392
        inputs := make([]input.Input, 0, len(b.inputs))
10✔
393
        for _, inp := range b.inputs {
25✔
394
                inputs = append(inputs, inp.Input)
15✔
395
        }
15✔
396

397
        return inputs
10✔
398
}
399

400
// StartingFeeRate returns the max starting fee rate found in the inputs.
401
//
402
// NOTE: part of the InputSet interface.
403
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
×
404
        maxFeeRate := chainfee.SatPerKWeight(0)
×
405
        startingFeeRate := fn.None[chainfee.SatPerKWeight]()
×
406

×
407
        for _, inp := range b.inputs {
×
408
                feerate := inp.params.StartingFeeRate.UnwrapOr(0)
×
409
                if feerate > maxFeeRate {
×
410
                        maxFeeRate = feerate
×
411
                        startingFeeRate = fn.Some(maxFeeRate)
×
412
                }
×
413
        }
414

415
        return startingFeeRate
×
416
}
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