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

lightningnetwork / lnd / 15838907453

24 Jun 2025 01:26AM UTC coverage: 57.079% (-11.1%) from 68.172%
15838907453

Pull #9982

github

web-flow
Merge e42780be2 into 45c15646c
Pull Request #9982: lnwire+lnwallet: add LocalNonces field for splice nonce coordination w/ taproot channels

103 of 167 new or added lines in 5 files covered. (61.68%)

30191 existing lines in 463 files now uncovered.

96331 of 168768 relevant lines covered (57.08%)

0.6 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
        "maps"
7
        "math"
8
        "slices"
9
        "time"
10

11
        "github.com/btcsuite/btcd/btcutil"
12
        "github.com/btcsuite/btcd/txscript"
13
        "github.com/btcsuite/btcd/wire"
14
        "github.com/btcsuite/btcwallet/wtxmgr"
15
        "github.com/lightningnetwork/lnd/fn/v2"
16
        "github.com/lightningnetwork/lnd/input"
17
        "github.com/lightningnetwork/lnd/lnwallet"
18
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
19
        "github.com/lightningnetwork/lnd/lnwallet/chanfunding"
20
)
21

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

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

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

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

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

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

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

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

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

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

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

1✔
84
        var (
1✔
85
                feeRate chainfee.SatPerKWeight
1✔
86
                err     error
1✔
87
        )
1✔
88

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

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

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

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

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

1✔
123
                        feeRate = chainfee.FeePerKwFloor
1✔
124
                }
1✔
125
        }
126

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

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

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

1✔
147
                return maxFeeRate, nil
1✔
148
        }
1✔
149

150
        return feeRate, nil
1✔
151
}
152

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

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

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

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

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

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

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

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

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

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

1✔
233
        var outputsForSweep []*lnwallet.Utxo
1✔
234

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

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

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

270
                log.Trace("[WithCoinSelectLock] finished fetching UTXOs")
1✔
271

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

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

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

298
                log.Trace("[WithCoinSelectLock] exited the lock")
1✔
299

1✔
300
                outputsForSweep = append(outputsForSweep, utxos...)
1✔
301

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

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

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

1✔
330
                pkScript := output.PkScript
1✔
331

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

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

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

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

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

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

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

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

×
377
                        return nil, err
×
378
                }
×
379

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

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

×
392
                return nil, err
×
393
        }
×
394

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

×
404
                return nil, err
×
405
        }
×
406

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

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

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

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

429
        fetchedUtxos := slices.Collect(maps.Values(subMap))
1✔
430

1✔
431
        return fetchedUtxos, nil
1✔
432
}
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