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

lightningnetwork / lnd / 11170835610

03 Oct 2024 10:41PM UTC coverage: 49.188% (-9.6%) from 58.738%
11170835610

push

github

web-flow
Merge pull request #9154 from ziggie1984/master

multi: bump btcd version.

3 of 6 new or added lines in 6 files covered. (50.0%)

26110 existing lines in 428 files now uncovered.

97359 of 197934 relevant lines covered (49.19%)

1.04 hits per line

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

81.25
/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 {
2✔
24
        return fmt.Sprintf("not enough witness outputs to create funding "+
2✔
25
                "transaction, need %v only have %v available",
2✔
26
                e.amountAvailable, e.amountSelected)
2✔
27
}
2✔
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

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

2✔
72
        // All coin selection code in the btcwallet library requires sat/KB.
2✔
73
        feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte())
2✔
74

2✔
75
        arrangedCoins, err := strategy.ArrangeCoins(coins, feeSatPerKB)
2✔
76
        if err != nil {
2✔
77
                return 0, nil, err
×
78
        }
×
79

80
        satSelected := btcutil.Amount(0)
2✔
81
        for i, coin := range arrangedCoins {
4✔
82
                satSelected += btcutil.Amount(coin.Value)
2✔
83
                if satSelected >= amt {
4✔
84
                        return satSelected, arrangedCoins[:i+1], nil
2✔
85
                }
2✔
86
        }
87

88
        return 0, nil, &ErrInsufficientFunds{amt, satSelected}
2✔
89
}
90

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

2✔
98
        weightEstimate := existingWeight
2✔
99
        for _, utxo := range utxos {
4✔
100
                switch {
2✔
101
                case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
2✔
102
                        weightEstimate.AddP2WKHInput()
2✔
103

104
                case txscript.IsPayToScriptHash(utxo.PkScript):
2✔
105
                        weightEstimate.AddNestedP2WKHInput()
2✔
106

107
                case txscript.IsPayToTaproot(utxo.PkScript):
2✔
108
                        weightEstimate.AddTaprootKeySpendInput(
2✔
109
                                txscript.SigHashDefault,
2✔
110
                        )
2✔
111

UNCOV
112
                default:
×
UNCOV
113
                        return 0, 0, &errUnsupportedInput{utxo.PkScript}
×
114
                }
115
        }
116

117
        // Estimate the fee required for a transaction without a change
118
        // output.
119
        totalWeight := weightEstimate.Weight()
2✔
120
        requiredFeeNoChange := feeRate.FeeForWeight(totalWeight)
2✔
121

2✔
122
        // Estimate the fee required for a transaction with a change output.
2✔
123
        switch changeType {
2✔
124
        case P2WKHChangeAddress:
×
125
                weightEstimate.AddP2WKHOutput()
×
126

127
        case P2TRChangeAddress:
2✔
128
                weightEstimate.AddP2TROutput()
2✔
129

130
        case ExistingChangeAddress:
2✔
131
                // Don't add an extra output.
132

133
        default:
×
134
                return 0, 0, fmt.Errorf("unknown change address type: %v",
×
135
                        changeType)
×
136
        }
137

138
        // Now that we have added the change output, redo the fee
139
        // estimate.
140
        totalWeight = weightEstimate.Weight()
2✔
141
        requiredFeeWithChange := feeRate.FeeForWeight(totalWeight)
2✔
142

2✔
143
        return requiredFeeNoChange, requiredFeeWithChange, nil
2✔
144
}
145

146
// sanityCheckFee checks if the specified fee amounts to over 20% of the total
147
// output amount and raises an error.
148
func sanityCheckFee(totalOut, fee btcutil.Amount) error {
2✔
149
        // Fail if more than 20% goes to fees.
2✔
150
        // TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
2✔
151
        // total funding size?
2✔
152
        if fee > totalOut/5 {
2✔
UNCOV
153
                return fmt.Errorf("fee %v on total output value %v", fee,
×
UNCOV
154
                        totalOut)
×
UNCOV
155
        }
×
156
        return nil
2✔
157
}
158

