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

lightningnetwork / lnd / 13157733617

05 Feb 2025 12:49PM UTC coverage: 57.712% (-1.1%) from 58.82%
13157733617

Pull #9447

github

yyforyongyu
sweep: rename methods for clarity

We now rename "third party" to "unknown" as the inputs can be spent via
an older sweeping tx, a third party (anchor), or a remote party (pin).
In fee bumper we don't have the info to distinguish the above cases, and
leave them to be further handled by the sweeper as it has more context.
Pull Request #9447: sweep: start tracking input spending status in the fee bumper

83 of 87 new or added lines in 2 files covered. (95.4%)

19472 existing lines in 252 files now uncovered.

103634 of 179570 relevant lines covered (57.71%)

24840.31 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) {
18✔
82

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

18✔
88
        switch {
18✔
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:
14✔
102
                feeRate, err = estimator.EstimateFeePerKW((f.ConfTarget))
14✔
103
                if err != nil {
18✔
104
                        return 0, fmt.Errorf("unable to query fee "+
4✔
105
                                "estimator: %w", err)
4✔
106
                }
4✔
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✔
UNCOV
118
                        log.Infof("Manual fee rate input of %d sat/kw is "+
×
UNCOV
119
                                "too low, using %d sat/kw instead", feeRate,
×
UNCOV
120
                                chainfee.FeePerKwFloor)
×
UNCOV
121

×
UNCOV
122
                        feeRate = chainfee.FeePerKwFloor
×
UNCOV
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

UNCOV
347
                case lnwallet.TaprootPubkey:
×
UNCOV
348
                        witnessType = input.TaprootPubKeySpend
×
UNCOV
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✔
UNCOV
372
                pkScript, err := txscript.PayToAddrScript(d.Addr)
×
UNCOV
373
                if err != nil {
×
374
                        unlockOutputs()
×
375

×
376
                        return nil, err
×
377
                }
×
378

UNCOV
379
                txOuts = append(txOuts, &wire.TxOut{
×
UNCOV
380
                        PkScript: pkScript,
×
UNCOV
381
                        Value:    int64(d.Amt),
×
UNCOV
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