• 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

75.49
/sweep/walletsweep.go
1
package sweep
2

3
import (
4
        "errors"
5
        "fmt"
6
        "math"
7
        "time"
8

9
        "github.com/btcsuite/btcd/btcutil"
10
        "github.com/btcsuite/btcd/txscript"
11
        "github.com/btcsuite/btcd/wire"
12
        "github.com/btcsuite/btcwallet/wtxmgr"
13
        "github.com/lightningnetwork/lnd/fn/v2"
14
        "github.com/lightningnetwork/lnd/input"
15
        "github.com/lightningnetwork/lnd/lnwallet"
16
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
17
        "github.com/lightningnetwork/lnd/lnwallet/chanfunding"
18
        "golang.org/x/exp/maps"
19
)
20

21
var (
22
        // ErrNoFeePreference is returned when we attempt to satisfy a sweep
23
        // request from a client whom did not specify a fee preference.
24
        ErrNoFeePreference = errors.New("no fee preference specified")
25

26
        // ErrFeePreferenceConflict is returned when both a fee rate and a conf
27
        // target is set for a fee preference.
28
        ErrFeePreferenceConflict = errors.New("fee preference conflict")
29

30
        // ErrUnknownUTXO is returned when creating a sweeping tx using an UTXO
31
        // that's unknown to the wallet.
32
        ErrUnknownUTXO = errors.New("unknown utxo")
33
)
34

35
// FeePreference defines an interface that allows the caller to specify how the
36
// fee rate should be handled. Depending on the implementation, the fee rate
37
// can either be specified directly, or via a conf target which relies on the
38
// chain backend(`bitcoind`) to give a fee estimation, or a customized fee
39
// function which handles fee calculation based on the specified
40
// urgency(deadline).
41
type FeePreference interface {
42
        // String returns a human-readable string of the fee preference.
43
        String() string
44

45
        // Estimate takes a fee estimator and a max allowed fee rate and
46
        // returns a fee rate for the given fee preference. It ensures that the
47
        // fee rate respects the bounds of the relay fee and the specified max
48
        // fee rates.
49
        Estimate(chainfee.Estimator,
50
                chainfee.SatPerKWeight) (chainfee.SatPerKWeight, error)
51
}
52

53
// FeeEstimateInfo allows callers to express their time value for inclusion of
54
// a transaction into a block via either a confirmation target, or a fee rate.
55
type FeeEstimateInfo struct {
56
        // ConfTarget if non-zero, signals a fee preference expressed in the
57
        // number of desired blocks between first broadcast, and confirmation.
58
        ConfTarget uint32
59

60
        // FeeRate if non-zero, signals a fee pre fence expressed in the fee
61
        // rate expressed in sat/kw for a particular transaction.
62
        FeeRate chainfee.SatPerKWeight
63
}
64

65
// Compile-time constraint to ensure FeeEstimateInfo implements FeePreference.
66
var _ FeePreference = (*FeeEstimateInfo)(nil)
67

68
// String returns a human-readable string of the fee preference.
69
func (f FeeEstimateInfo) String() string {
×
70
        if f.ConfTarget != 0 {
×
71
                return fmt.Sprintf("%v blocks", f.ConfTarget)
×
72
        }
×
73

74
        return f.FeeRate.String()
×
75
}
76

