• 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

89.52
/lnwallet/chanfunding/coin_select.go
1
package chanfunding
2

3
import (
4
        "errors"
5
        "fmt"
6

7
        "github.com/btcsuite/btcd/btcutil"
8
        "github.com/btcsuite/btcd/txscript"
9
        "github.com/btcsuite/btcwallet/wallet"
10
        "github.com/lightningnetwork/lnd/input"
11
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
12
)
13

14
// ErrInsufficientFunds is a type matching the error interface which is
15
// returned when coin selection for a new funding transaction fails due to
16
// having an insufficient amount of confirmed funds.
17
type ErrInsufficientFunds struct {
18
        amountAvailable btcutil.Amount
19
        amountSelected  btcutil.Amount
20
}
21

22
// Error returns a human-readable string describing the error.
UNCOV
23
func (e *ErrInsufficientFunds) Error() string {
×
UNCOV
24
        return fmt.Sprintf("not enough witness outputs to create funding "+
×
UNCOV
25
                "transaction, need %v only have %v available",
×
UNCOV
26
                e.amountAvailable, e.amountSelected)
×
UNCOV
27
}
×
28

29
// errUnsupportedInput is a type matching the error interface, which is returned
30
// when trying to calculate the fee of a transaction that references an
31
// unsupported script in the outpoint of a transaction input.
32
type errUnsupportedInput struct {
33
        PkScript []byte
34
}
35

36
// Error returns a human-readable string describing the error.
37
func (e *errUnsupportedInput) Error() string {
×
38
        return fmt.Sprintf("unsupported address type: %x", e.PkScript)
×
39
}
×
40

41
// ChangeAddressType is an enum-like type that describes the type of change
42
// address that should be generated for a transaction.
43
type ChangeAddressType uint8
44

45
const (
46
        // P2WKHChangeAddress indicates that the change output should be a
47
        // P2WKH output.
48
        P2WKHChangeAddress ChangeAddressType = 0
49

50
        // P2TRChangeAddress indicates that the change output should be a
51
        // P2TR output.
52
        P2TRChangeAddress ChangeAddressType = 1
53

54
        // ExistingChangeAddress indicates that the coin selection algorithm
55
        // should assume an existing output will be used for any change, meaning
56
        // that the change amount calculated will be added to an existing output
57
        // and no weight for a new change output should be assumed. The caller
58
        // must assert that the output value of the selected existing output
59
        // already is above dust when using this change address type.
60
        ExistingChangeAddress ChangeAddressType = 2
61

62
        // DefaultMaxFeeRatio is the default fee to total amount of outputs
63
        // ratio that is used to sanity check the fees of a transaction.
64
        DefaultMaxFeeRatio float64 = 0.2
65
)
66

67
// selectInputs selects a slice of inputs necessary to meet the specified
68
// selection amount. If input selection is unable to succeed due to insufficient
69
// funds, a non-nil error is returned. Additionally, the total amount of the
70
// selected coins are returned in order for the caller to properly handle
71
// change+fees.
72
func selectInputs(amt btcutil.Amount, coins []wallet.Coin,
73
        strategy wallet.CoinSelectionStrategy,
74
        feeRate chainfee.SatPerKWeight) (btcutil.Amount, []wallet.Coin, error) {
143✔
75

143✔
76
        // All coin selection code in the btcwallet library requires sat/KB.
143✔
77
        feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte())
143✔
78

143✔
79
        arrangedCoins, err := strategy.ArrangeCoins(coins, feeSatPerKB)
143✔
80
        if err != nil {
143✔
81
                return 0, nil, err
×
82
        }
×
83

84
        satSelected := btcutil.Amount(0)
143✔
85
        for i, coin := range arrangedCoins {
469✔
86
                satSelected += btcutil.Amount(coin.Value)
326✔
87
                if satSelected >= amt {
454✔
88
                        return satSelected, arrangedCoins[:i+1], nil
128✔
89
                }
128✔
90
        }
91

92
        return 0, nil, &ErrInsufficientFunds{amt, satSelected}
15✔
93
}
94

