• 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

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"
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
×
UNCOV
86
        case lnwallet.TaprootPubkey:
×
UNCOV
87
                witnessType = input.TaprootPubKeySpend
×
UNCOV
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

116
// Compile-time constraint to ensure budgetInputSet implements InputSet.
117
var _ InputSet = (*BudgetInputSet)(nil)
118

119
// validateInputs is used when creating new BudgetInputSet to ensure there are
120
// no duplicate inputs and they all share the same deadline heights, if set.
121
func validateInputs(inputs []SweeperInput, deadlineHeight int32) error {
122
        // Sanity check the input slice to ensure it's non-empty.
123
        if len(inputs) == 0 {
124
                return fmt.Errorf("inputs slice is empty")
125
        }
126

127
        // inputDeadline tracks the input's deadline height. It will be updated
128
        // if the input has a different deadline than the specified
129
        // deadlineHeight.
130
        inputDeadline := deadlineHeight
24✔
131

24✔
132
        // dedupInputs is a set used to track unique outpoints of the inputs.
26✔
133
        dedupInputs := fn.NewSet(
2✔
134
                // Iterate all the inputs and map the function.
2✔
135
                fn.Map(func(inp SweeperInput) wire.OutPoint {
136
                        // If the input has a deadline height, we'll check if
137
                        // it's the same as the specified.
138
                        inp.params.DeadlineHeight.WhenSome(func(h int32) {
139
                                // Exit early if the deadlines matched.
22✔
140
                                if h == deadlineHeight {
22✔
141
                                        return
22✔
142
                                }
22✔
143

22✔
144
                                // Update the deadline height if it's
56✔
145
                                // different.
34✔
146
                                inputDeadline = h
34✔
147
                        })
50✔
148

16✔
149
                        return inp.OutPoint()
28✔
150
                }, inputs)...,
12✔
151
        )
12✔
152

153
        // Make sure the inputs share the same deadline height when there is
154
        // one.
155
        if inputDeadline != deadlineHeight {
4✔
156
                return fmt.Errorf("input deadline height not matched: want "+
157
                        "%d, got %d", deadlineHeight, inputDeadline)
158
        }
34✔
159

160
        // Provide a defensive check to ensure that we don't have any duplicate
161
        // inputs within the set.
162
        if len(dedupInputs) != len(inputs) {
163
                return fmt.Errorf("duplicate inputs")
164
        }
25✔
165

3✔
166
        return nil
3✔
167
}
3✔
168

169
// NewBudgetInputSet creates a new BudgetInputSet.
170
func NewBudgetInputSet(inputs []SweeperInput,
171
        deadlineHeight int32) (*BudgetInputSet, error) {
20✔
172

1✔
173
        // Validate the supplied inputs.
1✔
174
        if err := validateInputs(inputs, deadlineHeight); err != nil {
175
                return nil, err
18✔
176
        }
177

178
        bi := &BudgetInputSet{
179
                deadlineHeight: deadlineHeight,
180
                inputs:         make([]*SweeperInput, 0, len(inputs)),
24✔
181
        }
24✔
182

24✔
183
        for _, input := range inputs {
30✔
184
                bi.addInput(input)
6✔
185
        }
6✔
186

187
        log.Tracef("Created %v", bi.String())
18✔
188

18✔
189
        return bi, nil
18✔
190
}
18✔
191

18✔
192
// String returns a human-readable description of the input set.
44✔
193
func (b *BudgetInputSet) String() string {
26✔
194
        inputsDesc := ""
26✔
195
        for _, input := range b.inputs {
196
                inputsDesc += fmt.Sprintf("\n%v", input)
18✔
197
        }
18✔
198

18✔
199
        return fmt.Sprintf("BudgetInputSet(budget=%v, deadline=%v, "+
18✔
200
                "inputs=[%v])", b.Budget(), b.DeadlineHeight(), inputsDesc)
18✔
UNCOV
201
}
×
UNCOV
202

×
203
// addInput adds an input to the input set.
204
func (b *BudgetInputSet) addInput(input SweeperInput) {
18✔
205
        b.inputs = append(b.inputs, &input)
206
}
207

208
// NeedWalletInput returns true if the input set needs more wallet inputs.
209
//
18✔
210
// A set may need wallet inputs when it has a required output or its total
18✔
211
// value cannot cover its total budget.
24✔
212
func (b *BudgetInputSet) NeedWalletInput() bool {
6✔
213
        var (
6✔
214
                // budgetNeeded is the amount that needs to be covered from
215
                // other inputs.
18✔
UNCOV
216
                budgetNeeded btcutil.Amount
×
UNCOV
217

×
218
                // budgetBorrowable is the amount that can be borrowed from
219
                // other inputs.
18✔
220
                budgetBorrowable btcutil.Amount
18✔
221
        )
18✔
222

223
        for _, inp := range b.inputs {
224
                // If this input has a required output, we can assume it's a
225
                // second-level htlc txns input. Although this input must have
18✔
226
                // a value that can cover its budget, it cannot be used to pay
18✔
227
                // fees. Instead, we need to borrow budget from other inputs to
44✔
228
                // make the sweep happen. Once swept, the input value will be
26✔
229
                // credited to the wallet.
26✔
230
                if inp.RequiredTxOut() != nil {
231
                        budgetNeeded += inp.params.Budget
18✔
232
                        continue
18✔
233
                }
234

235
                // Get the amount left after covering the input's own budget.
236
                // This amount can then be lent to the above input.
30✔
237
                budget := inp.params.Budget
30✔
238
                output := btcutil.Amount(inp.SignDesc().Output.Value)
30✔
239
                budgetBorrowable += output - budget
240

241
                // If the input's budget is not even covered by itself, we need
242
                // to borrow outputs from other inputs.
243
                if budgetBorrowable < 0 {
244
                        log.Tracef("Input %v specified a budget that exceeds "+
9✔
245
                                "its output value: %v > %v", inp, budget,
9✔
246
                                output)
9✔
247
                }
9✔
248
        }
9✔
249

9✔
250
        log.Debugf("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
9✔
251
                budgetNeeded, budgetBorrowable)
9✔
252

9✔
253
        // If we don't have enough extra budget to borrow, we need wallet
9✔
254
        // inputs.
9✔
255
        return budgetBorrowable < budgetNeeded
9✔
256
}
9✔
257

