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

lightningnetwork / lnd / 12788338589

15 Jan 2025 12:28PM UTC coverage: 57.566% (-1.2%) from 58.718%
12788338589

Pull #9390

github

NishantBansal2003
docs: add release notes.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Pull Request #9390: Enhance `lncli` listchannels command with the chan_id and short_chan_id (human readable format)

68 of 74 new or added lines in 2 files covered. (91.89%)

19514 existing lines in 254 files now uncovered.

102806 of 178587 relevant lines covered (57.57%)

24754.42 hits per line

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

73.97
/sweep/fee_bumper.go
1
package sweep
2

3
import (
4
        "errors"
5
        "fmt"
6
        "sync"
7
        "sync/atomic"
8

9
        "github.com/btcsuite/btcd/btcutil"
10
        "github.com/btcsuite/btcd/chaincfg/chainhash"
11
        "github.com/btcsuite/btcd/rpcclient"
12
        "github.com/btcsuite/btcd/txscript"
13
        "github.com/btcsuite/btcd/wire"
14
        "github.com/btcsuite/btcwallet/chain"
15
        "github.com/lightningnetwork/lnd/chainio"
16
        "github.com/lightningnetwork/lnd/chainntnfs"
17
        "github.com/lightningnetwork/lnd/fn/v2"
18
        "github.com/lightningnetwork/lnd/input"
19
        "github.com/lightningnetwork/lnd/labels"
20
        "github.com/lightningnetwork/lnd/lntypes"
21
        "github.com/lightningnetwork/lnd/lnutils"
22
        "github.com/lightningnetwork/lnd/lnwallet"
23
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
24
        "github.com/lightningnetwork/lnd/tlv"
25
)
26

27
var (
28
        // ErrInvalidBumpResult is returned when the bump result is invalid.
29
        ErrInvalidBumpResult = errors.New("invalid bump result")
30

31
        // ErrNotEnoughBudget is returned when the fee bumper decides the
32
        // current budget cannot cover the fee.
33
        ErrNotEnoughBudget = errors.New("not enough budget")
34

35
        // ErrLocktimeImmature is returned when sweeping an input whose
36
        // locktime is not reached.
37
        ErrLocktimeImmature = errors.New("immature input")
38

39
        // ErrTxNoOutput is returned when an output cannot be created during tx
40
        // preparation, usually due to the output being dust.
41
        ErrTxNoOutput = errors.New("tx has no output")
42

43
        // ErrThirdPartySpent is returned when a third party has spent the
44
        // input in the sweeping tx.
45
        ErrThirdPartySpent = errors.New("third party spent the output")
46
)
47

48
var (
49
        // dummyChangePkScript is a dummy tapscript change script that's used
50
        // when we don't need a real address, just something that can be used
51
        // for fee estimation.
52
        dummyChangePkScript = []byte{
53
                0x51, 0x20,
54
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
55
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
56
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
57
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
58
        }
59
)
60

61
// Bumper defines an interface that can be used by other subsystems for fee
62
// bumping.
63
type Bumper interface {
64
        // Broadcast is used to publish the tx created from the given inputs
65
        // specified in the request. It handles the tx creation, broadcasts it,
66
        // and monitors its confirmation status for potential fee bumping. It
67
        // returns a chan that the caller can use to receive updates about the
68
        // broadcast result and potential RBF attempts.
69
        Broadcast(req *BumpRequest) <-chan *BumpResult
70
}
71

72
// BumpEvent represents the event of a fee bumping attempt.
73
type BumpEvent uint8
74

75
const (
76
        // TxPublished is sent when the broadcast attempt is finished.
77
        TxPublished BumpEvent = iota
78

79
        // TxFailed is sent when the tx has encountered a fee-related error
80
        // during its creation or broadcast, or an internal error from the fee
81
        // bumper. In either case the inputs in this tx should be retried with
82
        // either a different grouping strategy or an increased budget.
83
        //
84
        // NOTE: We also send this event when there's a third party spend
85
        // event, and the sweeper will handle cleaning this up once it's
86
        // confirmed.
87
        //
88
        // TODO(yy): Remove the above usage once we remove sweeping non-CPFP
89
        // anchors.
90
        TxFailed
91

92
        // TxReplaced is sent when the original tx is replaced by a new one.
93
        TxReplaced
94

95
        // TxConfirmed is sent when the tx is confirmed.
96
        TxConfirmed
97

98
        // TxFatal is sent when the inputs in this tx cannot be retried. Txns
99
        // will end up in this state if they have encountered a non-fee related
100
        // error, which means they cannot be retried with increased budget.
101
        TxFatal
102

103
        // sentinalEvent is used to check if an event is unknown.
104
        sentinalEvent
105
)
106

107
// String returns a human-readable string for the event.
UNCOV
108
func (e BumpEvent) String() string {
×
UNCOV
109
        switch e {
×
UNCOV
110
        case TxPublished:
×
UNCOV
111
                return "Published"
×
UNCOV
112
        case TxFailed:
×
UNCOV
113
                return "Failed"
×
UNCOV
114
        case TxReplaced:
×
UNCOV
115
                return "Replaced"
×
UNCOV
116
        case TxConfirmed:
×
UNCOV
117
                return "Confirmed"
×
UNCOV
118
        case TxFatal:
×
UNCOV
119
                return "Fatal"
×
120
        default:
×
121
                return "Unknown"
×
122
        }
123
}
124

125
// Unknown returns true if the event is unknown.
126
func (e BumpEvent) Unknown() bool {
11✔
127
        return e >= sentinalEvent
11✔
128
}
11✔
129

130
// BumpRequest is used by the caller to give the Bumper the necessary info to
131
// create and manage potential fee bumps for a set of inputs.
132
type BumpRequest struct {
133
        // Budget givens the total amount that can be used as fees by these
134
        // inputs.
135
        Budget btcutil.Amount
136

137
        // Inputs is the set of inputs to sweep.
138
        Inputs []input.Input
139

140
        // DeadlineHeight is the block height at which the tx should be
141
        // confirmed.
142
        DeadlineHeight int32
143

144
        // DeliveryAddress is the script to send the change output to.
145
        DeliveryAddress lnwallet.AddrWithKey
146

147
        // MaxFeeRate is the maximum fee rate that can be used for fee bumping.
148
        MaxFeeRate chainfee.SatPerKWeight
149

150
        // StartingFeeRate is an optional parameter that can be used to specify
151
        // the initial fee rate to use for the fee function.
152
        StartingFeeRate fn.Option[chainfee.SatPerKWeight]
153

154
        // ExtraTxOut tracks if this bump request has an optional set of extra
155
        // outputs to add to the transaction.
156
        ExtraTxOut fn.Option[SweepOutput]
157

158
        // Immediate is used to specify that the tx should be broadcast
159
        // immediately.
160
        Immediate bool
161
}
162

163
// MaxFeeRateAllowed returns the maximum fee rate allowed for the given
164
// request. It calculates the feerate using the supplied budget and the weight,
165
// compares it with the specified MaxFeeRate, and returns the smaller of the
166
// two.
167
func (r *BumpRequest) MaxFeeRateAllowed() (chainfee.SatPerKWeight, error) {
9✔
168
        // We'll want to know if we have any blobs, as we need to factor this
9✔
169
        // into the max fee rate for this bump request.
9✔
170
        hasBlobs := fn.Any(r.Inputs, func(i input.Input) bool {
17✔
171
                return fn.MapOptionZ(
8✔
172
                        i.ResolutionBlob(), func(b tlv.Blob) bool {
8✔
UNCOV
173
                                return len(b) > 0
×
UNCOV
174
                        },
×
175
                )
176
        })
177

178
        sweepAddrs := [][]byte{
9✔
179
                r.DeliveryAddress.DeliveryAddress,
9✔
180
        }
9✔
181

9✔
182
        // If we have blobs, then we'll add an extra sweep addr for the size
9✔
183
        // estimate below. We know that these blobs will also always be based on
9✔
184
        // p2tr addrs.
9✔
185
        if hasBlobs {
9✔
186
                // We need to pass in a real address, so we'll use a dummy
×
187
                // tapscript change script that's used elsewhere for tests.
×
188
                sweepAddrs = append(sweepAddrs, dummyChangePkScript)
×
189
        }
×
190

191
        // Get the size of the sweep tx, which will be used to calculate the
192
        // budget fee rate.
193
        size, err := calcSweepTxWeight(
9✔
194
                r.Inputs, sweepAddrs,
9✔
195
        )
9✔
196
        if err != nil {
10✔
197
                return 0, err
1✔
198
        }
1✔
199

200
        // Use the budget and MaxFeeRate to decide the max allowed fee rate.
201
        // This is needed as, when the input has a large value and the user
202
        // sets the budget to be proportional to the input value, the fee rate
203
        // can be very high and we need to make sure it doesn't exceed the max
204
        // fee rate.
205
        maxFeeRateAllowed := chainfee.NewSatPerKWeight(r.Budget, size)
8✔
206
        if maxFeeRateAllowed > r.MaxFeeRate {
9✔
207
                log.Debugf("Budget feerate %v exceeds MaxFeeRate %v, use "+
1✔
208
                        "MaxFeeRate instead, txWeight=%v", maxFeeRateAllowed,
1✔
209
                        r.MaxFeeRate, size)
1✔
210

1✔
211
                return r.MaxFeeRate, nil
1✔
212
        }
1✔
213

214
        log.Debugf("Budget feerate %v below MaxFeeRate %v, use budget feerate "+
7✔
215
                "instead, txWeight=%v", maxFeeRateAllowed, r.MaxFeeRate, size)
7✔
216

7✔
217
        return maxFeeRateAllowed, nil
7✔
218
}
219