77
// Estimate returns a fee rate for the given fee preference. It ensures that
78
// the fee rate respects the bounds of the relay fee and the max fee rates, if
79
// specified.
80
func (f FeeEstimateInfo) Estimate(estimator chainfee.Estimator,
81
        maxFeeRate chainfee.SatPerKWeight) (chainfee.SatPerKWeight, error) {
3✔
82

3✔
83
        var (
3✔
84
                feeRate chainfee.SatPerKWeight
3✔
85
                err     error
3✔
86
        )
3✔
87

3✔
88
        switch {
3✔
89
        // Ensure a type of fee preference is specified to prevent using a
90
        // default below.
UNCOV
91
        case f.FeeRate == 0 && f.ConfTarget == 0:
×
UNCOV
92
                return 0, ErrNoFeePreference
×
93

94
        // If both values are set, then we'll return an error as we require a
95
        // strict directive.
UNCOV
96
        case f.FeeRate != 0 && f.ConfTarget != 0:
×
UNCOV
97
                return 0, ErrFeePreferenceConflict
×
98

99
        // If the target number of confirmations is set, then we'll use that to
100
        // consult our fee estimator for an adequate fee.
101
        case f.ConfTarget != 0:
3✔
102
                feeRate, err = estimator.EstimateFeePerKW((f.ConfTarget))
3✔
103
                if err != nil {
3✔
UNCOV
104
                        return 0, fmt.Errorf("unable to query fee "+
×
UNCOV
105
                                "estimator: %w", err)
×
UNCOV
106
                }
×
107

108
        // If a manual sat/kw fee rate is set, then we'll use that directly.
109
        // We'll need to convert it to sat/kw as this is what we use
110
        // internally.
111
        case f.FeeRate != 0:
3✔
112
                feeRate = f.FeeRate
3✔
113

3✔
114
                // Because the user can specify 1 sat/vByte on the RPC
3✔
115
                // interface, which corresponds to 250 sat/kw, we need to bump
3✔
116
                // that to the minimum "safe" fee rate which is 253 sat/kw.
3✔
117
                if feeRate == chainfee.AbsoluteFeePerKwFloor {
6✔
118
                        log.Infof("Manual fee rate input of %d sat/kw is "+
3✔
119
                                "too low, using %d sat/kw instead", feeRate,
3✔
120
                                chainfee.FeePerKwFloor)
3✔
121

3✔
122
                        feeRate = chainfee.FeePerKwFloor
3✔
123
                }
3✔
124
        }
125

126
        // Get the relay fee as the min fee rate.
127
        minFeeRate := estimator.RelayFeePerKW()
3✔
128

3✔
129
        // If that bumped fee rate of at least 253 sat/kw is still lower than
3✔
130
        // the relay fee rate, we return an error to let the user know. Note
3✔
131
        // that "Relay fee rate" may mean slightly different things depending
3✔
132
        // on the backend. For bitcoind, it is effectively max(relay fee, min
3✔
133
        // mempool fee).
3✔
134
        if feeRate < minFeeRate {
3✔
UNCOV
135
                return 0, fmt.Errorf("%w: got %v, minimum is %v",
×
UNCOV
136
                        ErrFeePreferenceTooLow, feeRate, minFeeRate)
×
UNCOV
137
        }
×
138

139
        // If a maxFeeRate is specified and the estimated fee rate is above the
140
        // maximum allowed fee rate, default to the max fee rate.
141
        if maxFeeRate != 0 && feeRate > maxFeeRate {
6✔
142
                log.Warnf("Estimated fee rate %v exceeds max allowed fee "+
3✔
143
                        "rate %v, using max fee rate instead", feeRate,
3✔
144
                        maxFeeRate)
3✔
145

3✔
146
                return maxFeeRate, nil
3✔
147
        }
3✔
148

149
        return feeRate, nil
3✔
150
}
151

152
// UtxoSource is an interface that allows a caller to access a source of UTXOs
153
// to use when crafting sweep transactions.
154
type UtxoSource interface {
155
        // ListUnspentWitnessFromDefaultAccount returns all UTXOs from the
156
        // default wallet account that have between minConfs and maxConfs
157
        // number of confirmations.
158
        ListUnspentWitnessFromDefaultAccount(minConfs, maxConfs int32) (
159
                []*lnwallet.Utxo, error)
160
}
161

162
// CoinSelectionLocker is an interface that allows the caller to perform an
163
// operation, which is synchronized with all coin selection attempts. This can
164
// be used when an operation requires that all coin selection operations cease
165
// forward progress. Think of this as an exclusive lock on coin selection
166
// operations.
167
type CoinSelectionLocker interface {
168
        // WithCoinSelectLock will execute the passed function closure in a
169
        // synchronized manner preventing any coin selection operations from
170
        // proceeding while the closure is executing. This can be seen as the
171
        // ability to execute a function closure under an exclusive coin
172
        // selection lock.
173
        WithCoinSelectLock(func() error) error
174
}
175