95
// calculateFees returns for the specified utxos and fee rate two fee
96
// estimates, one calculated using a change output and one without. The weight
97
// added to the estimator from a change output is for a P2WKH output.
98
func calculateFees(utxos []wallet.Coin, feeRate chainfee.SatPerKWeight,
99
        existingWeight input.TxWeightEstimator,
100
        changeType ChangeAddressType) (btcutil.Amount, btcutil.Amount, error) {
131✔
101

131✔
102
        weightEstimate := existingWeight
131✔
103
        for _, utxo := range utxos {
382✔
104
                switch {
251✔
105
                case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
227✔
106
                        weightEstimate.AddP2WKHInput()
227✔
107

108
                case txscript.IsPayToScriptHash(utxo.PkScript):
1✔
109
                        weightEstimate.AddNestedP2WKHInput()
1✔
110

111
                case txscript.IsPayToTaproot(utxo.PkScript):
22✔
112
                        weightEstimate.AddTaprootKeySpendInput(
22✔
113
                                txscript.SigHashDefault,
22✔
114
                        )
22✔
115

116
                default:
1✔
117
                        return 0, 0, &errUnsupportedInput{utxo.PkScript}
1✔
118
                }
119
        }
120

121
        // Estimate the fee required for a transaction without a change
122
        // output.
123
        totalWeight := weightEstimate.Weight()
130✔
124
        requiredFeeNoChange := feeRate.FeeForWeight(totalWeight)
130✔
125

130✔
126
        // Estimate the fee required for a transaction with a change output.
130✔
127
        switch changeType {
130✔
128
        case P2WKHChangeAddress:
×
129
                weightEstimate.AddP2WKHOutput()
×
130

131
        case P2TRChangeAddress:
129✔
132
                weightEstimate.AddP2TROutput()
129✔
133

134
        case ExistingChangeAddress:
1✔
135
                // Don't add an extra output.
136

137
        default:
×
138
                return 0, 0, fmt.Errorf("unknown change address type: %v",
×
139
                        changeType)
×
140
        }
141

142
        // Now that we have added the change output, redo the fee
143
        // estimate.
144
        totalWeight = weightEstimate.Weight()
130✔
145
        requiredFeeWithChange := feeRate.FeeForWeight(totalWeight)
130✔
146

130✔
147
        return requiredFeeNoChange, requiredFeeWithChange, nil
130✔
148
}
149

150
// sanityCheckFee checks if the specified fee amounts to what the provided ratio
151
// allows.
152
func sanityCheckFee(totalOut, fee btcutil.Amount, maxFeeRatio float64) error {
118✔
153
        // Sanity check the maxFeeRatio itself.
118✔
154
        if maxFeeRatio <= 0.00 || maxFeeRatio > 1.00 {
119✔
155
                return fmt.Errorf("maxFeeRatio must be between 0.00 and 1.00 "+
1✔
156
                        "got %.2f", maxFeeRatio)
1✔
157
        }
1✔
158

159
        maxFee := btcutil.Amount(float64(totalOut) * maxFeeRatio)
117✔
160

117✔
161
        // Check that the fees do not exceed the max allowed value.
117✔
162
        if fee > maxFee {
121✔
163
                return fmt.Errorf("fee %v exceeds max fee (%v) on total "+
4✔
164
                        "output value %v with max fee ratio of %.2f", fee,
4✔
165
                        maxFee, totalOut, maxFeeRatio)
4✔
166
        }
4✔
167

168
        // All checks passed, we return nil to signal that the fees are valid.
169
        return nil
113✔
170
}
171