220
// calcSweepTxWeight calculates the weight of the sweep tx. It assumes a
221
// sweeping tx always has a single output(change).
222
func calcSweepTxWeight(inputs []input.Input,
223
        outputPkScript [][]byte) (lntypes.WeightUnit, error) {
12✔
224

12✔
225
        // Use a const fee rate as we only use the weight estimator to
12✔
226
        // calculate the size.
12✔
227
        const feeRate = 1
12✔
228

12✔
229
        // Initialize the tx weight estimator with,
12✔
230
        // - nil outputs as we only have one single change output.
12✔
231
        // - const fee rate as we don't care about the fees here.
12✔
232
        // - 0 maxfeerate as we don't care about fees here.
12✔
233
        //
12✔
234
        // TODO(yy): we should refactor the weight estimator to not require a
12✔
235
        // fee rate and max fee rate and make it a pure tx weight calculator.
12✔
236
        _, estimator, err := getWeightEstimate(
12✔
237
                inputs, nil, feeRate, 0, outputPkScript,
12✔
238
        )
12✔
239
        if err != nil {
14✔
240
                return 0, err
2✔
241
        }
2✔
242

243
        return estimator.weight(), nil
10✔
244
}
245

246
// BumpResult is used by the Bumper to send updates about the tx being
247
// broadcast.
248
type BumpResult struct {
249
        // Event is the type of event that the result is for.
250
        Event BumpEvent
251

252
        // Tx is the tx being broadcast.
253
        Tx *wire.MsgTx
254

255
        // ReplacedTx is the old, replaced tx if a fee bump is attempted.
256
        ReplacedTx *wire.MsgTx
257

258
        // FeeRate is the fee rate used for the new tx.
259
        FeeRate chainfee.SatPerKWeight
260

261
        // Fee is the fee paid by the new tx.
262
        Fee btcutil.Amount
263

264
        // Err is the error that occurred during the broadcast.
265
        Err error
266

267
        // requestID is the ID of the request that created this record.
268
        requestID uint64
269
}
270

271
// String returns a human-readable string for the result.
UNCOV
272
func (b *BumpResult) String() string {
×
UNCOV
273
        desc := fmt.Sprintf("Event=%v", b.Event)
×
UNCOV
274
        if b.Tx != nil {
×
UNCOV
275
                desc += fmt.Sprintf(", Tx=%v", b.Tx.TxHash())
×
UNCOV
276
        }
×
277

UNCOV
278
        return fmt.Sprintf("[%s]", desc)
×
279
}
280

281
// Validate validates the BumpResult so it's safe to use.
282
func (b *BumpResult) Validate() error {
12✔
283
        isFailureEvent := b.Event == TxFailed || b.Event == TxFatal
12✔
284

12✔
285
        // Every result must have a tx except the fatal or failed case.
12✔
286
        if b.Tx == nil && !isFailureEvent {
13✔
287
                return fmt.Errorf("%w: nil tx", ErrInvalidBumpResult)
1✔
288
        }
1✔
289

290
        // Every result must have a known event.
291
        if b.Event.Unknown() {
12✔
292
                return fmt.Errorf("%w: unknown event", ErrInvalidBumpResult)
1✔
293
        }
1✔
294

295
        // If it's a replacing event, it must have a replaced tx.
296
        if b.Event == TxReplaced && b.ReplacedTx == nil {
11✔
297
                return fmt.Errorf("%w: nil replacing tx", ErrInvalidBumpResult)
1✔
298
        }
1✔
299

300
        // If it's a failed or fatal event, it must have an error.
301
        if isFailureEvent && b.Err == nil {
11✔
302
                return fmt.Errorf("%w: nil error", ErrInvalidBumpResult)
2✔
303
        }
2✔
304

305
        // If it's a confirmed event, it must have a fee rate and fee.
306
        if b.Event == TxConfirmed && (b.FeeRate == 0 || b.Fee == 0) {
8✔
307
                return fmt.Errorf("%w: missing fee rate or fee",
1✔
308
                        ErrInvalidBumpResult)
1✔
309
        }
1✔
310

311
        return nil
6✔
312
}
313

314
// TxPublisherConfig is the config used to create a new TxPublisher.
315
type TxPublisherConfig struct {
316
        // Signer is used to create the tx signature.
317
        Signer input.Signer
318

319
        // Wallet is used primarily to publish the tx.
320
        Wallet Wallet
321

322
        // Estimator is used to estimate the fee rate for the new tx based on
323
        // its deadline conf target.
324
        Estimator chainfee.Estimator
325

326
        // Notifier is used to monitor the confirmation status of the tx.
327
        Notifier chainntnfs.ChainNotifier
328

329
        // AuxSweeper is an optional interface that can be used to modify the
330
        // way sweep transaction are generated.
331
        AuxSweeper fn.Option[AuxSweeper]
332
}
333

334
// TxPublisher is an implementation of the Bumper interface. It utilizes the
335
// `testmempoolaccept` RPC to bump the fee of txns it created based on
336
// different fee function selected or configed by the caller. Its purpose is to
337
// take a list of inputs specified, and create a tx that spends them to a
338
// specified output. It will then monitor the confirmation status of the tx,
339
// and if it's not confirmed within a certain time frame, it will attempt to
340
// bump the fee of the tx by creating a new tx that spends the same inputs to
341
// the same output, but with a higher fee rate. It will continue to do this
342
// until the tx is confirmed or the fee rate reaches the maximum fee rate
343
// specified by the caller.
344
type TxPublisher struct {
345
        started atomic.Bool
346
        stopped atomic.Bool
347

348
        // Embed the blockbeat consumer struct to get access to the method
349
        // `NotifyBlockProcessed` and the `BlockbeatChan`.
350
        chainio.BeatConsumer
351

352
        wg sync.WaitGroup
353

354
        // cfg specifies the configuration of the TxPublisher.
355
        cfg *TxPublisherConfig
356

357
        // currentHeight is the current block height.
358
        currentHeight atomic.Int32
359

360
        // records is a map keyed by the requestCounter and the value is the tx
361
        // being monitored.
362
        records lnutils.SyncMap[uint64, *monitorRecord]
363

364
        // requestCounter is a monotonically increasing counter used to keep
365
        // track of how many requests have been made.
366
        requestCounter atomic.Uint64
367

368
        // subscriberChans is a map keyed by the requestCounter, each item is
369
        // the chan that the publisher sends the fee bump result to.
370
        subscriberChans lnutils.SyncMap[uint64, chan *BumpResult]
371

372
        // quit is used to signal the publisher to stop.
373
        quit chan struct{}
374
}
375

376
// Compile-time constraint to ensure TxPublisher implements Bumper.
377
var _ Bumper = (*TxPublisher)(nil)
378

379
// Compile-time check for the chainio.Consumer interface.
380
var _ chainio.Consumer = (*TxPublisher)(nil)
381

382
// NewTxPublisher creates a new TxPublisher.
383
func NewTxPublisher(cfg TxPublisherConfig) *TxPublisher {
16✔
384
        tp := &TxPublisher{
16✔
385
                cfg:             &cfg,
16✔
386
                records:         lnutils.SyncMap[uint64, *monitorRecord]{},
16✔
387
                subscriberChans: lnutils.SyncMap[uint64, chan *BumpResult]{},
16✔
388
                quit:            make(chan struct{}),
16✔
389
        }
16✔
390

16✔
391
        // Mount the block consumer.
16✔
392
        tp.BeatConsumer = chainio.NewBeatConsumer(tp.quit, tp.Name())
16✔
393

16✔
394
        return tp
16✔
395
}
16✔
396

397
// isNeutrinoBackend checks if the wallet backend is neutrino.
398
func (t *TxPublisher) isNeutrinoBackend() bool {
1✔
399
        return t.cfg.Wallet.BackEnd() == "neutrino"
1✔
400
}
1✔
401

402
// Broadcast is used to publish the tx created from the given inputs. It will
403
// register the broadcast request and return a chan to the caller to subscribe
404
// the broadcast result. The initial broadcast is guaranteed to be
405
// RBF-compliant unless the budget specified cannot cover the fee.
406
//
407
// NOTE: part of the Bumper interface.
408
func (t *TxPublisher) Broadcast(req *BumpRequest) <-chan *BumpResult {
5✔
409
        log.Tracef("Received broadcast request: %s",
5✔
410
                lnutils.SpewLogClosure(req))
5✔
411

5✔
412
        // Store the request.
5✔
413
        requestID, record := t.storeInitialRecord(req)
5✔
414

5✔
415
        // Create a chan to send the result to the caller.
5✔
416
        subscriber := make(chan *BumpResult, 1)
5✔
417
        t.subscriberChans.Store(requestID, subscriber)
5✔
418

5✔
419
        // Publish the tx immediately if specified.
5✔
420
        if req.Immediate {
6✔
421
                t.handleInitialBroadcast(record, requestID)
1✔
422
        }
1✔
423

424
        return subscriber
5✔
425
}
426

427
// storeInitialRecord initializes a monitor record and saves it in the map.
428
func (t *TxPublisher) storeInitialRecord(req *BumpRequest) (
429
        uint64, *monitorRecord) {
5✔
430

5✔
431
        // Increase the request counter.
5✔
432
        //
5✔
433
        // NOTE: this is the only place where we increase the counter.
5✔
434
        requestID := t.requestCounter.Add(1)
5✔
435

5✔
436
        // Register the record.
5✔
437
        record := &monitorRecord{req: req}
5✔
438
        t.records.Store(requestID, record)
5✔
439

5✔
440
        return requestID, record
5✔
441
}
5✔
442