159
// CoinSelect attempts to select a sufficient amount of coins, including a
160
// change output to fund amt satoshis, adhering to the specified fee rate. The
161
// specified fee rate should be expressed in sat/kw for coin selection to
162
// function properly.
163
func CoinSelect(feeRate chainfee.SatPerKWeight, amt, dustLimit btcutil.Amount,
164
        coins []wallet.Coin, strategy wallet.CoinSelectionStrategy,
165
        existingWeight input.TxWeightEstimator,
166
        changeType ChangeAddressType) ([]wallet.Coin, btcutil.Amount, error) {
2✔
167

2✔
168
        amtNeeded := amt
2✔
169
        for {
4✔
170
                // First perform an initial round of coin selection to estimate
2✔
171
                // the required fee.
2✔
172
                totalSat, selectedUtxos, err := selectInputs(
2✔
173
                        amtNeeded, coins, strategy, feeRate,
2✔
174
                )
2✔
175
                if err != nil {
4✔
176
                        return nil, 0, err
2✔
177
                }
2✔
178

179
                // Obtain fee estimates both with and without using a change
180
                // output.
181
                requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
2✔
182
                        selectedUtxos, feeRate, existingWeight, changeType,
2✔
183
                )
2✔
184
                if err != nil {
2✔
185
                        return nil, 0, err
×
186
                }
×
187

188
                changeAmount, newAmtNeeded, err := CalculateChangeAmount(
2✔
189
                        totalSat, amt, requiredFeeNoChange,
2✔
190
                        requiredFeeWithChange, dustLimit, changeType,
2✔
191
                )
2✔
192
                if err != nil {
2✔
UNCOV
193
                        return nil, 0, err
×
UNCOV
194
                }
×
195

196
                // Need another round, the selected coins aren't enough to pay
197
                // for the fees.
198
                if newAmtNeeded != 0 {
2✔
UNCOV
199
                        amtNeeded = newAmtNeeded
×
UNCOV
200

×
UNCOV
201
                        continue
×
202
                }
203

204
                // Coin selection was successful.
205
                return selectedUtxos, changeAmount, nil
2✔
206
        }
207
}
208

209
// CalculateChangeAmount calculates the change amount being left over when the
210
// given total amount of sats is provided as inputs for the required output
211
// amount. The calculation takes into account that we might not want to add a
212
// change output if the change amount is below the dust limit. The first amount
213
// returned is the change amount. If that is non-zero, change is left over and
214
// should be dealt with. The second amount, if non-zero, indicates that the
215
// total input amount was just not enough to pay for the required amount and
216
// fees and that more coins need to be selected.
217
func CalculateChangeAmount(totalInputAmt, requiredAmt, requiredFeeNoChange,
218
        requiredFeeWithChange, dustLimit btcutil.Amount,
219
        changeType ChangeAddressType) (btcutil.Amount, btcutil.Amount, error) {
2✔
220

2✔
221
        // This is just a sanity check to make sure the function is used
2✔
222
        // correctly.
2✔
223
        if changeType == ExistingChangeAddress &&
2✔
224
                requiredFeeNoChange != requiredFeeWithChange {
2✔
UNCOV
225

×
UNCOV
226
                return 0, 0, fmt.Errorf("when using existing change address, " +
×
UNCOV
227
                        "the fees for with or without change must be the same")
×
UNCOV
228
        }
×
229

230
        // The difference between the selected amount and the amount
231
        // requested will be used to pay fees, and generate a change
232
        // output with the remaining.
233
        overShootAmt := totalInputAmt - requiredAmt
2✔
234

2✔
235
        var changeAmt btcutil.Amount
2✔
236

2✔
237
        switch {
2✔
238
        // If the excess amount isn't enough to pay for fees based on
239
        // fee rate and estimated size without using a change output,
240
        // then increase the requested coin amount by the estimate
241
        // required fee without using change, performing another round
242
        // of coin selection.
UNCOV
243
        case overShootAmt < requiredFeeNoChange:
×
UNCOV
244
                return 0, requiredAmt + requiredFeeNoChange, nil
×
245

246
        // If sufficient funds were selected to cover the fee required
247
        // to include a change output, the remainder will be our change
248
        // amount.
249
        case overShootAmt > requiredFeeWithChange:
2✔
250
                changeAmt = overShootAmt - requiredFeeWithChange
2✔
251

252
        // Otherwise we have selected enough to pay for a tx without a
253
        // change output.
254
        default:
2✔
255
                changeAmt = 0
2✔
256
        }
257

258
        // In case we would end up with a dust output if we created a
259
        // change output, we instead just let the dust amount go to
260
        // fees. Unless we want the change to go to an existing output,
261
        // in that case we can increase that output value by any amount.
262
        if changeAmt < dustLimit && changeType != ExistingChangeAddress {
4✔
263
                changeAmt = 0
2✔
264
        }
2✔
265

266
        // Sanity check the resulting output values to make sure we
267
        // don't burn a great part to fees.
268
        totalOut := requiredAmt + changeAmt
2✔
269
        err := sanityCheckFee(totalOut, totalInputAmt-totalOut)
2✔
270
        if err != nil {
2✔
UNCOV
271
                return 0, 0, err
×
UNCOV
272
        }
×
273

274
        return changeAmt, 0, nil
2✔
275
}
276

