• 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

88.37
/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/v2"
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
        // Immediate returns a boolean to indicate whether the tx made from
69
        // this input set should be published immediately.
70
        //
71
        // TODO(yy): create a new method `Params` to combine the informational
72
        // methods DeadlineHeight, Budget, StartingFeeRate and Immediate.
73
        Immediate() bool
74
}
75

76
// createWalletTxInput converts a wallet utxo into an object that can be added
77
// to the other inputs to sweep.
78
func createWalletTxInput(utxo *lnwallet.Utxo) (input.Input, error) {
3✔
79
        signDesc := &input.SignDescriptor{
3✔
80
                Output: &wire.TxOut{
3✔
81
                        PkScript: utxo.PkScript,
3✔
82
                        Value:    int64(utxo.Value),
3✔
83
                },
3✔
84
                HashType: txscript.SigHashAll,
3✔
85
        }
3✔
86

3✔
87
        var witnessType input.WitnessType
3✔
88
        switch utxo.AddressType {
3✔
89
        case lnwallet.WitnessPubKey:
3✔
90
                witnessType = input.WitnessKeyHash
3✔
91
        case lnwallet.NestedWitnessPubKey:
×
92
                witnessType = input.NestedWitnessKeyHash
×
93
        case lnwallet.TaprootPubkey:
3✔
94
                witnessType = input.TaprootPubKeySpend
3✔
95
                signDesc.HashType = txscript.SigHashDefault
3✔
UNCOV
96
        default:
×
UNCOV
97
                return nil, fmt.Errorf("unknown address type %v",
×
UNCOV
98
                        utxo.AddressType)
×
99
        }
100

101
        // A height hint doesn't need to be set, because we don't monitor these
102
        // inputs for spend.
103
        heightHint := uint32(0)
3✔
104

3✔
105
        return input.NewBaseInput(
3✔
106
                &utxo.OutPoint, witnessType, signDesc, heightHint,
3✔
107
        ), nil
3✔
108
}
109

110
// BudgetInputSet implements the interface `InputSet`. It takes a list of
111
// pending inputs which share the same deadline height and groups them into a
112
// set conditionally based on their economical values.
113
type BudgetInputSet struct {
114
        // inputs is the set of inputs that have been added to the set after
115
        // considering their economical contribution.
116
        inputs []*SweeperInput
117

118
        // deadlineHeight is the height which the inputs in this set must be
119
        // confirmed by.
120
        deadlineHeight int32
121

122
        // extraBudget is a value that should be allocated to sweep the given
123
        // set of inputs. This can be used to add extra funds to the sweep
124
        // transaction, for example to cover fees for additional outputs of
125
        // custom channels.
126
        extraBudget btcutil.Amount
127
}
128

129
// Compile-time constraint to ensure budgetInputSet implements InputSet.
130
var _ InputSet = (*BudgetInputSet)(nil)
131

132
// errEmptyInputs is returned when the input slice is empty.
133
var errEmptyInputs = fmt.Errorf("inputs slice is empty")
134