443
// NOTE: part of the `chainio.Consumer` interface.
444
func (t *TxPublisher) Name() string {
16✔
445
        return "TxPublisher"
16✔
446
}
16✔
447

448
// initializeTx initializes a fee function and creates an RBF-compliant tx. If
449
// succeeded, the initial tx is stored in the records map.
450
func (t *TxPublisher) initializeTx(requestID uint64, req *BumpRequest) error {
4✔
451
        // Create a fee bumping algorithm to be used for future RBF.
4✔
452
        feeAlgo, err := t.initializeFeeFunction(req)
4✔
453
        if err != nil {
5✔
454
                return fmt.Errorf("init fee function: %w", err)
1✔
455
        }
1✔
456

457
        // Create the initial tx to be broadcasted. This tx is guaranteed to
458
        // comply with the RBF restrictions.
459
        err = t.createRBFCompliantTx(requestID, req, feeAlgo)
3✔
460
        if err != nil {
4✔
461
                return fmt.Errorf("create RBF-compliant tx: %w", err)
1✔
462
        }
1✔
463

464
        return nil
2✔
465
}
466

467
// initializeFeeFunction initializes a fee function to be used for this request
468
// for future fee bumping.
469
func (t *TxPublisher) initializeFeeFunction(
470
        req *BumpRequest) (FeeFunction, error) {
6✔
471

6✔
472
        // Get the max allowed feerate.
6✔
473
        maxFeeRateAllowed, err := req.MaxFeeRateAllowed()
6✔
474
        if err != nil {
6✔
475
                return nil, err
×
476
        }
×
477

478
        // Get the initial conf target.
479
        confTarget := calcCurrentConfTarget(
6✔
480
                t.currentHeight.Load(), req.DeadlineHeight,
6✔
481
        )
6✔
482

6✔
483
        log.Debugf("Initializing fee function with conf target=%v, budget=%v, "+
6✔
484
                "maxFeeRateAllowed=%v", confTarget, req.Budget,
6✔
485
                maxFeeRateAllowed)
6✔
486

6✔
487
        // Initialize the fee function and return it.
6✔
488
        //
6✔
489
        // TODO(yy): return based on differet req.Strategy?
6✔
490
        return NewLinearFeeFunction(
6✔
491
                maxFeeRateAllowed, confTarget, t.cfg.Estimator,
6✔
492
                req.StartingFeeRate,
6✔
493
        )
6✔
494
}
495

496
// createRBFCompliantTx creates a tx that is compliant with RBF rules. It does
497
// so by creating a tx, validate it using `TestMempoolAccept`, and bump its fee
498
// and redo the process until the tx is valid, or return an error when non-RBF
499
// related errors occur or the budget has been used up.
500
func (t *TxPublisher) createRBFCompliantTx(requestID uint64, req *BumpRequest,
501
        f FeeFunction) error {
9✔
502

9✔
503
        for {
21✔
504
                // Create a new tx with the given fee rate and check its
12✔
505
                // mempool acceptance.
12✔
506
                sweepCtx, err := t.createAndCheckTx(req, f)
12✔
507

12✔
508
                switch {
12✔
509
                case err == nil:
6✔
510
                        // The tx is valid, store it.
6✔
511
                        t.storeRecord(
6✔
512
                                requestID, sweepCtx.tx, req, f, sweepCtx.fee,
6✔
513
                                sweepCtx.outpointToTxIndex,
6✔
514
                        )
6✔
515

6✔
516
                        log.Infof("Created initial sweep tx=%v for %v inputs: "+
6✔
517
                                "feerate=%v, fee=%v, inputs:\n%v",
6✔
518
                                sweepCtx.tx.TxHash(), len(req.Inputs),
6✔
519
                                f.FeeRate(), sweepCtx.fee,
6✔
520
                                inputTypeSummary(req.Inputs))
6✔
521

6✔
522
                        return nil
6✔
523

524
                // If the error indicates the fees paid is not enough, we will
525
                // ask the fee function to increase the fee rate and retry.
526
                case errors.Is(err, lnwallet.ErrMempoolFee):
2✔
527
                        // We should at least start with a feerate above the
2✔
528
                        // mempool min feerate, so if we get this error, it
2✔
529
                        // means something is wrong earlier in the pipeline.
2✔
530
                        log.Errorf("Current fee=%v, feerate=%v, %v",
2✔
531
                                sweepCtx.fee, f.FeeRate(), err)
2✔
532

2✔
533
                        fallthrough
2✔
534

535
                // We are not paying enough fees so we increase it.
536
                case errors.Is(err, chain.ErrInsufficientFee):
4✔
537
                        increased := false
4✔
538

4✔
539
                        // Keep calling the fee function until the fee rate is
4✔
540
                        // increased or maxed out.
4✔
541
                        for !increased {
9✔
542
                                log.Debugf("Increasing fee for next round, "+
5✔
543
                                        "current fee=%v, feerate=%v",
5✔
544
                                        sweepCtx.fee, f.FeeRate())
5✔
545

5✔
546
                                // If the fee function tells us that we have
5✔
547
                                // used up the budget, we will return an error
5✔
548
                                // indicating this tx cannot be made. The
5✔
549
                                // sweeper should handle this error and try to
5✔
550
                                // cluster these inputs differetly.
5✔
551
                                increased, err = f.Increment()
5✔
552
                                if err != nil {
6✔
553
                                        return err
1✔
554
                                }
1✔
555
                        }
556

557
                // TODO(yy): suppose there's only one bad input, we can do a
558
                // binary search to find out which input is causing this error
559
                // by recreating a tx using half of the inputs and check its
560
                // mempool acceptance.
561
                default:
2✔
562
                        log.Debugf("Failed to create RBF-compliant tx: %v", err)
2✔
563
                        return err
2✔
564
                }
565
        }
566
}
567

568
// storeRecord stores the given record in the records map.
569
func (t *TxPublisher) storeRecord(requestID uint64, tx *wire.MsgTx,
570
        req *BumpRequest, f FeeFunction, fee btcutil.Amount,
571
        outpointToTxIndex map[wire.OutPoint]int) {
14✔
572

14✔
573
        // Register the record.
14✔
574
        t.records.Store(requestID, &monitorRecord{
14✔
575
                tx:                tx,
14✔
576
                req:               req,
14✔
577
                feeFunction:       f,
14✔
578
                fee:               fee,
14✔
579
                outpointToTxIndex: outpointToTxIndex,
14✔
580
        })
14✔
581
}
14✔
582

583
// createAndCheckTx creates a tx based on the given inputs, change output
584
// script, and the fee rate. In addition, it validates the tx's mempool
585
// acceptance before returning a tx that can be published directly, along with
586
// its fee.
587
func (t *TxPublisher) createAndCheckTx(req *BumpRequest,
588
        f FeeFunction) (*sweepTxCtx, error) {
22✔
589

22✔
590
        // Create the sweep tx with max fee rate of 0 as the fee function
22✔
591
        // guarantees the fee rate used here won't exceed the max fee rate.
22✔
592
        sweepCtx, err := t.createSweepTx(
22✔
593
                req.Inputs, req.DeliveryAddress, f.FeeRate(),
22✔
594
        )
22✔
595
        if err != nil {
22✔
UNCOV
596
                return sweepCtx, fmt.Errorf("create sweep tx: %w", err)
×
UNCOV
597
        }
×
598

599
        // Sanity check the budget still covers the fee.
600
        if sweepCtx.fee > req.Budget {
24✔
601
                return sweepCtx, fmt.Errorf("%w: budget=%v, fee=%v",
2✔
602
                        ErrNotEnoughBudget, req.Budget, sweepCtx.fee)
2✔
603
        }
2✔
604

605
        // If we had an extra txOut, then we'll update the result to include
606
        // it.
607
        req.ExtraTxOut = sweepCtx.extraTxOut
20✔
608

20✔
609
        // Validate the tx's mempool acceptance.
20✔
610
        err = t.cfg.Wallet.CheckMempoolAcceptance(sweepCtx.tx)
20✔
611

20✔
612
        // Exit early if the tx is valid.
20✔
613
        if err == nil {
31✔
614
                return sweepCtx, nil
11✔
615
        }
11✔
616

617
        // Print an error log if the chain backend doesn't support the mempool
618
        // acceptance test RPC.
619
        if errors.Is(err, rpcclient.ErrBackendVersion) {
9✔
620
                log.Errorf("TestMempoolAccept not supported by backend, " +
×
621
                        "consider upgrading it to a newer version")
×
622
                return sweepCtx, nil
×
623
        }
×
624

625
        // We are running on a backend that doesn't implement the RPC
626
        // testmempoolaccept, eg, neutrino, so we'll skip the check.
627
        if errors.Is(err, chain.ErrUnimplemented) {
9✔
UNCOV
628
                log.Debug("Skipped testmempoolaccept due to not implemented")
×
UNCOV
629
                return sweepCtx, nil
×
UNCOV
630
        }
×
631

632
        return sweepCtx, fmt.Errorf("tx=%v failed mempool check: %w",
9✔
633
                sweepCtx.tx.TxHash(), err)
9✔
634
}
635

