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

lightningnetwork / lnd / 11292787765

11 Oct 2024 12:58PM UTC coverage: 49.179% (-9.5%) from 58.716%
11292787765

push

github

web-flow
Merge pull request #9168 from feelancer21/fix-lncli-wallet-proto

lnrpc: fix lncli documentation tags in walletkit.proto

97369 of 197987 relevant lines covered (49.18%)

1.04 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"
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) {
2✔
82

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

2✔
88
        switch {
2✔
89
        // Ensure a type of fee preference is specified to prevent using a
90
        // default below.
91
        case f.FeeRate == 0 && f.ConfTarget == 0:
×
92
                return 0, ErrNoFeePreference
×
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:
×
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:
2✔
102
                feeRate, err = estimator.EstimateFeePerKW((f.ConfTarget))
2✔
103
                if err != nil {
2✔
104
                        return 0, fmt.Errorf("unable to query fee "+
×
105
                                "estimator: %w", err)
×
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:
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 {
4✔
118
                        log.Infof("Manual fee rate input of %d sat/kw is "+
2✔
119
                                "too low, using %d sat/kw instead", feeRate,
2✔
120
                                chainfee.FeePerKwFloor)
2✔
121

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

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

2✔
129
        // If that bumped fee rate of at least 253 sat/kw is still lower than
2✔
130
        // the relay fee rate, we return an error to let the user know. Note
2✔
131
        // that "Relay fee rate" may mean slightly different things depending
2✔
132
        // on the backend. For bitcoind, it is effectively max(relay fee, min
2✔
133
        // mempool fee).
2✔
134
        if feeRate < minFeeRate {
2✔
135
                return 0, fmt.Errorf("%w: got %v, minimum is %v",
×
136
                        ErrFeePreferenceTooLow, feeRate, minFeeRate)
×
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 {
4✔
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
2✔
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
        // ListUnspentWitness returns all UTXOs from the default wallet account
156
        // that have between minConfs and maxConfs number of confirmations.
157
        ListUnspentWitnessFromDefaultAccount(minConfs, maxConfs int32) (
158
                []*lnwallet.Utxo, error)
159
}
160

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

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

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

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

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

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

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

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

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

2✔
231
        var outputsForSweep []*lnwallet.Utxo
2✔
232

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

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

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

268
                log.Trace("[WithCoinSelectLock] finished fetching UTXOs")
2✔
269

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

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

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

296
                log.Trace("[WithCoinSelectLock] exited the lock")
2✔
297

2✔
298
                outputsForSweep = append(outputsForSweep, utxos...)
2✔
299

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

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

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

2✔
328
                pkScript := output.PkScript
2✔
329

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

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

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

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

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

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

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

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

×
375
                        return nil, err
×
376
                }
×
377

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

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

×
390
                return nil, err
×
391
        }
×
392

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

×
402
                return nil, err
×
403
        }
×
404

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

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

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

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

427
        fetchedUtxos := maps.Values(subMap)
2✔
428

2✔
429
        return fetchedUtxos, nil
2✔
430
}
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