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

lightningnetwork / lnd / 12430766295

20 Dec 2024 11:38AM UTC coverage: 52.607% (-6.1%) from 58.716%
12430766295

Pull #9384

github

ziggie1984
funding: refactor gossip msg code

We almost never need to create all messages at the same time
(ChanUpdate,ChanAnnouncement,Proof) so we split it up into own
functions.
Pull Request #9384: Refactor gossip msg code

224 of 279 new or added lines in 7 files covered. (80.29%)

27070 existing lines in 437 files now uncovered.

53540 of 101773 relevant lines covered (52.61%)

4.11 hits per line

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

88.37
/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) {
72
        signDesc := &input.SignDescriptor{
73
                Output: &wire.TxOut{
74
                        PkScript: utxo.PkScript,
75
                        Value:    int64(utxo.Value),
76
                },
77
                HashType: txscript.SigHashAll,
78
        }
4✔
79

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

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

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

103
// BudgetInputSet implements the interface `InputSet`. It takes a list of
4✔
104
// pending inputs which share the same deadline height and groups them into a
4✔
105
// set conditionally based on their economical values.
4✔
106
type BudgetInputSet struct {
4✔
107
        // inputs is the set of inputs that have been added to the set after
4✔
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 {
131
        // Sanity check the input slice to ensure it's non-empty.
132
        if len(inputs) == 0 {
133
                return errEmptyInputs
134
        }
135

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

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

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

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

UNCOV
162
        // Make sure the inputs share the same deadline height when there is
×
163
        // one.
164
        if inputDeadline != deadlineHeight {
165
                return fmt.Errorf("input deadline height not matched: want "+
4✔
166
                        "%d, got %d", deadlineHeight, inputDeadline)
167
        }
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) {
4✔
UNCOV
172
                return fmt.Errorf("duplicate inputs")
×
UNCOV
173
        }
×
UNCOV
174

×
175
        return nil
176
}
177

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

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

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

×
UNCOV
192
        for _, input := range inputs {
×
193
                bi.addInput(input)
194
        }
4✔
195

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

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

4✔
204
        return bi, nil
4✔
205
}
4✔
206

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

4✔
UNCOV
219
        b.extraBudget = extraBudget
×
UNCOV
220

×
221
        return nil
222
}
4✔
223

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

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

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

4✔
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.
4✔
244
func (b *BudgetInputSet) NeedWalletInput() bool {
4✔
245
        var (
4✔
246
                // budgetNeeded is the amount that needs to be covered from
247
                // other inputs. We start at the value of the extra budget,
248
                // which might be needed for custom channels that add extra
249
                // outputs.
250
                budgetNeeded = b.extraBudget
251

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

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

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

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

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

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

4✔
292
// copyInputs returns a copy of the slice of the inputs in the set.
4✔
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
}
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
4✔
301
// specified, it cannot be directly spent from these required outputs. Instead,
4✔
302
// we need to borrow budget from other inputs to make the sweep happen.
4✔
303
// There are two sources to borrow from: 1) other inputs, 2) wallet utxos. If
4✔
304
// we are calling this method, it means other inputs cannot cover the specified
4✔
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 {
312
        // Retrieve wallet utxos. Only consider confirmed utxos to prevent
313
        // problems around RBF rules for unconfirmed inputs. This currently
314
        // ignores the configured coin selection strategy.
315
        utxos, err := wallet.ListUnspentWitnessFromDefaultAccount(
316
                1, math.MaxInt32,
317
        )
318
        if err != nil {
4✔
319
                return fmt.Errorf("list unspent witness: %w", err)
4✔
320
        }
4✔
321

4✔
322
        // Sort the UTXOs by putting smaller values at the start of the slice
4✔
323
        // to avoid locking large UTXO for sweeping.
4✔
324
        //
4✔
325
        // TODO(yy): add more choices to CoinSelectionStrategy and use the
4✔
UNCOV
326
        // configured value here.
×
UNCOV
327
        sort.Slice(utxos, func(i, j int) bool {
×
328
                return utxos[i].Value < utxos[j].Value
329
        })
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()
8✔
335

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

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

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

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

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

4✔
364
        return ErrNotEnoughInputs
4✔
365
}
366

367
// Budget returns the total budget of the set.
368
//
369
// NOTE: part of the InputSet interface.
4✔
370
func (b *BudgetInputSet) Budget() btcutil.Amount {
4✔
371
        budget := btcutil.Amount(0)
4✔
372
        for _, input := range b.inputs {
373
                budget += input.params.Budget
374
        }
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.
4✔
378
        return budget + b.extraBudget
4✔
379
}
8✔
380

4✔
381
// DeadlineHeight returns the deadline height of the set.
4✔
382
//
383
// NOTE: part of the InputSet interface.
384
func (b *BudgetInputSet) DeadlineHeight() int32 {
385
        return b.deadlineHeight
4✔
386
}
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 {
4✔
392
        inputs := make([]input.Input, 0, len(b.inputs))
4✔
393
        for _, inp := range b.inputs {
4✔
394
                inputs = append(inputs, inp.Input)
395
        }
396

397
        return inputs
398
}
4✔
399

4✔
400
// StartingFeeRate returns the max starting fee rate found in the inputs.
8✔
401
//
4✔
402
// NOTE: part of the InputSet interface.
4✔
403
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
404
        maxFeeRate := chainfee.SatPerKWeight(0)
4✔
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
4✔
411
                        startingFeeRate = fn.Some(maxFeeRate)
4✔
412
                }
4✔
413
        }
4✔
414

8✔
415
        return startingFeeRate
4✔
416
}
7✔
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