636
// broadcast takes a monitored tx and publishes it to the network. Prior to the
637
// broadcast, it will subscribe the tx's confirmation notification and attach
638
// the event channel to the record. Any broadcast-related errors will not be
639
// returned here, instead, they will be put inside the `BumpResult` and
640
// returned to the caller.
641
func (t *TxPublisher) broadcast(requestID uint64) (*BumpResult, error) {
9✔
642
        // Get the record being monitored.
9✔
643
        record, ok := t.records.Load(requestID)
9✔
644
        if !ok {
10✔
645
                return nil, fmt.Errorf("tx record %v not found", requestID)
1✔
646
        }
1✔
647

648
        txid := record.tx.TxHash()
8✔
649

8✔
650
        tx := record.tx
8✔
651
        log.Debugf("Publishing sweep tx %v, num_inputs=%v, height=%v",
8✔
652
                txid, len(tx.TxIn), t.currentHeight.Load())
8✔
653

8✔
654
        // Before we go to broadcast, we'll notify the aux sweeper, if it's
8✔
655
        // present of this new broadcast attempt.
8✔
656
        err := fn.MapOptionZ(t.cfg.AuxSweeper, func(aux AuxSweeper) error {
16✔
657
                return aux.NotifyBroadcast(
8✔
658
                        record.req, tx, record.fee, record.outpointToTxIndex,
8✔
659
                )
8✔
660
        })
8✔
661
        if err != nil {
8✔
662
                return nil, fmt.Errorf("unable to notify aux sweeper: %w", err)
×
663
        }
×
664

665
        // Set the event, and change it to TxFailed if the wallet fails to
666
        // publish it.
667
        event := TxPublished
8✔
668

8✔
669
        // Publish the sweeping tx with customized label. If the publish fails,
8✔
670
        // this error will be saved in the `BumpResult` and it will be removed
8✔
671
        // from being monitored.
8✔
672
        err = t.cfg.Wallet.PublishTransaction(
8✔
673
                tx, labels.MakeLabel(labels.LabelTypeSweepTransaction, nil),
8✔
674
        )
8✔
675
        if err != nil {
11✔
676
                // NOTE: we decide to attach this error to the result instead
3✔
677
                // of returning it here because by the time the tx reaches
3✔
678
                // here, it should have passed the mempool acceptance check. If
3✔
679
                // it still fails to be broadcast, it's likely a non-RBF
3✔
680
                // related error happened. So we send this error back to the
3✔
681
                // caller so that it can handle it properly.
3✔
682
                //
3✔
683
                // TODO(yy): find out which input is causing the failure.
3✔
684
                log.Errorf("Failed to publish tx %v: %v", txid, err)
3✔
685
                event = TxFailed
3✔
686
        }
3✔
687

688
        result := &BumpResult{
8✔
689
                Event:     event,
8✔
690
                Tx:        record.tx,
8✔
691
                Fee:       record.fee,
8✔
692
                FeeRate:   record.feeFunction.FeeRate(),
8✔
693
                Err:       err,
8✔
694
                requestID: requestID,
8✔
695
        }
8✔
696

8✔
697
        return result, nil
8✔
698
}
699

700
// notifyResult sends the result to the resultChan specified by the requestID.
701
// This channel is expected to be read by the caller.
702
func (t *TxPublisher) notifyResult(result *BumpResult) {
11✔
703
        id := result.requestID
11✔
704
        subscriber, ok := t.subscriberChans.Load(id)
11✔
705
        if !ok {
11✔
UNCOV
706
                log.Errorf("Result chan for id=%v not found", id)
×
UNCOV
707
                return
×
UNCOV
708
        }
×
709

710
        log.Debugf("Sending result %v for requestID=%v", result, id)
11✔
711

11✔
712
        select {
11✔
713
        // Send the result to the subscriber.
714
        //
715
        // TODO(yy): Add timeout in case it's blocking?
716
        case subscriber <- result:
10✔
717
        case <-t.quit:
1✔
718
                log.Debug("Fee bumper stopped")
1✔
719
        }
720
}
721

722
// removeResult removes the tracking of the result if the result contains a
723
// non-nil error, or the tx is confirmed, the record will be removed from the
724
// maps.
725
func (t *TxPublisher) removeResult(result *BumpResult) {
11✔
726
        id := result.requestID
11✔
727

11✔
728
        var txid chainhash.Hash
11✔
729
        if result.Tx != nil {
20✔
730
                txid = result.Tx.TxHash()
9✔
731
        }
9✔
732

733
        // Remove the record from the maps if there's an error or the tx is
734
        // confirmed. When there's an error, it means this tx has failed its
735
        // broadcast and cannot be retried. There are two cases it may fail,
736
        // - when the budget cannot cover the increased fee calculated by the
737
        //   fee function, hence the budget is used up.
738
        // - when a non-fee related error returned from PublishTransaction.
739
        switch result.Event {
11✔
740
        case TxFailed:
2✔
741
                log.Errorf("Removing monitor record=%v, tx=%v, due to err: %v",
2✔
742
                        id, txid, result.Err)
2✔
743

744
        case TxConfirmed:
3✔
745
                // Remove the record if the tx is confirmed.
3✔
746
                log.Debugf("Removing confirmed monitor record=%v, tx=%v", id,
3✔
747
                        txid)
3✔
748

749
        case TxFatal:
2✔
750
                // Remove the record if there's an error.
2✔
751
                log.Debugf("Removing monitor record=%v due to fatal err: %v",
2✔
752
                        id, result.Err)
2✔
753

754
        // Do nothing if it's neither failed or confirmed.
755
        default:
4✔
756
                log.Tracef("Skipping record removal for id=%v, event=%v", id,
4✔
757
                        result.Event)
4✔
758

4✔
759
                return
4✔
760
        }
761

762
        t.records.Delete(id)
7✔
763
        t.subscriberChans.Delete(id)
7✔
764
}
765

766
// handleResult handles the result of a tx broadcast. It will notify the
767
// subscriber and remove the record if the tx is confirmed or failed to be
768
// broadcast.
769
func (t *TxPublisher) handleResult(result *BumpResult) {
8✔
770
        // Notify the subscriber.
8✔
771
        t.notifyResult(result)
8✔
772

8✔
773
        // Remove the record if it's failed or confirmed.
8✔
774
        t.removeResult(result)
8✔
775
}
8✔
776

777
// monitorRecord is used to keep track of the tx being monitored by the
778
// publisher internally.
779
type monitorRecord struct {
780
        // tx is the tx being monitored.
781
        tx *wire.MsgTx
782

783
        // req is the original request.
784
        req *BumpRequest
785

786
        // feeFunction is the fee bumping algorithm used by the publisher.
787
        feeFunction FeeFunction
788

789
        // fee is the fee paid by the tx.
790
        fee btcutil.Amount
791

792
        // outpointToTxIndex is a map of outpoint to tx index.
793
        outpointToTxIndex map[wire.OutPoint]int
794
}
795

796
// Start starts the publisher by subscribing to block epoch updates and kicking
797
// off the monitor loop.
UNCOV
798
func (t *TxPublisher) Start(beat chainio.Blockbeat) error {
×
UNCOV
799
        log.Info("TxPublisher starting...")
×
UNCOV
800

×
UNCOV
801
        if t.started.Swap(true) {
×
802
                return fmt.Errorf("TxPublisher started more than once")
×
803
        }
×
804

805
        // Set the current height.
UNCOV
806
        t.currentHeight.Store(beat.Height())
×
UNCOV
807

×
UNCOV
808
        t.wg.Add(1)
×
UNCOV
809
        go t.monitor()
×
UNCOV
810

×
UNCOV
811
        log.Debugf("TxPublisher started")
×
UNCOV
812

×
UNCOV
813
        return nil
×
814
}
815

816
// Stop stops the publisher and waits for the monitor loop to exit.
UNCOV
817
func (t *TxPublisher) Stop() error {
×
UNCOV
818
        log.Info("TxPublisher stopping...")
×
UNCOV
819

×
UNCOV
820
        if t.stopped.Swap(true) {
×
821
                return fmt.Errorf("TxPublisher stopped more than once")
×
822
        }
×
823

UNCOV
824
        close(t.quit)
×
UNCOV
825
        t.wg.Wait()
×
UNCOV
826

×
UNCOV
827
        log.Debug("TxPublisher stopped")
×
UNCOV
828

×
UNCOV
829
        return nil
×
830
}
831

832
// monitor is the main loop driven by new blocks. Whevenr a new block arrives,
833
// it will examine all the txns being monitored, and check if any of them needs
834
// to be bumped. If so, it will attempt to bump the fee of the tx.
835
//
836
// NOTE: Must be run as a goroutine.
UNCOV
837
func (t *TxPublisher) monitor() {
×
UNCOV
838
        defer t.wg.Done()
×
UNCOV
839

×
UNCOV
840
        for {
×
UNCOV
841
                select {
×
UNCOV
842
                case beat := <-t.BlockbeatChan:
×
UNCOV
843
                        height := beat.Height()
×
UNCOV
844
                        log.Debugf("TxPublisher received new block: %v", height)
×
UNCOV
845

×
UNCOV
846
                        // Update the best known height for the publisher.
×
UNCOV
847
                        t.currentHeight.Store(height)
×
UNCOV
848

×
UNCOV
849
                        // Check all monitored txns to see if any of them needs
×
UNCOV
850
                        // to be bumped.
×
UNCOV
851
                        t.processRecords()
×
UNCOV
852

×
UNCOV
853
                        // Notify we've processed the block.
×
UNCOV
854
                        t.NotifyBlockProcessed(beat, nil)
×
855

UNCOV
856
                case <-t.quit:
×
UNCOV
857
                        log.Debug("Fee bumper stopped, exit monitor")
×
UNCOV
858
                        return
×
859
                }
860
        }
861
}
862