277
// CoinSelectSubtractFees attempts to select coins such that we'll spend up to
278
// amt in total after fees, adhering to the specified fee rate. The selected
279
// coins, the final output and change values are returned.
280
func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
281
        dustLimit btcutil.Amount, coins []wallet.Coin,
282
        strategy wallet.CoinSelectionStrategy,
283
        existingWeight input.TxWeightEstimator,
284
        changeType ChangeAddressType) ([]wallet.Coin, btcutil.Amount,
285
        btcutil.Amount, error) {
2✔
286

2✔
287
        // First perform an initial round of coin selection to estimate
2✔
288
        // the required fee.
2✔
289
        totalSat, selectedUtxos, err := selectInputs(
2✔
290
                amt, coins, strategy, feeRate,
2✔
291
        )
2✔
292
        if err != nil {
2✔
293
                return nil, 0, 0, err
×
294
        }
×
295

296
        // Obtain fee estimates both with and without using a change
297
        // output.
298
        requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
2✔
299
                selectedUtxos, feeRate, existingWeight, changeType,
2✔
300
        )
2✔
301
        if err != nil {
2✔
302
                return nil, 0, 0, err
×
303
        }
×
304

305
        // For a transaction without a change output, we'll let everything go
306
        // to our multi-sig output after subtracting fees.
307
        outputAmt := totalSat - requiredFeeNoChange
2✔
308
        changeAmt := btcutil.Amount(0)
2✔
309

2✔
310
        // If the output is too small after subtracting the fee, the coin
2✔
311
        // selection cannot be performed with an amount this small.
2✔
312
        if outputAmt < dustLimit {
4✔
313
                return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
2✔
314
                        "subtracting fees(%v) below dust limit(%v)", outputAmt,
2✔
315
                        requiredFeeNoChange, dustLimit)
2✔
316
        }
2✔
317

318
        // For a transaction with a change output, everything we don't spend
319
        // will go to change.
320
        newOutput := amt - requiredFeeWithChange
2✔
321
        newChange := totalSat - amt
2✔
322

2✔
323
        // If adding a change output leads to both outputs being above
2✔
324
        // the dust limit, we'll add the change output. Otherwise we'll
2✔
325
        // go with the no change tx we originally found.
2✔
326
        if newChange >= dustLimit && newOutput >= dustLimit {
4✔
327
                outputAmt = newOutput
2✔
328
                changeAmt = newChange
2✔
329
        }
2✔
330

331
        // Sanity check the resulting output values to make sure we
332
        // don't burn a great part to fees.
333
        totalOut := outputAmt + changeAmt
2✔
334
        err = sanityCheckFee(totalOut, totalSat-totalOut)
2✔
335
        if err != nil {
2✔
UNCOV
336
                return nil, 0, 0, err
×
UNCOV
337
        }
×
338

339
        return selectedUtxos, outputAmt, changeAmt, nil
2✔
340
}
341

342
// CoinSelectUpToAmount attempts to select coins such that we'll select up to
343
// maxAmount exclusive of fees and optional reserve if sufficient funds are
344
// available. If insufficient funds are available this method selects all
345
// available coins.
346
func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
347
        reserved, dustLimit btcutil.Amount, coins []wallet.Coin,
348
        strategy wallet.CoinSelectionStrategy,
349
        existingWeight input.TxWeightEstimator,