176
// OutputLeaser allows a caller to lease/release an output. When leased, the
177
// outputs shouldn't be used for any sort of channel funding or coin selection.
178
// Leased outputs are expected to be persisted between restarts.
179
type OutputLeaser interface {
180
        // LeaseOutput leases a target output, rendering it unusable for coin
181
        // selection.
182
        LeaseOutput(i wtxmgr.LockID, o wire.OutPoint, d time.Duration) (
183
                time.Time, error)
184

185
        // ReleaseOutput releases a target output, allowing it to be used for
186
        // coin selection once again.
187
        ReleaseOutput(i wtxmgr.LockID, o wire.OutPoint) error
188
}
189

190
// WalletSweepPackage is a package that gives the caller the ability to sweep
191
// relevant funds from a wallet in a single transaction. We also package a
192
// function closure that allows one to abort the operation.
193
type WalletSweepPackage struct {
194
        // SweepTx is a fully signed, and valid transaction that is broadcast,
195
        // will sweep ALL relevant confirmed coins in the wallet with a single
196
        // transaction.
197
        SweepTx *wire.MsgTx
198

199
        // CancelSweepAttempt allows the caller to cancel the sweep attempt.
200
        //
201
        // NOTE: If the sweeping transaction isn't or cannot be broadcast, then
202
        // this closure MUST be called, otherwise all selected utxos will be
203
        // unable to be used.
204
        CancelSweepAttempt func()
205
}
206

207
// DeliveryAddr is a pair of (address, amount) used to craft a transaction
208
// paying to more than one specified address.
209
type DeliveryAddr struct {
210
        // Addr is the address to pay to.
211
        Addr btcutil.Address
212

213
        // Amt is the amount to pay to the given address.
214
        Amt btcutil.Amount
215
}
216

217
// CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the
218
// caller to sweep ALL funds in ALL or SELECT outputs within the wallet to a
219
// list of outputs. Any leftover amount after these outputs and transaction fee,
220
// is sent to a single output, as specified by the change address. The sweep
221
// transaction will be crafted with the target fee rate, and will use the
222
// utxoSource and outputLeaser as sources for wallet funds.
223
func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
224
        blockHeight uint32, deliveryAddrs []DeliveryAddr,
225
        changeAddr btcutil.Address, coinSelectLocker CoinSelectionLocker,
226
        utxoSource UtxoSource, outputLeaser OutputLeaser,
227
        signer input.Signer, minConfs int32,