863
// processRecords checks all the txns being monitored, and checks if any of
864
// them needs to be bumped. If so, it will attempt to bump the fee of the tx.
865
func (t *TxPublisher) processRecords() {
1✔
866
        // confirmedRecords stores a map of the records which have been
1✔
867
        // confirmed.
1✔
868
        confirmedRecords := make(map[uint64]*monitorRecord)
1✔
869

1✔
870
        // feeBumpRecords stores a map of records which need to be bumped.
1✔
871
        feeBumpRecords := make(map[uint64]*monitorRecord)
1✔
872

1✔
873
        // failedRecords stores a map of records which has inputs being spent
1✔
874
        // by a third party.
1✔
875
        //
1✔
876
        // NOTE: this is only used for neutrino backend.
1✔
877
        failedRecords := make(map[uint64]*monitorRecord)
1✔
878

1✔
879
        // initialRecords stores a map of records which are being created and
1✔
880
        // published for the first time.
1✔
881
        initialRecords := make(map[uint64]*monitorRecord)
1✔
882

1✔
883
        // visitor is a helper closure that visits each record and divides them
1✔
884
        // into two groups.
1✔
885
        visitor := func(requestID uint64, r *monitorRecord) error {
3✔
886
                if r.tx == nil {
2✔
UNCOV
887
                        initialRecords[requestID] = r
×
UNCOV
888
                        return nil
×
UNCOV
889
                }
×
890

891
                log.Tracef("Checking monitor recordID=%v for tx=%v", requestID,
2✔
892
                        r.tx.TxHash())
2✔
893

2✔
894
                // If the tx is already confirmed, we can stop monitoring it.
2✔
895
                if t.isConfirmed(r.tx.TxHash()) {
3✔
896
                        confirmedRecords[requestID] = r
1✔
897

1✔
898
                        // Move to the next record.
1✔
899
                        return nil
1✔
900
                }
1✔
901

902
                // Check whether the inputs has been spent by a third party.
903
                //
904
                // NOTE: this check is only done for neutrino backend.
905
                if t.isThirdPartySpent(r.tx.TxHash(), r.req.Inputs) {
1✔
UNCOV
906
                        failedRecords[requestID] = r
×
UNCOV
907

×
UNCOV
908
                        // Move to the next record.
×
UNCOV
909
                        return nil
×
UNCOV
910
                }
×
911

912
                feeBumpRecords[requestID] = r
1✔
913

1✔
914
                // Return nil to move to the next record.
1✔
915
                return nil
1✔
916
        }
917

918
        // Iterate through all the records and divide them into four groups.
919
        t.records.ForEach(visitor)
1✔
920

1✔
921
        // Handle the initial broadcast.
1✔
922
        for requestID, r := range initialRecords {
1✔
UNCOV
923
                t.handleInitialBroadcast(r, requestID)
×
UNCOV
924
        }
×
925

926
        // For records that are confirmed, we'll notify the caller about this
927
        // result.
928
        for requestID, r := range confirmedRecords {
2✔
929
                log.Debugf("Tx=%v is confirmed", r.tx.TxHash())
1✔
930
                t.wg.Add(1)
1✔
931
                go t.handleTxConfirmed(r, requestID)
1✔
932
        }
1✔
933

934
        // Get the current height to be used in the following goroutines.
935
        currentHeight := t.currentHeight.Load()
1✔
936

1✔
937
        // For records that are not confirmed, we perform a fee bump if needed.
1✔
938
        for requestID, r := range feeBumpRecords {
2✔
939
                log.Debugf("Attempting to fee bump Tx=%v", r.tx.TxHash())
1✔
940
                t.wg.Add(1)
1✔
941
                go t.handleFeeBumpTx(requestID, r, currentHeight)
1✔
942
        }
1✔
943

944
        // For records that are failed, we'll notify the caller about this
945
        // result.
946
        for requestID, r := range failedRecords {
1✔
UNCOV
947
                log.Debugf("Tx=%v has inputs been spent by a third party, "+
×
UNCOV
948
                        "failing it now", r.tx.TxHash())
×
UNCOV
949
                t.wg.Add(1)
×
UNCOV
950
                go t.handleThirdPartySpent(r, requestID)
×
UNCOV
951
        }
×
952
}
953

954
// handleTxConfirmed is called when a monitored tx is confirmed. It will
955
// notify the subscriber then remove the record from the maps .
956
//
957
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
958
func (t *TxPublisher) handleTxConfirmed(r *monitorRecord, requestID uint64) {
2✔
959
        defer t.wg.Done()
2✔
960

2✔
961
        // Create a result that will be sent to the resultChan which is
2✔
962
        // listened by the caller.
2✔
963
        result := &BumpResult{
2✔
964
                Event:     TxConfirmed,
2✔
965
                Tx:        r.tx,
2✔
966
                requestID: requestID,
2✔
967
                Fee:       r.fee,
2✔
968
                FeeRate:   r.feeFunction.FeeRate(),
2✔
969
        }
2✔
970

2✔
971
        // Notify that this tx is confirmed and remove the record from the map.
2✔
972
        t.handleResult(result)
2✔
973
}
2✔
974

975
// handleInitialTxError takes the error from `initializeTx` and decides the
976
// bump event. It will construct a BumpResult and handles it.
977
func (t *TxPublisher) handleInitialTxError(requestID uint64, err error) {
2✔
978
        // We now decide what type of event to send.
2✔
979
        var event BumpEvent
2✔
980

2✔
981
        switch {
2✔
982
        // When the error is due to a dust output, we'll send a TxFailed so
983
        // these inputs can be retried with a different group in the next
984
        // block.
UNCOV
985
        case errors.Is(err, ErrTxNoOutput):
×
UNCOV
986
                event = TxFailed
×
987

988
        // When the error is due to budget being used up, we'll send a TxFailed
989
        // so these inputs can be retried with a different group in the next
990
        // block.
UNCOV
991
        case errors.Is(err, ErrMaxPosition):
×
UNCOV
992
                event = TxFailed
×
993

994
        // When the error is due to zero fee rate delta, we'll send a TxFailed
995
        // so these inputs can be retried in the next block.
UNCOV
996
        case errors.Is(err, ErrZeroFeeRateDelta):
×
UNCOV
997
                event = TxFailed
×
998

999
        // Otherwise this is not a fee-related error and the tx cannot be
1000
        // retried. In that case we will fail ALL the inputs in this tx, which
1001
        // means they will be removed from the sweeper and never be tried
1002
        // again.
1003
        //
1004
        // TODO(yy): Find out which input is causing the failure and fail that
1005
        // one only.
1006
        default:
2✔
1007
                event = TxFatal
2✔
1008
        }
1009

1010
        result := &BumpResult{
2✔
1011
                Event:     event,
2✔
1012
                Err:       err,
2✔
1013
                requestID: requestID,
2✔
1014
        }
2✔
1015

2✔
1016
        t.handleResult(result)
2✔
1017
}
1018

1019
// handleInitialBroadcast is called when a new request is received. It will
1020
// handle the initial tx creation and broadcast. In details,
1021
// 1. init a fee function based on the given strategy.
1022
// 2. create an RBF-compliant tx and monitor it for confirmation.
1023
// 3. notify the initial broadcast result back to the caller.
1024
func (t *TxPublisher) handleInitialBroadcast(r *monitorRecord,
1025
        requestID uint64) {
4✔
1026

4✔
1027
        log.Debugf("Initial broadcast for requestID=%v", requestID)
4✔
1028

4✔
1029
        var (
4✔
1030
                result *BumpResult
4✔
1031
                err    error
4✔
1032
        )
4✔
1033

4✔
1034
        // Attempt an initial broadcast which is guaranteed to comply with the
4✔
1035
        // RBF rules.
4✔
1036
        //
4✔
1037
        // Create the initial tx to be broadcasted.
4✔
1038
        err = t.initializeTx(requestID, r.req)
4✔
1039
        if err != nil {
6✔
1040
                log.Errorf("Initial broadcast failed: %v", err)
2✔
1041

2✔
1042
                // We now handle the initialization error and exit.
2✔
1043
                t.handleInitialTxError(requestID, err)
2✔
1044

2✔
1045
                return
2✔
1046
        }
2✔
1047

1048
        // Successfully created the first tx, now broadcast it.
1049
        result, err = t.broadcast(requestID)
2✔
1050
        if err != nil {
2✔
1051
                // The broadcast failed, which can only happen if the tx record
×
1052
                // cannot be found or the aux sweeper returns an error. In
×
1053
                // either case, we will send back a TxFail event so these
×
1054
                // inputs can be retried.
×
1055
                result = &BumpResult{
×
1056
                        Event:     TxFailed,
×
1057
                        Err:       err,
×
1058
                        requestID: requestID,
×
1059
                }
×
1060
        }
×
1061

1062
        t.handleResult(result)
2✔
1063
}
1064