135
// validateInputs is used when creating new BudgetInputSet to ensure there are
136
// no duplicate inputs and they all share the same deadline heights, if set.
137
func validateInputs(inputs []SweeperInput, deadlineHeight int32) error {
3✔
138
        // Sanity check the input slice to ensure it's non-empty.
3✔
139
        if len(inputs) == 0 {
3✔
UNCOV
140
                return errEmptyInputs
×
UNCOV
141
        }
×
142

143
        // inputDeadline tracks the input's deadline height. It will be updated
144
        // if the input has a different deadline than the specified
145
        // deadlineHeight.
146
        inputDeadline := deadlineHeight
3✔
147

3✔
148
        // dedupInputs is a set used to track unique outpoints of the inputs.
3✔
149
        dedupInputs := fn.NewSet(
3✔
150
                // Iterate all the inputs and map the function.
3✔
151
                fn.Map(inputs, func(inp SweeperInput) wire.OutPoint {
6✔
152
                        // If the input has a deadline height, we'll check if
3✔
153
                        // it's the same as the specified.
3✔
154
                        inp.params.DeadlineHeight.WhenSome(func(h int32) {
6✔
155
                                // Exit early if the deadlines matched.
3✔
156
                                if h == deadlineHeight {
6✔
157
                                        return
3✔
158
                                }
3✔
159

160
                                // Update the deadline height if it's
161
                                // different.
UNCOV
162
                                inputDeadline = h
×
163
                        })
164

165
                        return inp.OutPoint()
3✔
166
                })...,
167
        )
168

169
        // Make sure the inputs share the same deadline height when there is
170
        // one.
171
        if inputDeadline != deadlineHeight {
3✔
UNCOV
172
                return fmt.Errorf("input deadline height not matched: want "+
×
UNCOV
173
                        "%d, got %d", deadlineHeight, inputDeadline)
×
UNCOV
174
        }
×
175

176
        // Provide a defensive check to ensure that we don't have any duplicate
177
        // inputs within the set.
178
        if len(dedupInputs) != len(inputs) {
3✔
UNCOV
179
                return fmt.Errorf("duplicate inputs")
×
UNCOV
180
        }
×
181

182
        return nil
3✔
183
}
184

185
// NewBudgetInputSet creates a new BudgetInputSet.
186
func NewBudgetInputSet(inputs []SweeperInput, deadlineHeight int32,
187
        auxSweeper fn.Option[AuxSweeper]) (*BudgetInputSet, error) {
3✔
188

3✔
189
        // Validate the supplied inputs.
3✔
190
        if err := validateInputs(inputs, deadlineHeight); err != nil {
3✔
UNCOV
191
                return nil, err
×
UNCOV
192
        }
×
193

194
        bi := &BudgetInputSet{
3✔
195
                deadlineHeight: deadlineHeight,
3✔
196
                inputs:         make([]*SweeperInput, 0, len(inputs)),
3✔
197
        }
3✔
198

3✔
199
        for _, input := range inputs {
6✔
200
                bi.addInput(input)
3✔
201
        }
3✔
202

203
        log.Tracef("Created %v", bi.String())
3✔
204

3✔
205
        // Attach an optional budget. This will be a no-op if the auxSweeper
3✔
206
        // is not set.
3✔
207
        if err := bi.attachExtraBudget(auxSweeper); err != nil {
3✔
208
                return nil, err
×
209
        }
×
210

211
        return bi, nil
3✔
212
}
213

214
// attachExtraBudget attaches an extra budget to the input set, if the passed
215
// aux sweeper is set.
216
func (b *BudgetInputSet) attachExtraBudget(s fn.Option[AuxSweeper]) error {
3✔
217
        extraBudget, err := fn.MapOptionZ(
3✔
218
                s, func(aux AuxSweeper) fn.Result[btcutil.Amount] {
3✔
UNCOV
219
                        return aux.ExtraBudgetForInputs(b.Inputs())
×
UNCOV
220
                },
×
221
        ).Unpack()
222
        if err != nil {
3✔
223
                return err
×
224
        }
×
225

226
        b.extraBudget = extraBudget
3✔
227

3✔
228
        return nil
3✔
229
}
230

231
// String returns a human-readable description of the input set.
232
func (b *BudgetInputSet) String() string {
3✔
233
        inputsDesc := ""
3✔
234
        for _, input := range b.inputs {
6✔
235
                inputsDesc += fmt.Sprintf("\n%v", input)
3✔
236
        }
3✔
237

238
        return fmt.Sprintf("BudgetInputSet(budget=%v, deadline=%v, "+
3✔
239
                "inputs=[%v])", b.Budget(), b.DeadlineHeight(), inputsDesc)
3✔
240
}
241

242
// addInput adds an input to the input set.
243
func (b *BudgetInputSet) addInput(input SweeperInput) {
3✔
244
        b.inputs = append(b.inputs, &input)
3✔
245
}
3✔
246