228
        selectUtxos fn.Set[wire.OutPoint]) (*WalletSweepPackage, error) {
3✔
229

3✔
230
        // TODO(roasbeef): turn off ATPL as well when available?
3✔
231

3✔
232
        var outputsForSweep []*lnwallet.Utxo
3✔
233

3✔
234
        // We'll make a function closure up front that allows us to unlock all
3✔
235
        // selected outputs to ensure that they become available again in the
3✔
236
        // case of an error after the outputs have been locked, but before we
3✔
237
        // can actually craft a sweeping transaction.
3✔
238
        unlockOutputs := func() {
6✔
239
                for _, utxo := range outputsForSweep {
6✔
240
                        // Log the error but continue since we're already
3✔
241
                        // handling an error.
3✔
242
                        err := outputLeaser.ReleaseOutput(
3✔
243
                                chanfunding.LndInternalLockID, utxo.OutPoint,
3✔
244
                        )
3✔
245
                        if err != nil {
3✔
246
                                log.Warnf("Failed to release UTXO %s (%v))",
×
247
                                        utxo.OutPoint, err)
×
248
                        }
×
249
                }
250
        }
251

252
        // Next, we'll use the coinSelectLocker to ensure that no coin
253
        // selection takes place while we fetch and lock outputs in the
254
        // wallet. Otherwise, it may be possible for a new funding flow to lock
255
        // an output while we fetch the set of unspent witnesses.
256
        err := coinSelectLocker.WithCoinSelectLock(func() error {
6✔
257
                log.Trace("[WithCoinSelectLock] entered the lock")
3✔
258

3✔
259
                // Now that we can be sure that no other coin selection
3✔
260
                // operations are going on, we can grab a clean snapshot of the
3✔
261
                // current UTXO state of the wallet.
3✔
262
                utxos, err := utxoSource.ListUnspentWitnessFromDefaultAccount(
3✔
263
                        minConfs, math.MaxInt32,
3✔
264
                )
3✔
265
                if err != nil {
3✔
266
                        return err
×
267
                }
×
268

269
                log.Trace("[WithCoinSelectLock] finished fetching UTXOs")
3✔
270

3✔
271
                // Use select utxos, if provided.
3✔
272
                if len(selectUtxos) > 0 {
6✔
273
                        utxos, err = fetchUtxosFromOutpoints(
3✔
274
                                utxos, selectUtxos.ToSlice(),
3✔
275
                        )
3✔
276
                        if err != nil {
3✔
UNCOV
277
                                return err
×
UNCOV
278
                        }
×
279
                }
280

281
                // We'll now lock each UTXO to ensure that other callers don't
282
                // attempt to use these UTXOs in transactions while we're
283
                // crafting out sweep all transaction.
284
                for _, utxo := range utxos {
6✔
285
                        log.Tracef("[WithCoinSelectLock] leasing utxo: %v",
3✔
286
                                utxo.OutPoint)
3✔
287

3✔
288
                        _, err = outputLeaser.LeaseOutput(
3✔
289
                                chanfunding.LndInternalLockID, utxo.OutPoint,
3✔
290
                                chanfunding.DefaultLockDuration,
3✔
291
                        )
3✔
292
                        if err != nil {
3✔
293
                                return err
×
294
                        }
×
295
                }
296

297
                log.Trace("[WithCoinSelectLock] exited the lock")
3✔
298

3✔
299
                outputsForSweep = append(outputsForSweep, utxos...)
3✔
300

3✔
301
                return nil
3✔
302
        })
303
        if err != nil {
3✔
UNCOV
304
                // If we failed at all, we'll unlock any outputs selected just
×
UNCOV
305
                // in case we had any lingering outputs.
×
UNCOV
306
                unlockOutputs()
×
UNCOV
307

×
UNCOV
308
                return nil, fmt.Errorf("unable to fetch+lock wallet utxos: %w",
×
UNCOV
309
                        err)
×
UNCOV
310
        }
×
311

312
        // Now that we've locked all the potential outputs to sweep, we'll
313
        // assemble an input for each of them, so we can hand it off to the
314
        // sweeper to generate and sign a transaction for us.
315
        var inputsToSweep []input.Input
3✔
316
        for _, output := range outputsForSweep {
6✔
317
                // As we'll be signing for outputs under control of the wallet,
3✔
318
                // we only need to populate the output value and output script.
3✔
319
                // The rest of the items will be populated internally within
3✔
320
                // the sweeper via the witness generation function.
3✔
321
                signDesc := &input.SignDescriptor{
3✔
322
                        Output: &wire.TxOut{
3✔
323
                                PkScript: output.PkScript,
3✔
324
                                Value:    int64(output.Value),
3✔
325
                        },
3✔
326
                        HashType: txscript.SigHashAll,
3✔
327
                }
3✔
328

3✔
329
                pkScript := output.PkScript
3✔
330

3✔
331
                // Based on the output type, we'll map it to the proper witness
3✔
332
                // type so we can generate the set of input scripts needed to
3✔
333
                // sweep the output.
3✔
334
                var witnessType input.WitnessType
3✔
335
                switch output.AddressType {
3✔
336

337
                // If this is a p2wkh output, then we'll assume it's a witness
338
                // key hash witness type.
339
                case lnwallet.WitnessPubKey:
3✔
340
                        witnessType = input.WitnessKeyHash
3✔
341

342
                // If this is a p2sh output, then as since it's under control
343
                // of the wallet, we'll assume it's a nested p2sh output.
344
                case lnwallet.NestedWitnessPubKey:
3✔
345
                        witnessType = input.NestedWitnessKeyHash
3✔
346

347
                case lnwallet.TaprootPubkey:
3✔
348
                        witnessType = input.TaprootPubKeySpend
3✔
349
                        signDesc.HashType = txscript.SigHashDefault
3✔
350

351
                // All other output types we count as unknown and will fail to
352
                // sweep.
UNCOV
353
                default:
×
UNCOV
354
                        unlockOutputs()
×
UNCOV
355

×
UNCOV
356
                        return nil, fmt.Errorf("unable to sweep coins, "+
×
UNCOV
357
                                "unknown script: %x", pkScript[:])
×
358
                }
359

360
                // Now that we've constructed the items required, we'll make an
361
                // input which can be passed to the sweeper for ultimate
362
                // sweeping.
363
                input := input.MakeBaseInput(
3✔
364
                        &output.OutPoint, witnessType, signDesc, 0, nil,
3✔
365
                )
3✔
366
                inputsToSweep = append(inputsToSweep, &input)
3✔
367
        }
368

369
        // Create a list of TxOuts from the given delivery addresses.
370
        var txOuts []*wire.TxOut
3✔
371
        for _, d := range deliveryAddrs {
6✔
372
                pkScript, err := txscript.PayToAddrScript(d.Addr)
3✔
373
                if err != nil {
3✔
374
                        unlockOutputs()
×
375

×
376
                        return nil, err
×
377
                }
×
378

379
                txOuts = append(txOuts, &wire.TxOut{
3✔
380
                        PkScript: pkScript,
3✔
381
                        Value:    int64(d.Amt),
3✔
382
                })
3✔
383
        }
384

385
        // Next, we'll convert the change addr to a pkScript that we can use
386
        // to create the sweep transaction.
387
        changePkScript, err := txscript.PayToAddrScript(changeAddr)
3✔
388
        if err != nil {
3✔
389
                unlockOutputs()
×
390

×
391
                return nil, err
×
392
        }
×
393

394
        // Finally, we'll ask the sweeper to craft a sweep transaction which
395
        // respects our fee preference and targets all the UTXOs of the wallet.
396
        sweepTx, _, err := createSweepTx(
3✔
397
                inputsToSweep, txOuts, changePkScript, blockHeight,
3✔
398
                feeRate, maxFeeRate, signer,
3✔
399
        )
3✔
400
        if err != nil {
3✔
401
                unlockOutputs()
×
402

×
403
                return nil, err
×
404
        }
×
405

406
        return &WalletSweepPackage{
3✔
407
                SweepTx:            sweepTx,
3✔
408
                CancelSweepAttempt: unlockOutputs,
3✔
409
        }, nil
3✔
410
}
411

412
// fetchUtxosFromOutpoints returns UTXOs for given outpoints. Errors if any
413
// outpoint is not in the passed slice of utxos.
414
func fetchUtxosFromOutpoints(utxos []*lnwallet.Utxo,
415
        outpoints []wire.OutPoint) ([]*lnwallet.Utxo, error) {
3✔
416

3✔
417
        lookup := fn.SliceToMap(utxos, func(utxo *lnwallet.Utxo) wire.OutPoint {
6✔
418
                return utxo.OutPoint
3✔
419
        }, func(utxo *lnwallet.Utxo) *lnwallet.Utxo {
6✔
420
                return utxo
3✔
421
        })
3✔
422

423
        subMap, err := fn.NewSubMap(lookup, outpoints)
3✔
424
        if err != nil {
3✔
UNCOV
425
                return nil, fmt.Errorf("%w: %v", ErrUnknownUTXO, err.Error())
×
UNCOV
426
        }
×
427

428
        fetchedUtxos := maps.Values(subMap)
3✔
429

3✔
430
        return fetchedUtxos, nil
3✔
431
}
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