1065
// handleFeeBumpTx checks if the tx needs to be bumped, and if so, it will
1066
// attempt to bump the fee of the tx.
1067
//
1068
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
1069
func (t *TxPublisher) handleFeeBumpTx(requestID uint64, r *monitorRecord,
1070
        currentHeight int32) {
4✔
1071

4✔
1072
        defer t.wg.Done()
4✔
1073

4✔
1074
        oldTxid := r.tx.TxHash()
4✔
1075

4✔
1076
        // Get the current conf target for this record.
4✔
1077
        confTarget := calcCurrentConfTarget(currentHeight, r.req.DeadlineHeight)
4✔
1078

4✔
1079
        // Ask the fee function whether a bump is needed. We expect the fee
4✔
1080
        // function to increase its returned fee rate after calling this
4✔
1081
        // method.
4✔
1082
        increased, err := r.feeFunction.IncreaseFeeRate(confTarget)
4✔
1083
        if err != nil {
5✔
1084
                // TODO(yy): send this error back to the sweeper so it can
1✔
1085
                // re-group the inputs?
1✔
1086
                log.Errorf("Failed to increase fee rate for tx %v at "+
1✔
1087
                        "height=%v: %v", oldTxid, t.currentHeight.Load(), err)
1✔
1088

1✔
1089
                return
1✔
1090
        }
1✔
1091

1092
        // If the fee rate was not increased, there's no need to bump the fee.
1093
        if !increased {
4✔
1094
                log.Tracef("Skip bumping tx %v at height=%v", oldTxid,
1✔
1095
                        t.currentHeight.Load())
1✔
1096

1✔
1097
                return
1✔
1098
        }
1✔
1099

1100
        // The fee function now has a new fee rate, we will use it to bump the
1101
        // fee of the tx.
1102
        resultOpt := t.createAndPublishTx(requestID, r)
2✔
1103

2✔
1104
        // If there's a result, we will notify the caller about the result.
2✔
1105
        resultOpt.WhenSome(func(result BumpResult) {
4✔
1106
                // Notify the new result.
2✔
1107
                t.handleResult(&result)
2✔
1108
        })
2✔
1109
}
1110

1111
// handleThirdPartySpent is called when the inputs in an unconfirmed tx is
1112
// spent. It will notify the subscriber then remove the record from the maps
1113
// and send a TxFailed event to the subscriber.
1114
//
1115
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
1116
func (t *TxPublisher) handleThirdPartySpent(r *monitorRecord,
UNCOV
1117
        requestID uint64) {
×
UNCOV
1118

×
UNCOV
1119
        defer t.wg.Done()
×
UNCOV
1120

×
UNCOV
1121
        // Create a result that will be sent to the resultChan which is
×
UNCOV
1122
        // listened by the caller.
×
UNCOV
1123
        //
×
UNCOV
1124
        // TODO(yy): create a new state `TxThirdPartySpent` to notify the
×
UNCOV
1125
        // sweeper to remove the input, hence moving the monitoring of inputs
×
UNCOV
1126
        // spent inside the fee bumper.
×
UNCOV
1127
        result := &BumpResult{
×
UNCOV
1128
                Event:     TxFailed,
×
UNCOV
1129
                Tx:        r.tx,
×
UNCOV
1130
                requestID: requestID,
×
UNCOV
1131
                Err:       ErrThirdPartySpent,
×
UNCOV
1132
        }
×
UNCOV
1133

×
UNCOV
1134
        // Notify that this tx is confirmed and remove the record from the map.
×
UNCOV
1135
        t.handleResult(result)
×
UNCOV
1136
}
×
1137

1138
// createAndPublishTx creates a new tx with a higher fee rate and publishes it
1139
// to the network. It will update the record with the new tx and fee rate if
1140
// successfully created, and return the result when published successfully.
1141
func (t *TxPublisher) createAndPublishTx(requestID uint64,
1142
        r *monitorRecord) fn.Option[BumpResult] {
7✔
1143

7✔
1144
        // Fetch the old tx.
7✔
1145
        oldTx := r.tx
7✔
1146

7✔
1147
        // Create a new tx with the new fee rate.
7✔
1148
        //
7✔
1149
        // NOTE: The fee function is expected to have increased its returned
7✔
1150
        // fee rate after calling the SkipFeeBump method. So we can use it
7✔
1151
        // directly here.
7✔
1152
        sweepCtx, err := t.createAndCheckTx(r.req, r.feeFunction)
7✔
1153

7✔
1154
        // If the error is fee related, we will return no error and let the fee
7✔
1155
        // bumper retry it at next block.
7✔
1156
        //
7✔
1157
        // NOTE: we can check the RBF error here and ask the fee function to
7✔
1158
        // recalculate the fee rate. However, this would defeat the purpose of
7✔
1159
        // using a deadline based fee function:
7✔
1160
        // - if the deadline is far away, there's no rush to RBF the tx.
7✔
1161
        // - if the deadline is close, we expect the fee function to give us a
7✔
1162
        //   higher fee rate. If the fee rate cannot satisfy the RBF rules, it
7✔
1163
        //   means the budget is not enough.
7✔
1164
        if errors.Is(err, chain.ErrInsufficientFee) ||
7✔
1165
                errors.Is(err, lnwallet.ErrMempoolFee) {
9✔
1166

2✔
1167
                log.Debugf("Failed to bump tx %v: %v", oldTx.TxHash(), err)
2✔
1168
                return fn.None[BumpResult]()
2✔
1169
        }
2✔
1170

1171
        // If the error is not fee related, we will return a `TxFailed` event
1172
        // so this input can be retried.
1173
        if err != nil {
6✔
1174
                // If the tx doesn't not have enought budget, we will return a
1✔
1175
                // result so the sweeper can handle it by re-clustering the
1✔
1176
                // utxos.
1✔
1177
                if errors.Is(err, ErrNotEnoughBudget) {
2✔
1178
                        log.Warnf("Fail to fee bump tx %v: %v", oldTx.TxHash(),
1✔
1179
                                err)
1✔
1180
                } else {
1✔
UNCOV
1181
                        // Otherwise, an unexpected error occurred, we will
×
UNCOV
1182
                        // fail the tx and let the sweeper retry the whole
×
UNCOV
1183
                        // process.
×
UNCOV
1184
                        log.Errorf("Failed to bump tx %v: %v", oldTx.TxHash(),
×
UNCOV
1185
                                err)
×
UNCOV
1186
                }
×
1187

1188
                return fn.Some(BumpResult{
1✔
1189
                        Event:     TxFailed,
1✔
1190
                        Tx:        oldTx,
1✔
1191
                        Err:       err,
1✔
1192
                        requestID: requestID,
1✔
1193
                })
1✔
1194
        }
1195

1196
        // The tx has been created without any errors, we now register a new
1197
        // record by overwriting the same requestID.
1198
        t.records.Store(requestID, &monitorRecord{
4✔
1199
                tx:                sweepCtx.tx,
4✔
1200
                req:               r.req,
4✔
1201
                feeFunction:       r.feeFunction,
4✔
1202
                fee:               sweepCtx.fee,
4✔
1203
                outpointToTxIndex: sweepCtx.outpointToTxIndex,
4✔
1204
        })
4✔
1205

4✔
1206
        // Attempt to broadcast this new tx.
4✔
1207
        result, err := t.broadcast(requestID)
4✔
1208
        if err != nil {
4✔
1209
                log.Infof("Failed to broadcast replacement tx %v: %v",
×
1210
                        sweepCtx.tx.TxHash(), err)
×
1211

×
1212
                return fn.None[BumpResult]()
×
1213
        }
×
1214

1215
        // If the result error is fee related, we will return no error and let
1216
        // the fee bumper retry it at next block.
1217
        //
1218
        // NOTE: we may get this error if we've bypassed the mempool check,
1219
        // which means we are suing neutrino backend.
1220
        if errors.Is(result.Err, chain.ErrInsufficientFee) ||
4✔
1221
                errors.Is(result.Err, lnwallet.ErrMempoolFee) {
4✔
UNCOV
1222

×
UNCOV
1223
                log.Debugf("Failed to bump tx %v: %v", oldTx.TxHash(), err)
×
UNCOV
1224
                return fn.None[BumpResult]()
×
UNCOV
1225
        }
×
1226

1227
        // A successful replacement tx is created, attach the old tx.
1228
        result.ReplacedTx = oldTx
4✔
1229

4✔
1230
        // If the new tx failed to be published, we will return the result so
4✔
1231
        // the caller can handle it.
4✔
1232
        if result.Event == TxFailed {
5✔
1233
                return fn.Some(*result)
1✔
1234
        }
1✔
1235

1236
        log.Infof("Replaced tx=%v with new tx=%v", oldTx.TxHash(),
3✔
1237
                sweepCtx.tx.TxHash())
3✔
1238

3✔
1239
        // Otherwise, it's a successful RBF, set the event and return.
3✔
1240
        result.Event = TxReplaced
3✔
1241

3✔
1242
        return fn.Some(*result)
3✔
1243
}
1244

1245
// isConfirmed checks the btcwallet to see whether the tx is confirmed.
1246
func (t *TxPublisher) isConfirmed(txid chainhash.Hash) bool {
2✔
1247
        details, err := t.cfg.Wallet.GetTransactionDetails(&txid)
2✔
1248
        if err != nil {
2✔
UNCOV
1249
                log.Warnf("Failed to get tx details for %v: %v", txid, err)
×
UNCOV
1250
                return false
×
UNCOV
1251
        }
×
1252

1253
        return details.NumConfirmations > 0
2✔
1254
}
1255