247
// NeedWalletInput returns true if the input set needs more wallet inputs.
248
//
249
// A set may need wallet inputs when it has a required output or its total
250
// value cannot cover its total budget.
251
func (b *BudgetInputSet) NeedWalletInput() bool {
3✔
252
        var (
3✔
253
                // budgetNeeded is the amount that needs to be covered from
3✔
254
                // other inputs. We start at the value of the extra budget,
3✔
255
                // which might be needed for custom channels that add extra
3✔
256
                // outputs.
3✔
257
                budgetNeeded = b.extraBudget
3✔
258

3✔
259
                // budgetBorrowable is the amount that can be borrowed from
3✔
260
                // other inputs.
3✔
261
                budgetBorrowable btcutil.Amount
3✔
262
        )
3✔
263

3✔
264
        for _, inp := range b.inputs {
6✔
265
                // If this input has a required output, we can assume it's a
3✔
266
                // second-level htlc txns input. Although this input must have
3✔
267
                // a value that can cover its budget, it cannot be used to pay
3✔
268
                // fees. Instead, we need to borrow budget from other inputs to
3✔
269
                // make the sweep happen. Once swept, the input value will be
3✔
270
                // credited to the wallet.
3✔
271
                if inp.RequiredTxOut() != nil {
6✔
272
                        budgetNeeded += inp.params.Budget
3✔
273
                        continue
3✔
274
                }
275

276
                // Get the amount left after covering the input's own budget.
277
                // This amount can then be lent to the above input.
278
                budget := inp.params.Budget
3✔
279
                output := btcutil.Amount(inp.SignDesc().Output.Value)
3✔
280
                budgetBorrowable += output - budget
3✔
281

3✔
282
                // If the input's budget is not even covered by itself, we need
3✔
283
                // to borrow outputs from other inputs.
3✔
284
                if budgetBorrowable < 0 {
6✔
285
                        log.Tracef("Input %v specified a budget that exceeds "+
3✔
286
                                "its output value: %v > %v", inp, budget,
3✔
287
                                output)
3✔
288
                }
3✔
289
        }
290

291
        log.Debugf("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
3✔
292
                budgetNeeded, budgetBorrowable)
3✔
293

3✔
294
        // If we don't have enough extra budget to borrow, we need wallet
3✔
295
        // inputs.
3✔
296
        return budgetBorrowable < budgetNeeded
3✔
297
}
298

299
// copyInputs returns a copy of the slice of the inputs in the set.
300
func (b *BudgetInputSet) copyInputs() []*SweeperInput {
3✔
301
        inputs := make([]*SweeperInput, len(b.inputs))
3✔
302
        copy(inputs, b.inputs)
3✔
303
        return inputs
3✔
304
}
3✔
305

306
// AddWalletInputs adds wallet inputs to the set until the specified budget is
307
// met. When sweeping inputs with required outputs, although there's budget
308
// specified, it cannot be directly spent from these required outputs. Instead,
309
// we need to borrow budget from other inputs to make the sweep happen.
310
// There are two sources to borrow from: 1) other inputs, 2) wallet utxos. If
311
// we are calling this method, it means other inputs cannot cover the specified
312
// budget, so we need to borrow from wallet utxos.
313
//
314
// Return an error if there are not enough wallet inputs, and the budget set is
315
// set to its initial state by removing any wallet inputs added.
316
//
317
// NOTE: must be called with the wallet lock held via `WithCoinSelectLock`.
318
func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error {
3✔
319
        // Retrieve wallet utxos. Only consider confirmed utxos to prevent
3✔
320
        // problems around RBF rules for unconfirmed inputs. This currently
3✔
321
        // ignores the configured coin selection strategy.
3✔
322
        utxos, err := wallet.ListUnspentWitnessFromDefaultAccount(
3✔
323
                1, math.MaxInt32,
3✔
324
        )
3✔
325
        if err != nil {
3✔
UNCOV
326
                return fmt.Errorf("list unspent witness: %w", err)
×
UNCOV
327
        }
×
328

329
        // Sort the UTXOs by putting smaller values at the start of the slice
330
        // to avoid locking large UTXO for sweeping.
331
        //
332
        // TODO(yy): add more choices to CoinSelectionStrategy and use the
333
        // configured value here.
334
        sort.Slice(utxos, func(i, j int) bool {
6✔
335
                return utxos[i].Value < utxos[j].Value
3✔
336
        })
3✔
337

338
        // Make a copy of the current inputs. If the wallet doesn't have enough
339
        // utxos to cover the budget, we will revert the current set to its
340
        // original state by removing the added wallet inputs.
341
        originalInputs := b.copyInputs()
3✔
342

3✔
343
        // Add wallet inputs to the set until the specified budget is covered.
3✔
344
        for _, utxo := range utxos {
6✔
345
                input, err := createWalletTxInput(utxo)
3✔
346
                if err != nil {
3✔
UNCOV
347
                        return err
×
UNCOV
348
                }
×
349

350
                pi := SweeperInput{
3✔
351
                        Input: input,
3✔
352
                        params: Params{
3✔
353
                                DeadlineHeight: fn.Some(b.deadlineHeight),
3✔
354
                        },
3✔
355
                }
3✔
356
                b.addInput(pi)
3✔
357

3✔
358
                log.Debugf("Added wallet input to input set: op=%v, amt=%v",
3✔
359
                        pi.OutPoint(), utxo.Value)
3✔
360

3✔
361
                // Return if we've reached the minimum output amount.
3✔
362
                if !b.NeedWalletInput() {
6✔
363
                        return nil
3✔
364
                }
3✔
365
        }
366

367
        // The wallet doesn't have enough utxos to cover the budget. Revert the
368
        // input set to its original state.
369
        b.inputs = originalInputs
3✔
370

3✔
371
        return ErrNotEnoughInputs
3✔
372
}
373