24✔
258
// copyInputs returns a copy of the slice of the inputs in the set.
15✔
259
func (b *BudgetInputSet) copyInputs() []*SweeperInput {
15✔
260
        inputs := make([]*SweeperInput, len(b.inputs))
15✔
261
        copy(inputs, b.inputs)
15✔
262
        return inputs
15✔
263
}
15✔
264

21✔
265
// AddWalletInputs adds wallet inputs to the set until the specified budget is
6✔
266
// met. When sweeping inputs with required outputs, although there's budget
6✔
267
// specified, it cannot be directly spent from these required outputs. Instead,
268
// we need to borrow budget from other inputs to make the sweep happen.
269
// There are two sources to borrow from: 1) other inputs, 2) wallet utxos. If
270
// we are calling this method, it means other inputs cannot cover the specified
271
// budget, so we need to borrow from wallet utxos.
9✔
272
//
9✔
273
// Return an error if there are not enough wallet inputs, and the budget set is
9✔
274
// set to its initial state by removing any wallet inputs added.
9✔
275
//
9✔
276
// NOTE: must be called with the wallet lock held via `WithCoinSelectLock`.
9✔
277
func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error {
10✔
278
        // Retrieve wallet utxos. Only consider confirmed utxos to prevent
1✔
279
        // problems around RBF rules for unconfirmed inputs. This currently
1✔
280
        // ignores the configured coin selection strategy.
1✔
281
        utxos, err := wallet.ListUnspentWitnessFromDefaultAccount(
1✔
282
                1, math.MaxInt32,
283
        )
284
        if err != nil {
9✔
285
                return fmt.Errorf("list unspent witness: %w", err)
9✔
286
        }
9✔
287

9✔
288
        // Sort the UTXOs by putting smaller values at the start of the slice
9✔
289
        // to avoid locking large UTXO for sweeping.
9✔
290
        //
291
        // TODO(yy): add more choices to CoinSelectionStrategy and use the
292
        // configured value here.
293
        sort.Slice(utxos, func(i, j int) bool {
4✔
294
                return utxos[i].Value < utxos[j].Value
4✔
295
        })
4✔
296

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

302
        // Add wallet inputs to the set until the specified budget is covered.
303
        for _, utxo := range utxos {
304
                input, err := createWalletTxInput(utxo)
305
                if err != nil {
306
                        return err
307
                }
308

309
                pi := SweeperInput{
310
                        Input: input,
311
                        params: Params{
5✔
312
                                DeadlineHeight: fn.Some(b.deadlineHeight),
5✔
313
                        },
5✔
314
                }
5✔
315
                b.addInput(pi)
5✔
316

5✔
317
                log.Debugf("Added wallet input to input set: op=%v, amt=%v",
5✔
318
                        pi.OutPoint(), utxo.Value)
6✔
319

1✔
320
                // Return if we've reached the minimum output amount.
1✔
321
                if !b.NeedWalletInput() {
322
                        return nil
323
                }
324
        }
325

326
        // The wallet doesn't have enough utxos to cover the budget. Revert the
327
        // input set to its original state.
5✔
328
        b.inputs = originalInputs
1✔
329

1✔
330
        return ErrNotEnoughInputs
331
}
332

333
// Budget returns the total budget of the set.
334
//
4✔
335
// NOTE: part of the InputSet interface.
4✔
336
func (b *BudgetInputSet) Budget() btcutil.Amount {
4✔
337
        budget := btcutil.Amount(0)
8✔
338
        for _, input := range b.inputs {
4✔
339
                budget += input.params.Budget
5✔
340
        }
1✔
341

1✔
342
        return budget
343
}
3✔
344

3✔
345
// DeadlineHeight returns the deadline height of the set.
3✔
346
//
3✔
347
// NOTE: part of the InputSet interface.
3✔
348
func (b *BudgetInputSet) DeadlineHeight() int32 {
3✔
349
        return b.deadlineHeight
3✔
350
}
3✔
351

3✔
352
// Inputs returns the inputs that should be used to create a tx.
3✔
353
//
3✔
354
// NOTE: part of the InputSet interface.
3✔
355
func (b *BudgetInputSet) Inputs() []input.Input {
4✔
356
        inputs := make([]input.Input, 0, len(b.inputs))
1✔
357
        for _, inp := range b.inputs {
1✔
358
                inputs = append(inputs, inp.Input)
359
        }
360

361
        return inputs
362
}
2✔
363

2✔
364
// StartingFeeRate returns the max starting fee rate found in the inputs.
2✔
365
//
366
// NOTE: part of the InputSet interface.
367
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
368
        maxFeeRate := chainfee.SatPerKWeight(0)
369
        startingFeeRate := fn.None[chainfee.SatPerKWeight]()
370

23✔
371
        for _, inp := range b.inputs {
23✔
372
                feerate := inp.params.StartingFeeRate.UnwrapOr(0)
60✔
373
                if feerate > maxFeeRate {
37✔
374
                        maxFeeRate = feerate
37✔
375
                        startingFeeRate = fn.Some(maxFeeRate)
376
                }
23✔
377
        }
378

379
        return startingFeeRate
380
}
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