1256
// isThirdPartySpent checks whether the inputs of the tx has already been spent
1257
// by a third party. When a tx is not confirmed, yet its inputs has been spent,
1258
// then it must be spent by a different tx other than the sweeping tx here.
1259
//
1260
// NOTE: this check is only performed for neutrino backend as it has no
1261
// reliable way to tell a tx has been replaced.
1262
func (t *TxPublisher) isThirdPartySpent(txid chainhash.Hash,
1263
        inputs []input.Input) bool {
1✔
1264

1✔
1265
        // Skip this check for if this is not neutrino backend.
1✔
1266
        if !t.isNeutrinoBackend() {
2✔
1267
                return false
1✔
1268
        }
1✔
1269

1270
        // Iterate all the inputs and check if they have been spent already.
UNCOV
1271
        for _, inp := range inputs {
×
UNCOV
1272
                op := inp.OutPoint()
×
UNCOV
1273

×
UNCOV
1274
                // For wallet utxos, the height hint is not set - we don't need
×
UNCOV
1275
                // to monitor them for third party spend.
×
UNCOV
1276
                heightHint := inp.HeightHint()
×
UNCOV
1277
                if heightHint == 0 {
×
UNCOV
1278
                        log.Debugf("Skipped third party check for wallet "+
×
UNCOV
1279
                                "input %v", op)
×
UNCOV
1280

×
UNCOV
1281
                        continue
×
1282
                }
1283

1284
                // If the input has already been spent after the height hint, a
1285
                // spend event is sent back immediately.
UNCOV
1286
                spendEvent, err := t.cfg.Notifier.RegisterSpendNtfn(
×
UNCOV
1287
                        &op, inp.SignDesc().Output.PkScript, heightHint,
×
UNCOV
1288
                )
×
UNCOV
1289
                if err != nil {
×
1290
                        log.Criticalf("Failed to register spend ntfn for "+
×
1291
                                "input=%v: %v", op, err)
×
1292
                        return false
×
1293
                }
×
1294

1295
                // Remove the subscription when exit.
UNCOV
1296
                defer spendEvent.Cancel()
×
UNCOV
1297

×
UNCOV
1298
                // Do a non-blocking read to see if the output has been spent.
×
UNCOV
1299
                select {
×
UNCOV
1300
                case spend, ok := <-spendEvent.Spend:
×
UNCOV
1301
                        if !ok {
×
1302
                                log.Debugf("Spend ntfn for %v canceled", op)
×
1303
                                return false
×
1304
                        }
×
1305

UNCOV
1306
                        spendingTxID := spend.SpendingTx.TxHash()
×
UNCOV
1307

×
UNCOV
1308
                        // If the spending tx is the same as the sweeping tx
×
UNCOV
1309
                        // then we are good.
×
UNCOV
1310
                        if spendingTxID == txid {
×
UNCOV
1311
                                continue
×
1312
                        }
1313

UNCOV
1314
                        log.Warnf("Detected third party spent of output=%v "+
×
UNCOV
1315
                                "in tx=%v", op, spend.SpendingTx.TxHash())
×
UNCOV
1316

×
UNCOV
1317
                        return true
×
1318

1319
                // Move to the next input.
UNCOV
1320
                default:
×
1321
                }
1322
        }
1323

UNCOV
1324
        return false
×
1325
}
1326

1327
// calcCurrentConfTarget calculates the current confirmation target based on
1328
// the deadline height. The conf target is capped at 0 if the deadline has
1329
// already been past.
1330
func calcCurrentConfTarget(currentHeight, deadline int32) uint32 {
12✔
1331
        var confTarget uint32
12✔
1332

12✔
1333
        // Calculate how many blocks left until the deadline.
12✔
1334
        deadlineDelta := deadline - currentHeight
12✔
1335

12✔
1336
        // If we are already past the deadline, we will set the conf target to
12✔
1337
        // be 1.
12✔
1338
        if deadlineDelta < 0 {
16✔
1339
                log.Warnf("Deadline is %d blocks behind current height %v",
4✔
1340
                        -deadlineDelta, currentHeight)
4✔
1341

4✔
1342
                confTarget = 0
4✔
1343
        } else {
12✔
1344
                confTarget = uint32(deadlineDelta)
8✔
1345
        }
8✔
1346

1347
        return confTarget
12✔
1348
}
1349

1350
// sweepTxCtx houses a sweep transaction with additional context.
1351
type sweepTxCtx struct {
1352
        tx *wire.MsgTx
1353

1354
        fee btcutil.Amount
1355

1356
        extraTxOut fn.Option[SweepOutput]
1357

1358
        // outpointToTxIndex maps the outpoint of the inputs to their index in
1359
        // the sweep transaction.
1360
        outpointToTxIndex map[wire.OutPoint]int
1361
}
1362

1363
// createSweepTx creates a sweeping tx based on the given inputs, change
1364
// address and fee rate.
1365
func (t *TxPublisher) createSweepTx(inputs []input.Input,
1366
        changePkScript lnwallet.AddrWithKey,
1367
        feeRate chainfee.SatPerKWeight) (*sweepTxCtx, error) {
22✔
1368

22✔
1369
        // Validate and calculate the fee and change amount.
22✔
1370
        txFee, changeOutputsOpt, locktimeOpt, err := prepareSweepTx(
22✔
1371
                inputs, changePkScript, feeRate, t.currentHeight.Load(),
22✔
1372
                t.cfg.AuxSweeper,
22✔
1373
        )
22✔
1374
        if err != nil {
22✔
UNCOV
1375
                return nil, err
×
UNCOV
1376
        }
×
1377

1378
        var (
22✔
1379
                // Create the sweep transaction that we will be building. We
22✔
1380
                // use version 2 as it is required for CSV.
22✔
1381
                sweepTx = wire.NewMsgTx(2)
22✔
1382

22✔
1383
                // We'll add the inputs as we go so we know the final ordering
22✔
1384
                // of inputs to sign.
22✔
1385
                idxs []input.Input
22✔
1386
        )
22✔
1387

22✔
1388
        // We start by adding all inputs that commit to an output. We do this
22✔
1389
        // since the input and output index must stay the same for the
22✔
1390
        // signatures to be valid.
22✔
1391
        outpointToTxIndex := make(map[wire.OutPoint]int)
22✔
1392
        for _, o := range inputs {
44✔
1393
                if o.RequiredTxOut() == nil {
44✔
1394
                        continue
22✔
1395
                }
1396

UNCOV
1397
                idxs = append(idxs, o)
×
UNCOV
1398
                sweepTx.AddTxIn(&wire.TxIn{
×
UNCOV
1399
                        PreviousOutPoint: o.OutPoint(),
×
UNCOV
1400
                        Sequence:         o.BlocksToMaturity(),
×
UNCOV
1401
                })
×
UNCOV
1402
                sweepTx.AddTxOut(o.RequiredTxOut())
×
UNCOV
1403

×
UNCOV
1404
                outpointToTxIndex[o.OutPoint()] = len(sweepTx.TxOut) - 1
×
1405
        }
1406

1407
        // Sum up the value contained in the remaining inputs, and add them to
1408
        // the sweep transaction.
1409
        for _, o := range inputs {
44✔
1410
                if o.RequiredTxOut() != nil {
22✔
UNCOV
1411
                        continue
×
1412
                }
1413

1414
                idxs = append(idxs, o)
22✔
1415
                sweepTx.AddTxIn(&wire.TxIn{
22✔
1416
                        PreviousOutPoint: o.OutPoint(),
22✔
1417
                        Sequence:         o.BlocksToMaturity(),
22✔
1418
                })
22✔
1419
        }
1420

1421
        // If we have change outputs to add, then add it the sweep transaction
1422
        // here.
1423
        changeOutputsOpt.WhenSome(func(changeOuts []SweepOutput) {
44✔
1424
                for i := range changeOuts {
66✔
1425
                        sweepTx.AddTxOut(&changeOuts[i].TxOut)
44✔
1426
                }
44✔
1427
        })
1428

1429
        // We'll default to using the current block height as locktime, if none
1430
        // of the inputs commits to a different locktime.
1431
        sweepTx.LockTime = uint32(locktimeOpt.UnwrapOr(t.currentHeight.Load()))
22✔
1432

22✔
1433
        prevInputFetcher, err := input.MultiPrevOutFetcher(inputs)
22✔
1434
        if err != nil {
22✔
1435
                return nil, fmt.Errorf("error creating prev input fetcher "+
×
1436
                        "for hash cache: %v", err)
×
1437
        }
×
1438
        hashCache := txscript.NewTxSigHashes(sweepTx, prevInputFetcher)
22✔
1439

22✔
1440
        // With all the inputs in place, use each output's unique input script
22✔
1441
        // function to generate the final witness required for spending.
22✔
1442
        addInputScript := func(idx int, tso input.Input) error {
44✔
1443
                inputScript, err := tso.CraftInputScript(
22✔
1444
                        t.cfg.Signer, sweepTx, hashCache, prevInputFetcher, idx,
22✔
1445
                )
22✔
1446
                if err != nil {
22✔
1447
                        return err
×
1448
                }
×
1449

1450
                sweepTx.TxIn[idx].Witness = inputScript.Witness
22✔
1451

22✔
1452
                if len(inputScript.SigScript) == 0 {
44✔
1453
                        return nil
22✔
1454
                }
22✔
1455

1456
                sweepTx.TxIn[idx].SignatureScript = inputScript.SigScript
×
1457

×
1458
                return nil
×
1459
        }
1460

1461
        for idx, inp := range idxs {
44✔
1462
                if err := addInputScript(idx, inp); err != nil {
22✔
1463
                        return nil, err
×
1464
                }
×
1465
        }
1466

1467
        log.Debugf("Created sweep tx %v for inputs:\n%v", sweepTx.TxHash(),
22✔
1468
                inputTypeSummary(inputs))
22✔
1469

22✔
1470
        // Try to locate the extra change output, though there might be None.
22✔
1471
        extraTxOut := fn.MapOption(
22✔
1472
                func(sweepOuts []SweepOutput) fn.Option[SweepOutput] {
44✔
1473
                        for _, sweepOut := range sweepOuts {
66✔
1474
                                if !sweepOut.IsExtra {
88✔
1475
                                        continue
44✔
1476
                                }
1477

1478
                                // If we sweep outputs of a custom channel, the
1479
                                // custom leaves in those outputs will be merged
1480
                                // into a single output, even if we sweep
1481
                                // multiple outputs (e.g. to_remote and breached
1482
                                // to_local of a breached channel) at the same
1483
                                // time. So there will only ever be one extra
1484
                                // output.
1485
                                log.Debugf("Sweep produced extra_sweep_out=%v",
×
1486
                                        lnutils.SpewLogClosure(sweepOut))
×
1487

×
1488
                                return fn.Some(sweepOut)
×
1489
                        }
1490

1491
                        return fn.None[SweepOutput]()
22✔
1492
                },
1493
        )(changeOutputsOpt)
1494

1495
        return &sweepTxCtx{
22✔
1496
                tx:                sweepTx,
22✔
1497
                fee:               txFee,
22✔
1498
                extraTxOut:        fn.FlattenOption(extraTxOut),
22✔
1499
                outpointToTxIndex: outpointToTxIndex,
22✔
1500
        }, nil
22✔
1501
}
1502