350
        changeType ChangeAddressType) ([]wallet.Coin, btcutil.Amount,
351
        btcutil.Amount, error) {
2✔
352

2✔
353
        var (
2✔
354
                // selectSubtractFee is tracking if our coin selection was
2✔
355
                // unsuccessful and whether we have to start a new round of
2✔
356
                // selecting coins considering fees.
2✔
357
                selectSubtractFee = false
2✔
358
                outputAmount      = maxAmount
2✔
359
        )
2✔
360

2✔
361
        // Get total balance from coins which we need for reserve considerations
2✔
362
        // and fee sanity checks.
2✔
363
        var totalBalance btcutil.Amount
2✔
364
        for _, coin := range coins {
4✔
365
                totalBalance += btcutil.Amount(coin.Value)
2✔
366
        }
2✔
367

368
        // First we try to select coins to create an output of the specified
369
        // maxAmount with or without a change output that covers the miner fee.
370
        selected, changeAmt, err := CoinSelect(
2✔
371
                feeRate, maxAmount, dustLimit, coins, strategy, existingWeight,
2✔
372
                changeType,
2✔
373
        )
2✔
374

2✔
375
        var errInsufficientFunds *ErrInsufficientFunds
2✔
376
        switch {
2✔
377
        case err == nil:
2✔
378
                // If the coin selection succeeds we check if our total balance
2✔
379
                // covers the selected set of coins including fees plus an
2✔
380
                // optional anchor reserve.
2✔
381

2✔
382
                // First we sum up the value of all selected coins.
2✔
383
                var sumSelected btcutil.Amount
2✔
384
                for _, coin := range selected {
4✔
385
                        sumSelected += btcutil.Amount(coin.Value)
2✔
386
                }
2✔
387

388
                // We then subtract the change amount from the value of all
389
                // selected coins to obtain the actual amount that is selected.
390
                sumSelected -= changeAmt
2✔
391

2✔
392
                // Next we check if our total balance can cover for the selected
2✔
393
                // output plus the optional anchor reserve.
2✔
394
                if totalBalance-sumSelected < reserved {
2✔
UNCOV
395
                        // If our local balance is insufficient to cover for the
×
UNCOV
396
                        // reserve we try to select an output amount that uses
×
UNCOV
397
                        // our total balance minus reserve and fees.
×
UNCOV
398
                        selectSubtractFee = true
×
UNCOV
399
                }
×
400

401
        case errors.As(err, &errInsufficientFunds):
2✔
402
                // If the initial coin selection fails due to insufficient funds
2✔
403
                // we select our total available balance minus fees.
2✔
404
                selectSubtractFee = true
2✔
405

UNCOV
406
        default:
×
UNCOV
407
                return nil, 0, 0, err
×
408
        }
409

410
        // If we determined that our local balance is insufficient we check
411
        // our total balance minus fees and optional reserve.
412
        if selectSubtractFee {
4✔
413
                selected, outputAmount, changeAmt, err = CoinSelectSubtractFees(
2✔
414
                        feeRate, totalBalance-reserved, dustLimit, coins,
2✔
415
                        strategy, existingWeight, changeType,
2✔
416
                )
2✔
417
                if err != nil {
4✔
418
                        return nil, 0, 0, err
2✔
419
                }
2✔
420
        }
421

422
        // Sanity check the resulting output values to make sure we don't burn a
423
        // great part to fees.
424
        totalOut := outputAmount + changeAmt
2✔
425
        sum := func(coins []wallet.Coin) btcutil.Amount {
4✔
426
                var sum btcutil.Amount
2✔
427
                for _, coin := range coins {
4✔
428
                        sum += btcutil.Amount(coin.Value)
2✔
429
                }
2✔
430

431
                return sum
2✔
432
        }
433
        err = sanityCheckFee(totalOut, sum(selected)-totalOut)
2✔
434
        if err != nil {
2✔
435
                return nil, 0, 0, err
×
436
        }
×
437

438
        // In case the selected amount is lower than minimum funding amount we
439
        // must return an error. The minimum funding amount is determined
440
        // upstream and denotes either the minimum viable channel size or an
441
        // amount sufficient to cover for the initial remote balance.
442
        if outputAmount < minAmount {
4✔
443
                return nil, 0, 0, fmt.Errorf("available funds(%v) below the "+
2✔
444
                        "minimum amount(%v)", outputAmount, minAmount)
2✔
445
        }
2✔
446

447
        return selected, outputAmount, changeAmt, nil
2✔
448
}
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