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

lightningnetwork / lnd / 13586005509

28 Feb 2025 10:14AM UTC coverage: 68.629% (+9.9%) from 58.77%
13586005509

Pull #9521

github

web-flow
Merge 37d3a70a5 into 8532955b3
Pull Request #9521: unit: remove GOACC, use Go 1.20 native coverage functionality

129950 of 189351 relevant lines covered (68.63%)

23726.46 hits per line

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

92.74
/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.
23
func (e *ErrInsufficientFunds) Error() string {
3✔
24
        return fmt.Sprintf("not enough witness outputs to create funding "+
3✔
25
                "transaction, need %v only have %v available",
3✔
26
                e.amountAvailable, e.amountSelected)
3✔
27
}
3✔
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) {
146✔
75

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

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

84
        satSelected := btcutil.Amount(0)
146✔
85
        for i, coin := range arrangedCoins {
475✔
86
                satSelected += btcutil.Amount(coin.Value)
329✔
87
                if satSelected >= amt {
460✔
88
                        return satSelected, arrangedCoins[:i+1], nil
131✔
89
                }
131✔
90
        }
91

92
        return 0, nil, &ErrInsufficientFunds{amt, satSelected}
18✔
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) {
134✔
101

134✔
102
        weightEstimate := existingWeight
134✔
103
        for _, utxo := range utxos {
388✔
104
                switch {
254✔
105
                case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
230✔
106
                        weightEstimate.AddP2WKHInput()
230✔
107

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

111
                case txscript.IsPayToTaproot(utxo.PkScript):
25✔
112
                        weightEstimate.AddTaprootKeySpendInput(
25✔
113
                                txscript.SigHashDefault,
25✔
114
                        )
25✔
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()
133✔
124
        requiredFeeNoChange := feeRate.FeeForWeight(totalWeight)
133✔
125

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

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

134
        case ExistingChangeAddress:
4✔
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()
133✔
145
        requiredFeeWithChange := feeRate.FeeForWeight(totalWeight)
133✔
146

133✔
147
        return requiredFeeNoChange, requiredFeeWithChange, nil
133✔
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 {
121✔
153
        // Sanity check the maxFeeRatio itself.
121✔
154
        if maxFeeRatio <= 0.00 || maxFeeRatio > 1.00 {
122✔
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)
120✔
160

120✔
161
        // Check that the fees do not exceed the max allowed value.
120✔
162
        if fee > maxFee {
124✔
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
116✔
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) {
104✔
181

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

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

202
                changeAmount, newAmtNeeded, err := CalculateChangeAmount(
115✔
203
                        totalSat, amt, requiredFeeNoChange,
115✔
204
                        requiredFeeWithChange, dustLimit, changeType,
115✔
205
                        maxFeeRatio,
115✔
206
                )
115✔
207
                if err != nil {
117✔
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 {
139✔
214
                        amtNeeded = newAmtNeeded
26✔
215

26✔
216
                        continue
26✔
217
                }
218

219
                // Coin selection was successful.
220
                return selectedUtxos, changeAmount, nil
87✔
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) {
123✔
236

123✔
237
        // This is just a sanity check to make sure the function is used
123✔
238
        // correctly.
123✔
239
        if changeType == ExistingChangeAddress &&
123✔
240
                requiredFeeNoChange != requiredFeeWithChange {
124✔
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
122✔
250

122✔
251
        var changeAmt btcutil.Amount
122✔
252

122✔
253
        switch {
122✔
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:
92✔
266
                changeAmt = overShootAmt - requiredFeeWithChange
92✔
267

268
        // Otherwise we have selected enough to pay for a tx without a
269
        // change output.
270
        default:
6✔
271
                changeAmt = 0
6✔
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 {
102✔
279
                changeAmt = 0
7✔
280
        }
7✔
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
95✔
285

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

291
        return changeAmt, 0, nil
91✔
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) {
19✔
303

19✔
304
        // First perform an initial round of coin selection to estimate
19✔
305
        // the required fee.
19✔
306
        totalSat, selectedUtxos, err := selectInputs(
19✔
307
                amt, coins, strategy, feeRate,
19✔
308
        )
19✔
309
        if err != nil {
19✔
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(
19✔
316
                selectedUtxos, feeRate, existingWeight, changeType,
19✔
317
        )
19✔
318
        if err != nil {
19✔
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
19✔
325
        changeAmt := btcutil.Amount(0)
19✔
326

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

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

17✔
340
        // If adding a change output leads to both outputs being above
17✔
341
        // the dust limit, we'll add the change output. Otherwise we'll
17✔
342
        // go with the no change tx we originally found.
17✔
343
        if newChange >= dustLimit && newOutput >= dustLimit {
23✔
344
                outputAmt = newOutput
6✔
345
                changeAmt = newChange
6✔
346
        }
6✔
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
17✔
351
        err = sanityCheckFee(totalOut, totalSat-totalOut, maxFeeRatio)
17✔
352
        if err != nil {
18✔
353
                return nil, 0, 0, err
1✔
354
        }
1✔
355

356
        return selectedUtxos, outputAmt, changeAmt, nil
16✔
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) {
17✔
369

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

17✔
378
        // Get total balance from coins which we need for reserve considerations
17✔
379
        // and fee sanity checks.
17✔
380
        var totalBalance btcutil.Amount
17✔
381
        for _, coin := range coins {
36✔
382
                totalBalance += btcutil.Amount(coin.Value)
19✔
383
        }
19✔
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(
17✔
388
                feeRate, maxAmount, dustLimit, coins, strategy, existingWeight,
17✔
389
                changeType, maxFeeRatio,
17✔
390
        )
17✔
391

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

10✔
399
                // First we sum up the value of all selected coins.
10✔
400
                var sumSelected btcutil.Amount
10✔
401
                for _, coin := range selected {
21✔
402
                        sumSelected += btcutil.Amount(coin.Value)
11✔
403
                }
11✔
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
10✔
408

10✔
409
                // Next we check if our total balance can cover for the selected
10✔
410
                // output plus the optional anchor reserve.
10✔
411
                if totalBalance-sumSelected < reserved {
11✔
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):
9✔
419
                // If the initial coin selection fails due to insufficient funds
9✔
420
                // we select our total available balance minus fees.
9✔
421
                selectSubtractFee = true
9✔
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 {
26✔
430
                selected, outputAmount, changeAmt, err = CoinSelectSubtractFees(
10✔
431
                        feeRate, totalBalance-reserved, dustLimit, coins,
10✔
432
                        strategy, existingWeight, changeType, maxFeeRatio,
10✔
433
                )
10✔
434
                if err != nil {
14✔
435
                        return nil, 0, 0, err
4✔
436
                }
4✔
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
15✔
442
        sum := func(coins []wallet.Coin) btcutil.Amount {
30✔
443
                var sum btcutil.Amount
15✔
444
                for _, coin := range coins {
32✔
445
                        sum += btcutil.Amount(coin.Value)
17✔
446
                }
17✔
447

448
                return sum
15✔
449
        }
450
        err = sanityCheckFee(totalOut, sum(selected)-totalOut, maxFeeRatio)
15✔
451
        if err != nil {
15✔
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 {
18✔
460
                return nil, 0, 0, fmt.Errorf("available funds(%v) below the "+
3✔
461
                        "minimum amount(%v)", outputAmount, minAmount)
3✔
462
        }
3✔
463

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