1503
// prepareSweepTx returns the tx fee, a set of optional change outputs and an
1504
// optional locktime after a series of validations:
1505
// 1. check the locktime has been reached.
1506
// 2. check the locktimes are the same.
1507
// 3. check the inputs cover the outputs.
1508
//
1509
// NOTE: if the change amount is below dust, it will be added to the tx fee.
1510
func prepareSweepTx(inputs []input.Input, changePkScript lnwallet.AddrWithKey,
1511
        feeRate chainfee.SatPerKWeight, currentHeight int32,
1512
        auxSweeper fn.Option[AuxSweeper]) (
1513
        btcutil.Amount, fn.Option[[]SweepOutput], fn.Option[int32], error) {
22✔
1514

22✔
1515
        noChange := fn.None[[]SweepOutput]()
22✔
1516
        noLocktime := fn.None[int32]()
22✔
1517

22✔
1518
        // Given the set of inputs we have, if we have an aux sweeper, then
22✔
1519
        // we'll attempt to see if we have any other change outputs we'll need
22✔
1520
        // to add to the sweep transaction.
22✔
1521
        changePkScripts := [][]byte{changePkScript.DeliveryAddress}
22✔
1522

22✔
1523
        var extraChangeOut fn.Option[SweepOutput]
22✔
1524
        err := fn.MapOptionZ(
22✔
1525
                auxSweeper, func(aux AuxSweeper) error {
44✔
1526
                        extraOut := aux.DeriveSweepAddr(inputs, changePkScript)
22✔
1527
                        if err := extraOut.Err(); err != nil {
22✔
1528
                                return err
×
1529
                        }
×
1530

1531
                        extraChangeOut = extraOut.LeftToSome()
22✔
1532

22✔
1533
                        return nil
22✔
1534
                },
1535
        )
1536
        if err != nil {
22✔
1537
                return 0, noChange, noLocktime, err
×
1538
        }
×
1539

1540
        // Creating a weight estimator with nil outputs and zero max fee rate.
1541
        // We don't allow adding customized outputs in the sweeping tx, and the
1542
        // fee rate is already being managed before we get here.
1543
        inputs, estimator, err := getWeightEstimate(
22✔
1544
                inputs, nil, feeRate, 0, changePkScripts,
22✔
1545
        )
22✔
1546
        if err != nil {
22✔
1547
                return 0, noChange, noLocktime, err
×
1548
        }
×
1549

1550
        txFee := estimator.fee()
22✔
1551

22✔
1552
        var (
22✔
1553
                // Track whether any of the inputs require a certain locktime.
22✔
1554
                locktime = int32(-1)
22✔
1555

22✔
1556
                // We keep track of total input amount, and required output
22✔
1557
                // amount to use for calculating the change amount below.
22✔
1558
                totalInput     btcutil.Amount
22✔
1559
                requiredOutput btcutil.Amount
22✔
1560
        )
22✔
1561

22✔
1562
        // If we have an extra change output, then we'll add it as a required
22✔
1563
        // output amt.
22✔
1564
        extraChangeOut.WhenSome(func(o SweepOutput) {
44✔
1565
                requiredOutput += btcutil.Amount(o.Value)
22✔
1566
        })
22✔
1567

1568
        // Go through each input and check if the required lock times have
1569
        // reached and are the same.
1570
        for _, o := range inputs {
44✔
1571
                // If the input has a required output, we'll add it to the
22✔
1572
                // required output amount.
22✔
1573
                if o.RequiredTxOut() != nil {
22✔
UNCOV
1574
                        requiredOutput += btcutil.Amount(
×
UNCOV
1575
                                o.RequiredTxOut().Value,
×
UNCOV
1576
                        )
×
UNCOV
1577
                }
×
1578

1579
                // Update the total input amount.
1580
                totalInput += btcutil.Amount(o.SignDesc().Output.Value)
22✔
1581

22✔
1582
                lt, ok := o.RequiredLockTime()
22✔
1583

22✔
1584
                // Skip if the input doesn't require a lock time.
22✔
1585
                if !ok {
44✔
1586
                        continue
22✔
1587
                }
1588

1589
                // Check if the lock time has reached
UNCOV
1590
                if lt > uint32(currentHeight) {
×
1591
                        return 0, noChange, noLocktime,
×
1592
                                fmt.Errorf("%w: current height is %v, "+
×
1593
                                        "locktime is %v", ErrLocktimeImmature,
×
1594
                                        currentHeight, lt)
×
1595
                }
×
1596

1597
                // If another input commits to a different locktime, they
1598
                // cannot be combined in the same transaction.
UNCOV
1599
                if locktime != -1 && locktime != int32(lt) {
×
1600
                        return 0, noChange, noLocktime, ErrLocktimeConflict
×
1601
                }
×
1602

1603
                // Update the locktime for next iteration.
UNCOV
1604
                locktime = int32(lt)
×
1605
        }
1606

1607
        // Make sure total output amount is less than total input amount.
1608
        if requiredOutput+txFee > totalInput {
22✔
1609
                return 0, noChange, noLocktime, fmt.Errorf("insufficient "+
×
1610
                        "input to create sweep tx: input_sum=%v, "+
×
1611
                        "output_sum=%v", totalInput, requiredOutput+txFee)
×
1612
        }
×
1613

1614
        // The value remaining after the required output and fees is the
1615
        // change output.
1616
        changeAmt := totalInput - requiredOutput - txFee
22✔
1617
        changeOuts := make([]SweepOutput, 0, 2)
22✔
1618

22✔
1619
        extraChangeOut.WhenSome(func(o SweepOutput) {
44✔
1620
                changeOuts = append(changeOuts, o)
22✔
1621
        })
22✔
1622

1623
        // We'll calculate the dust limit for the given changePkScript since it
1624
        // is variable.
1625
        changeFloor := lnwallet.DustLimitForSize(
22✔
1626
                len(changePkScript.DeliveryAddress),
22✔
1627
        )
22✔
1628

22✔
1629
        switch {
22✔
1630
        // If the change amount is dust, we'll move it into the fees, and
1631
        // ignore it.
UNCOV
1632
        case changeAmt < changeFloor:
×
UNCOV
1633
                log.Infof("Change amt %v below dustlimit %v, not adding "+
×
UNCOV
1634
                        "change output", changeAmt, changeFloor)
×
UNCOV
1635

×
UNCOV
1636
                // If there's no required output, and the change output is a
×
UNCOV
1637
                // dust, it means we are creating a tx without any outputs. In
×
UNCOV
1638
                // this case we'll return an error. This could happen when
×
UNCOV
1639
                // creating a tx that has an anchor as the only input.
×
UNCOV
1640
                if requiredOutput == 0 {
×
UNCOV
1641
                        return 0, noChange, noLocktime, ErrTxNoOutput
×
UNCOV
1642
                }
×
1643

1644
                // The dust amount is added to the fee.
1645
                txFee += changeAmt
×
1646

1647
        // Otherwise, we'll actually recognize it as a change output.
1648
        default:
22✔
1649
                changeOuts = append(changeOuts, SweepOutput{
22✔
1650
                        TxOut: wire.TxOut{
22✔
1651
                                Value:    int64(changeAmt),
22✔
1652
                                PkScript: changePkScript.DeliveryAddress,
22✔
1653
                        },
22✔
1654
                        IsExtra:     false,
22✔
1655
                        InternalKey: changePkScript.InternalKey,
22✔
1656
                })
22✔
1657
        }
1658

1659
        // Optionally set the locktime.
1660
        locktimeOpt := fn.Some(locktime)
22✔
1661
        if locktime == -1 {
44✔
1662
                locktimeOpt = noLocktime
22✔
1663
        }
22✔
1664

1665
        var changeOutsOpt fn.Option[[]SweepOutput]
22✔
1666
        if len(changeOuts) > 0 {
44✔
1667
                changeOutsOpt = fn.Some(changeOuts)
22✔
1668
        }
22✔
1669

1670
        log.Debugf("Creating sweep tx for %v inputs (%s) using %v, "+
22✔
1671
                "tx_weight=%v, tx_fee=%v, locktime=%v, parents_count=%v, "+
22✔
1672
                "parents_fee=%v, parents_weight=%v, current_height=%v",
22✔
1673
                len(inputs), inputTypeSummary(inputs), feeRate,
22✔
1674
                estimator.weight(), txFee, locktimeOpt, len(estimator.parents),
22✔
1675
                estimator.parentsFee, estimator.parentsWeight, currentHeight)
22✔
1676

22✔
1677
        return txFee, changeOutsOpt, locktimeOpt, nil
22✔
1678
}
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