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

lightningnetwork / lnd / 13211764208

08 Feb 2025 03:08AM UTC coverage: 49.288% (-9.5%) from 58.815%
13211764208

Pull #9489

github

calvinrzachman
itest: verify switchrpc server enforces send then track

We prevent the rpc server from allowing onion dispatches for
attempt IDs which have already been tracked by rpc clients.

This helps protect the client from leaking a duplicate onion
attempt. NOTE: This is not the only method for solving this
issue! The issue could be addressed via careful client side
programming which accounts for the uncertainty and async
nature of dispatching onions to a remote process via RPC.
This would require some lnd ChannelRouter changes for how
we intend to use these RPCs though.
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

474 of 990 new or added lines in 11 files covered. (47.88%)

27321 existing lines in 435 files now uncovered.

101192 of 205306 relevant lines covered (49.29%)

1.54 hits per line

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

80.24
/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) {
3✔
75

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

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

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

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

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

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

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

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

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

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

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

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

3✔
147
        return requiredFeeNoChange, requiredFeeWithChange, nil
3✔
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 {
3✔
153
        // Sanity check the maxFeeRatio itself.
3✔
154
        if maxFeeRatio <= 0.00 || maxFeeRatio > 1.00 {
3✔
UNCOV
155
                return fmt.Errorf("maxFeeRatio must be between 0.00 and 1.00 "+
×
UNCOV
156
                        "got %.2f", maxFeeRatio)
×
UNCOV
157
        }
×
158

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

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

168
        // All checks passed, we return nil to signal that the fees are valid.
169
        return nil
3✔
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) {
3✔
181

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

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

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

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

×
UNCOV
216
                        continue
×
217
                }
218

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

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

×
UNCOV
242
                return 0, 0, fmt.Errorf("when using existing change address, " +
×
UNCOV
243
                        "the fees for with or without change must be the same")
×
UNCOV
244
        }
×
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
3✔
250

3✔
251
        var changeAmt btcutil.Amount
3✔
252

3✔
253
        switch {
3✔
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.
UNCOV
259
        case overShootAmt < requiredFeeNoChange:
×
UNCOV
260
                return 0, requiredAmt + requiredFeeNoChange, nil
×
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:
3✔
266
                changeAmt = overShootAmt - requiredFeeWithChange
3✔
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 {
6✔
279
                changeAmt = 0
3✔
280
        }
3✔
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
3✔
285

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
423
        default:
×
UNCOV
424
                return nil, 0, 0, err
×
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 {
6✔
430
                selected, outputAmount, changeAmt, err = CoinSelectSubtractFees(
3✔
431
                        feeRate, totalBalance-reserved, dustLimit, coins,
3✔
432
                        strategy, existingWeight, changeType, maxFeeRatio,
3✔
433
                )
3✔
434
                if err != nil {
6✔
435
                        return nil, 0, 0, err
3✔
436
                }
3✔
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
3✔
442
        sum := func(coins []wallet.Coin) btcutil.Amount {
6✔
443
                var sum btcutil.Amount
3✔
444
                for _, coin := range coins {
6✔
445
                        sum += btcutil.Amount(coin.Value)
3✔
446
                }
3✔
447

448
                return sum
3✔
449
        }
450
        err = sanityCheckFee(totalOut, sum(selected)-totalOut, maxFeeRatio)
3✔
451
        if err != nil {
3✔
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 {
6✔
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
3✔
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