172
// CoinSelect attempts to select a sufficient amount of coins, including a
173
// change output to fund amt satoshis, adhering to the specified fee rate. The
174
// specified fee rate should be expressed in sat/kw for coin selection to
175
// function properly.
176
func CoinSelect(feeRate chainfee.SatPerKWeight, amt, dustLimit btcutil.Amount,
177
        coins []wallet.Coin, strategy wallet.CoinSelectionStrategy,
178
        existingWeight input.TxWeightEstimator,
179
        changeType ChangeAddressType, maxFeeRatio float64) ([]wallet.Coin,
180
        btcutil.Amount, error) {
101✔
181

101✔
182
        amtNeeded := amt
101✔
183
        for {
228✔
184
                // First perform an initial round of coin selection to estimate
127✔
185
                // the required fee.
127✔
186
                totalSat, selectedUtxos, err := selectInputs(
127✔
187
                        amtNeeded, coins, strategy, feeRate,
127✔
188
                )
127✔
189
                if err != nil {
142✔
190
                        return nil, 0, err
15✔
191
                }
15✔
192

193
                // Obtain fee estimates both with and without using a change
194
                // output.
195
                requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
112✔
196
                        selectedUtxos, feeRate, existingWeight, changeType,
112✔
197
                )
112✔
198
                if err != nil {
112✔
199
                        return nil, 0, err
×
200
                }
×
201

202
                changeAmount, newAmtNeeded, err := CalculateChangeAmount(
112✔
203
                        totalSat, amt, requiredFeeNoChange,
112✔
204
                        requiredFeeWithChange, dustLimit, changeType,
112✔
205
                        maxFeeRatio,
112✔
206
                )
112✔
207
                if err != nil {
114✔
208
                        return nil, 0, err
2✔
209
                }
2✔
210

211
                // Need another round, the selected coins aren't enough to pay
212
                // for the fees.
213
                if newAmtNeeded != 0 {
136✔
214
                        amtNeeded = newAmtNeeded
26✔
215

26✔
216
                        continue
26✔
217
                }
218

219
                // Coin selection was successful.
220
                return selectedUtxos, changeAmount, nil
84✔
221
        }
222
}
223

224
// CalculateChangeAmount calculates the change amount being left over when the
225
// given total amount of sats is provided as inputs for the required output
226
// amount. The calculation takes into account that we might not want to add a
227
// change output if the change amount is below the dust limit. The first amount
228
// returned is the change amount. If that is non-zero, change is left over and
229
// should be dealt with. The second amount, if non-zero, indicates that the
230
// total input amount was just not enough to pay for the required amount and
231
// fees and that more coins need to be selected.
232
func CalculateChangeAmount(totalInputAmt, requiredAmt, requiredFeeNoChange,
233
        requiredFeeWithChange, dustLimit btcutil.Amount,
234
        changeType ChangeAddressType, maxFeeRatio float64) (btcutil.Amount,
235
        btcutil.Amount, error) {
120✔
236

120✔
237
        // This is just a sanity check to make sure the function is used
120✔
238
        // correctly.
120✔
239
        if changeType == ExistingChangeAddress &&
120✔
240
                requiredFeeNoChange != requiredFeeWithChange {
121✔
241

1✔
242
                return 0, 0, fmt.Errorf("when using existing change address, " +
1✔
243
                        "the fees for with or without change must be the same")
1✔
244
        }
1✔
245

246
        // The difference between the selected amount and the amount
247
        // requested will be used to pay fees, and generate a change
248
        // output with the remaining.
249
        overShootAmt := totalInputAmt - requiredAmt
119✔
250

119✔
251
        var changeAmt btcutil.Amount
119✔
252

119✔
253
        switch {
119✔
254
        // If the excess amount isn't enough to pay for fees based on
255
        // fee rate and estimated size without using a change output,
256
        // then increase the requested coin amount by the estimate
257
        // required fee without using change, performing another round
258
        // of coin selection.
259
        case overShootAmt < requiredFeeNoChange:
27✔
260
                return 0, requiredAmt + requiredFeeNoChange, nil
27✔
261

262
        // If sufficient funds were selected to cover the fee required
263
        // to include a change output, the remainder will be our change
264
        // amount.
265
        case overShootAmt > requiredFeeWithChange:
89✔
266
                changeAmt = overShootAmt - requiredFeeWithChange
89✔
267

268
        // Otherwise we have selected enough to pay for a tx without a
269
        // change output.
270
        default:
3✔
271
                changeAmt = 0
3✔
272
        }
273

274
        // In case we would end up with a dust output if we created a
275
        // change output, we instead just let the dust amount go to
276
        // fees. Unless we want the change to go to an existing output,
277
        // in that case we can increase that output value by any amount.
278
        if changeAmt < dustLimit && changeType != ExistingChangeAddress {
96✔
279
                changeAmt = 0
4✔
280
        }
4✔
281

282
        // Sanity check the resulting output values to make sure we
283
        // don't burn a great part to fees.
284
        totalOut := requiredAmt + changeAmt
92✔
285

92✔
286
        err := sanityCheckFee(totalOut, totalInputAmt-totalOut, maxFeeRatio)
92✔
287
        if err != nil {
96✔
288
                return 0, 0, err
4✔
289
        }
4✔
290

291
        return changeAmt, 0, nil
88✔
292
}
293

