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

lightningnetwork / lnd / 12313002221

13 Dec 2024 09:25AM UTC coverage: 57.486% (+8.6%) from 48.92%
12313002221

push

github

web-flow
Merge pull request #9343 from ellemouton/contextGuard

fn: expand the ContextGuard and add tests

101902 of 177264 relevant lines covered (57.49%)

24909.26 hits per line

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

80.88
/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) {
17✔
82

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

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

94
        // If both values are set, then we'll return an error as we require a
95
        // strict directive.
96
        case f.FeeRate != 0 && f.ConfTarget != 0:
1✔
97
                return 0, ErrFeePreferenceConflict
1✔
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:
13✔
102
                feeRate, err = estimator.EstimateFeePerKW((f.ConfTarget))
13✔
103
                if err != nil {
16✔
104
                        return 0, fmt.Errorf("unable to query fee "+
3✔
105
                                "estimator: %w", err)
3✔
106
                }
3✔
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:
2✔
112
                feeRate = f.FeeRate
2✔
113

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

×
122
                        feeRate = chainfee.FeePerKwFloor
×
123
                }
×
124
        }
125

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

12✔
129
        // If that bumped fee rate of at least 253 sat/kw is still lower than
12✔
130
        // the relay fee rate, we return an error to let the user know. Note
12✔
131
        // that "Relay fee rate" may mean slightly different things depending
12✔
132
        // on the backend. For bitcoind, it is effectively max(relay fee, min
12✔
133
        // mempool fee).
12✔
134
        if feeRate < minFeeRate {
13✔
135
                return 0, fmt.Errorf("%w: got %v, minimum is %v",
1✔
136
                        ErrFeePreferenceTooLow, feeRate, minFeeRate)
1✔
137
        }
1✔
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 {
13✔
142
                log.Warnf("Estimated fee rate %v exceeds max allowed fee "+
2✔
143
                        "rate %v, using max fee rate instead", feeRate,
2✔
144
                        maxFeeRate)
2✔
145

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

149
        return feeRate, nil
9✔
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) {
5✔
229

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

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

5✔
234
        // We'll make a function closure up front that allows us to unlock all
5✔
235
        // selected outputs to ensure that they become available again in the
5✔
236
        // case of an error after the outputs have been locked, but before we
5✔
237
        // can actually craft a sweeping transaction.
5✔
238
        unlockOutputs := func() {
10✔
239
                for _, utxo := range outputsForSweep {
14✔
240
                        // Log the error but continue since we're already
9✔
241
                        // handling an error.
9✔
242
                        err := outputLeaser.ReleaseOutput(
9✔
243
                                chanfunding.LndInternalLockID, utxo.OutPoint,
9✔
244
                        )
9✔
245
                        if err != nil {
9✔
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 {
10✔
257
                log.Trace("[WithCoinSelectLock] entered the lock")
5✔
258

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

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

5✔
271
                // Use select utxos, if provided.
5✔
272
                if len(selectUtxos) > 0 {
7✔
273
                        utxos, err = fetchUtxosFromOutpoints(
2✔
274
                                utxos, selectUtxos.ToSlice(),
2✔
275
                        )
2✔
276
                        if err != nil {
3✔
277
                                return err
1✔
278
                        }
1✔
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 {
13✔
285
                        log.Tracef("[WithCoinSelectLock] leasing utxo: %v",
9✔
286
                                utxo.OutPoint)
9✔
287

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

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

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

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

2✔
308
                return nil, fmt.Errorf("unable to fetch+lock wallet utxos: %w",
2✔
309
                        err)
2✔
310
        }
2✔
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 {
9✔
317
                // As we'll be signing for outputs under control of the wallet,
6✔
318
                // we only need to populate the output value and output script.
6✔
319
                // The rest of the items will be populated internally within
6✔
320
                // the sweeper via the witness generation function.
6✔
321
                signDesc := &input.SignDescriptor{
6✔
322
                        Output: &wire.TxOut{
6✔
323
                                PkScript: output.PkScript,
6✔
324
                                Value:    int64(output.Value),
6✔
325
                        },
6✔
326
                        HashType: txscript.SigHashAll,
6✔
327
                }
6✔
328

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

6✔
331
                // Based on the output type, we'll map it to the proper witness
6✔
332
                // type so we can generate the set of input scripts needed to
6✔
333
                // sweep the output.
6✔
334
                var witnessType input.WitnessType
6✔
335
                switch output.AddressType {
6✔
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:
2✔
345
                        witnessType = input.NestedWitnessKeyHash
2✔
346

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

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

1✔
356
                        return nil, fmt.Errorf("unable to sweep coins, "+
1✔
357
                                "unknown script: %x", pkScript[:])
1✔
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(
5✔
364
                        &output.OutPoint, witnessType, signDesc, 0, nil,
5✔
365
                )
5✔
366
                inputsToSweep = append(inputsToSweep, &input)
5✔
367
        }
368

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

×
376
                        return nil, err
×
377
                }
×
378

379
                txOuts = append(txOuts, &wire.TxOut{
×
380
                        PkScript: pkScript,
×
381
                        Value:    int64(d.Amt),
×
382
                })
×
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)
2✔
388
        if err != nil {
2✔
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(
2✔
397
                inputsToSweep, txOuts, changePkScript, blockHeight,
2✔
398
                feeRate, maxFeeRate, signer,
2✔
399
        )
2✔
400
        if err != nil {
2✔
401
                unlockOutputs()
×
402

×
403
                return nil, err
×
404
        }
×
405

406
        return &WalletSweepPackage{
2✔
407
                SweepTx:            sweepTx,
2✔
408
                CancelSweepAttempt: unlockOutputs,
2✔
409
        }, nil
2✔
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) {
2✔
416

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

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

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

1✔
430
        return fetchedUtxos, nil
1✔
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