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

lightningnetwork / lnd / 13593508312

28 Feb 2025 05:41PM UTC coverage: 58.287% (-10.4%) from 68.65%
13593508312

Pull #9458

github

web-flow
Merge d40067c0c into f1182e433
Pull Request #9458: multi+server.go: add initial permissions for some peers

346 of 548 new or added lines in 10 files covered. (63.14%)

27412 existing lines in 442 files now uncovered.

94709 of 162488 relevant lines covered (58.29%)

1.81 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