294
// CoinSelectSubtractFees attempts to select coins such that we'll spend up to
295
// amt in total after fees, adhering to the specified fee rate. The selected
296
// coins, the final output and change values are returned.
297
func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
298
        dustLimit btcutil.Amount, coins []wallet.Coin,
299
        strategy wallet.CoinSelectionStrategy,
300
        existingWeight input.TxWeightEstimator, changeType ChangeAddressType,
301
        maxFeeRatio float64) ([]wallet.Coin, btcutil.Amount, btcutil.Amount,
302
        error) {
16✔
303

16✔
304
        // First perform an initial round of coin selection to estimate
16✔
305
        // the required fee.
16✔
306
        totalSat, selectedUtxos, err := selectInputs(
16✔
307
                amt, coins, strategy, feeRate,
16✔
308
        )
16✔
309
        if err != nil {
16✔
310
                return nil, 0, 0, err
×
311
        }
×
312

313
        // Obtain fee estimates both with and without using a change
314
        // output.
315
        requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
16✔
316
                selectedUtxos, feeRate, existingWeight, changeType,
16✔
317
        )
16✔
318
        if err != nil {
16✔
319
                return nil, 0, 0, err
×
320
        }
×
321

322
        // For a transaction without a change output, we'll let everything go
323
        // to our multi-sig output after subtracting fees.
324
        outputAmt := totalSat - requiredFeeNoChange
16✔
325
        changeAmt := btcutil.Amount(0)
16✔
326

16✔
327
        // If the output is too small after subtracting the fee, the coin
16✔
328
        // selection cannot be performed with an amount this small.
16✔
329
        if outputAmt < dustLimit {
18✔
330
                return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
2✔
331
                        "subtracting fees(%v) below dust limit(%v)", outputAmt,
2✔
332
                        requiredFeeNoChange, dustLimit)
2✔
333
        }
2✔
334

335
        // For a transaction with a change output, everything we don't spend
336
        // will go to change.
337
        newOutput := amt - requiredFeeWithChange
14✔
338
        newChange := totalSat - amt
14✔
339

14✔
340
        // If adding a change output leads to both outputs being above
14✔
341
        // the dust limit, we'll add the change output. Otherwise we'll
14✔
342
        // go with the no change tx we originally found.
14✔
343
        if newChange >= dustLimit && newOutput >= dustLimit {
17✔
344
                outputAmt = newOutput
3✔
345
                changeAmt = newChange
3✔
346
        }
3✔
347

348
        // Sanity check the resulting output values to make sure we
349
        // don't burn a great part to fees.
350
        totalOut := outputAmt + changeAmt
14✔
351
        err = sanityCheckFee(totalOut, totalSat-totalOut, maxFeeRatio)
14✔
352
        if err != nil {
15✔
353
                return nil, 0, 0, err
1✔
354
        }
1✔
355

356
        return selectedUtxos, outputAmt, changeAmt, nil
13✔
357
}
358

359
// CoinSelectUpToAmount attempts to select coins such that we'll select up to
360
// maxAmount exclusive of fees and optional reserve if sufficient funds are
361
// available. If insufficient funds are available this method selects all
362
// available coins.
363
func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
364
        reserved, dustLimit btcutil.Amount, coins []wallet.Coin,
365
        strategy wallet.CoinSelectionStrategy,
366
        existingWeight input.TxWeightEstimator,