374
// Budget returns the total budget of the set.
375
//
376
// NOTE: part of the InputSet interface.
377
func (b *BudgetInputSet) Budget() btcutil.Amount {
3✔
378
        budget := btcutil.Amount(0)
3✔
379
        for _, input := range b.inputs {
6✔
380
                budget += input.params.Budget
3✔
381
        }
3✔
382

383
        // We'll also tack on the extra budget which will eventually be
384
        // accounted for by the wallet txns when we're broadcasting.
385
        return budget + b.extraBudget
3✔
386
}
387

388
// DeadlineHeight returns the deadline height of the set.
389
//
390
// NOTE: part of the InputSet interface.
391
func (b *BudgetInputSet) DeadlineHeight() int32 {
3✔
392
        return b.deadlineHeight
3✔
393
}
3✔
394

395
// Inputs returns the inputs that should be used to create a tx.
396
//
397
// NOTE: part of the InputSet interface.
398
func (b *BudgetInputSet) Inputs() []input.Input {
3✔
399
        inputs := make([]input.Input, 0, len(b.inputs))
3✔
400
        for _, inp := range b.inputs {
6✔
401
                inputs = append(inputs, inp.Input)
3✔
402
        }
3✔
403

404
        return inputs
3✔
405
}
406

407
// StartingFeeRate returns the max starting fee rate found in the inputs.
408
//
409
// NOTE: part of the InputSet interface.
410
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
3✔
411
        maxFeeRate := chainfee.SatPerKWeight(0)
3✔
412
        startingFeeRate := fn.None[chainfee.SatPerKWeight]()
3✔
413

3✔
414
        for _, inp := range b.inputs {
6✔
415
                feerate := inp.params.StartingFeeRate.UnwrapOr(0)
3✔
416
                if feerate > maxFeeRate {
5✔
417
                        maxFeeRate = feerate
2✔
418
                        startingFeeRate = fn.Some(maxFeeRate)
2✔
419
                }
2✔
420
        }
421

422
        return startingFeeRate
3✔
423
}
424

425
// Immediate returns whether the inputs should be swept immediately.
426
//
427
// NOTE: part of the InputSet interface.
428
func (b *BudgetInputSet) Immediate() bool {
3✔
429
        for _, inp := range b.inputs {
6✔
430
                // As long as one of the inputs is immediate, the whole set is
3✔
431
                // immediate.
3✔
432
                if inp.params.Immediate {
6✔
433
                        return true
3✔
434
                }
3✔
435
        }
436

437
        return false
3✔
438
}
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