367
        changeType ChangeAddressType, maxFeeRatio float64) ([]wallet.Coin,
368
        btcutil.Amount, btcutil.Amount, error) {
14✔
369

14✔
370
        var (
14✔
371
                // selectSubtractFee is tracking if our coin selection was
14✔
372
                // unsuccessful and whether we have to start a new round of
14✔
373
                // selecting coins considering fees.
14✔
374
                selectSubtractFee = false
14✔
375
                outputAmount      = maxAmount
14✔
376
        )
14✔
377

14✔
378
        // Get total balance from coins which we need for reserve considerations
14✔
379
        // and fee sanity checks.
14✔
380
        var totalBalance btcutil.Amount
14✔
381
        for _, coin := range coins {
30✔
382
                totalBalance += btcutil.Amount(coin.Value)
16✔
383
        }
16✔
384

385
        // First we try to select coins to create an output of the specified
386
        // maxAmount with or without a change output that covers the miner fee.
387
        selected, changeAmt, err := CoinSelect(
14✔
388
                feeRate, maxAmount, dustLimit, coins, strategy, existingWeight,
14✔
389
                changeType, maxFeeRatio,
14✔
390
        )
14✔
391

14✔
392
        var errInsufficientFunds *ErrInsufficientFunds
14✔
393
        switch {
14✔
394
        case err == nil:
7✔
395
                // If the coin selection succeeds we check if our total balance
7✔
396
                // covers the selected set of coins including fees plus an
7✔
397
                // optional anchor reserve.
7✔
398

7✔
399
                // First we sum up the value of all selected coins.
7✔
400
                var sumSelected btcutil.Amount
7✔
401
                for _, coin := range selected {
15✔
402
                        sumSelected += btcutil.Amount(coin.Value)
8✔
403
                }
8✔
404

405
                // We then subtract the change amount from the value of all
406
                // selected coins to obtain the actual amount that is selected.
407
                sumSelected -= changeAmt
7✔
408

7✔
409
                // Next we check if our total balance can cover for the selected
7✔
410
                // output plus the optional anchor reserve.
7✔
411
                if totalBalance-sumSelected < reserved {
8✔
412
                        // If our local balance is insufficient to cover for the
1✔
413
                        // reserve we try to select an output amount that uses
1✔
414
                        // our total balance minus reserve and fees.
1✔
415
                        selectSubtractFee = true
1✔
416
                }
1✔
417

418
        case errors.As(err, &errInsufficientFunds):
6✔
419
                // If the initial coin selection fails due to insufficient funds
6✔
420
                // we select our total available balance minus fees.
6✔
421
                selectSubtractFee = true
6✔
422

423
        default:
1✔
424
                return nil, 0, 0, err
1✔
425
        }
426

427
        // If we determined that our local balance is insufficient we check
428
        // our total balance minus fees and optional reserve.
429
        if selectSubtractFee {
20✔
430
                selected, outputAmount, changeAmt, err = CoinSelectSubtractFees(
7✔
431
                        feeRate, totalBalance-reserved, dustLimit, coins,
7✔
432
                        strategy, existingWeight, changeType, maxFeeRatio,
7✔
433
                )
7✔
434
                if err != nil {
8✔
435
                        return nil, 0, 0, err
1✔
436
                }
1✔
437
        }
438

439
        // Sanity check the resulting output values to make sure we don't burn a
440
        // great part to fees.
441
        totalOut := outputAmount + changeAmt
12✔
442
        sum := func(coins []wallet.Coin) btcutil.Amount {
24✔
443
                var sum btcutil.Amount
12✔
444
                for _, coin := range coins {
26✔
445
                        sum += btcutil.Amount(coin.Value)
14✔
446
                }
14✔
447

448
                return sum
12✔
449
        }
450
        err = sanityCheckFee(totalOut, sum(selected)-totalOut, maxFeeRatio)
12✔
451
        if err != nil {
12✔
452
                return nil, 0, 0, err
×
453
        }
×
454

455
        // In case the selected amount is lower than minimum funding amount we
456
        // must return an error. The minimum funding amount is determined
457
        // upstream and denotes either the minimum viable channel size or an
458
        // amount sufficient to cover for the initial remote balance.
459
        if outputAmount < minAmount {
12✔
UNCOV
460
                return nil, 0, 0, fmt.Errorf("available funds(%v) below the "+
×
UNCOV
461
                        "minimum amount(%v)", outputAmount, minAmount)
×
UNCOV
462
        }
×
463

464
        return selected, outputAmount, changeAmt, nil
12✔
465
}
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