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

lightningnetwork / lnd / 13236757158

10 Feb 2025 08:39AM UTC coverage: 57.649% (-1.2%) from 58.815%
13236757158

Pull #9493

github

ziggie1984
lncli: for some cmds we don't replace the data of the response.

For some cmds it is not very practical to replace the json output
because we might pipe it into other commands. For example when
creating the route we want to pipe it into sendtoRoute.
Pull Request #9493: For some lncli cmds we should not replace the content with other data

0 of 9 new or added lines in 2 files covered. (0.0%)

19535 existing lines in 252 files now uncovered.

103517 of 179563 relevant lines covered (57.65%)

24878.49 hits per line

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

73.83
/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
        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(record.requestID, subscriber)
5✔
418

5✔
419
        // Publish the tx immediately if specified.
5✔
420
        if req.Immediate {
6✔
421
                t.handleInitialBroadcast(record)
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) *monitorRecord {
5✔
429
        // Increase the request counter.
5✔
430
        //
5✔
431
        // NOTE: this is the only place where we increase the counter.
5✔
432
        requestID := t.requestCounter.Add(1)
5✔
433

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

5✔
441
        return record
5✔
442
}
5✔
443

444
// updateRecord updates the given record's tx and fee, and saves it in the
445
// records map.
446
func (t *TxPublisher) updateRecord(r *monitorRecord,
447
        sweepCtx *sweepTxCtx) *monitorRecord {
18✔
448

18✔
449
        r.tx = sweepCtx.tx
18✔
450
        r.fee = sweepCtx.fee
18✔
451
        r.outpointToTxIndex = sweepCtx.outpointToTxIndex
18✔
452

18✔
453
        // Register the record.
18✔
454
        t.records.Store(r.requestID, r)
18✔
455

18✔
456
        return r
18✔
457
}
18✔
458

459
// NOTE: part of the `chainio.Consumer` interface.
460
func (t *TxPublisher) Name() string {
16✔
461
        return "TxPublisher"
16✔
462
}
16✔
463

464
// initializeTx initializes a fee function and creates an RBF-compliant tx. If
465
// succeeded, the initial tx is stored in the records map.
466
func (t *TxPublisher) initializeTx(r *monitorRecord) (*monitorRecord, error) {
4✔
467
        // Create a fee bumping algorithm to be used for future RBF.
4✔
468
        feeAlgo, err := t.initializeFeeFunction(r.req)
4✔
469
        if err != nil {
5✔
470
                return nil, fmt.Errorf("init fee function: %w", err)
1✔
471
        }
1✔
472

473
        // Attach the newly created fee function.
474
        //
475
        // TODO(yy): current we'd initialize a monitorRecord before creating the
476
        // fee function, while we could instead create the fee function first
477
        // then save it to the record. To make this happen we need to change the
478
        // conf target calculation below since we would be initializing the fee
479
        // function one block before.
480
        r.feeFunction = feeAlgo
3✔
481

3✔
482
        // Create the initial tx to be broadcasted. This tx is guaranteed to
3✔
483
        // comply with the RBF restrictions.
3✔
484
        record, err := t.createRBFCompliantTx(r)
3✔
485
        if err != nil {
4✔
486
                return nil, fmt.Errorf("create RBF-compliant tx: %w", err)
1✔
487
        }
1✔
488

489
        return record, nil
2✔
490
}
491

492
// initializeFeeFunction initializes a fee function to be used for this request
493
// for future fee bumping.
494
func (t *TxPublisher) initializeFeeFunction(
495
        req *BumpRequest) (FeeFunction, error) {
6✔
496

6✔
497
        // Get the max allowed feerate.
6✔
498
        maxFeeRateAllowed, err := req.MaxFeeRateAllowed()
6✔
499
        if err != nil {
6✔
500
                return nil, err
×
501
        }
×
502

503
        // Get the initial conf target.
504
        confTarget := calcCurrentConfTarget(
6✔
505
                t.currentHeight.Load(), req.DeadlineHeight,
6✔
506
        )
6✔
507

6✔
508
        log.Debugf("Initializing fee function with conf target=%v, budget=%v, "+
6✔
509
                "maxFeeRateAllowed=%v", confTarget, req.Budget,
6✔
510
                maxFeeRateAllowed)
6✔
511

6✔
512
        // Initialize the fee function and return it.
6✔
513
        //
6✔
514
        // TODO(yy): return based on differet req.Strategy?
6✔
515
        return NewLinearFeeFunction(
6✔
516
                maxFeeRateAllowed, confTarget, t.cfg.Estimator,
6✔
517
                req.StartingFeeRate,
6✔
518
        )
6✔
519
}
520

521
// createRBFCompliantTx creates a tx that is compliant with RBF rules. It does
522
// so by creating a tx, validate it using `TestMempoolAccept`, and bump its fee
523
// and redo the process until the tx is valid, or return an error when non-RBF
524
// related errors occur or the budget has been used up.
525
func (t *TxPublisher) createRBFCompliantTx(
526
        r *monitorRecord) (*monitorRecord, error) {
9✔
527

9✔
528
        f := r.feeFunction
9✔
529

9✔
530
        for {
21✔
531
                // Create a new tx with the given fee rate and check its
12✔
532
                // mempool acceptance.
12✔
533
                sweepCtx, err := t.createAndCheckTx(r.req, f)
12✔
534

12✔
535
                switch {
12✔
536
                case err == nil:
6✔
537
                        // The tx is valid, store it.
6✔
538
                        record := t.updateRecord(r, sweepCtx)
6✔
539

6✔
540
                        log.Infof("Created initial sweep tx=%v for %v inputs: "+
6✔
541
                                "feerate=%v, fee=%v, inputs:\n%v",
6✔
542
                                sweepCtx.tx.TxHash(), len(r.req.Inputs),
6✔
543
                                f.FeeRate(), sweepCtx.fee,
6✔
544
                                inputTypeSummary(r.req.Inputs))
6✔
545

6✔
546
                        return record, nil
6✔
547

548
                // If the error indicates the fees paid is not enough, we will
549
                // ask the fee function to increase the fee rate and retry.
550
                case errors.Is(err, lnwallet.ErrMempoolFee):
2✔
551
                        // We should at least start with a feerate above the
2✔
552
                        // mempool min feerate, so if we get this error, it
2✔
553
                        // means something is wrong earlier in the pipeline.
2✔
554
                        log.Errorf("Current fee=%v, feerate=%v, %v",
2✔
555
                                sweepCtx.fee, f.FeeRate(), err)
2✔
556

2✔
557
                        fallthrough
2✔
558

559
                // We are not paying enough fees so we increase it.
560
                case errors.Is(err, chain.ErrInsufficientFee):
4✔
561
                        increased := false
4✔
562

4✔
563
                        // Keep calling the fee function until the fee rate is
4✔
564
                        // increased or maxed out.
4✔
565
                        for !increased {
9✔
566
                                log.Debugf("Increasing fee for next round, "+
5✔
567
                                        "current fee=%v, feerate=%v",
5✔
568
                                        sweepCtx.fee, f.FeeRate())
5✔
569

5✔
570
                                // If the fee function tells us that we have
5✔
571
                                // used up the budget, we will return an error
5✔
572
                                // indicating this tx cannot be made. The
5✔
573
                                // sweeper should handle this error and try to
5✔
574
                                // cluster these inputs differetly.
5✔
575
                                increased, err = f.Increment()
5✔
576
                                if err != nil {
6✔
577
                                        return nil, err
1✔
578
                                }
1✔
579
                        }
580

581
                // TODO(yy): suppose there's only one bad input, we can do a
582
                // binary search to find out which input is causing this error
583
                // by recreating a tx using half of the inputs and check its
584
                // mempool acceptance.
585
                default:
2✔
586
                        log.Debugf("Failed to create RBF-compliant tx: %v", err)
2✔
587
                        return nil, err
2✔
588
                }
589
        }
590
}
591

592
// createAndCheckTx creates a tx based on the given inputs, change output
593
// script, and the fee rate. In addition, it validates the tx's mempool
594
// acceptance before returning a tx that can be published directly, along with
595
// its fee.
596
func (t *TxPublisher) createAndCheckTx(req *BumpRequest,
597
        f FeeFunction) (*sweepTxCtx, error) {
22✔
598

22✔
599
        // Create the sweep tx with max fee rate of 0 as the fee function
22✔
600
        // guarantees the fee rate used here won't exceed the max fee rate.
22✔
601
        sweepCtx, err := t.createSweepTx(
22✔
602
                req.Inputs, req.DeliveryAddress, f.FeeRate(),
22✔
603
        )
22✔
604
        if err != nil {
22✔
UNCOV
605
                return sweepCtx, fmt.Errorf("create sweep tx: %w", err)
×
UNCOV
606
        }
×
607

608
        // Sanity check the budget still covers the fee.
609
        if sweepCtx.fee > req.Budget {
24✔
610
                return sweepCtx, fmt.Errorf("%w: budget=%v, fee=%v",
2✔
611
                        ErrNotEnoughBudget, req.Budget, sweepCtx.fee)
2✔
612
        }
2✔
613

614
        // If we had an extra txOut, then we'll update the result to include
615
        // it.
616
        req.ExtraTxOut = sweepCtx.extraTxOut
20✔
617

20✔
618
        // Validate the tx's mempool acceptance.
20✔
619
        err = t.cfg.Wallet.CheckMempoolAcceptance(sweepCtx.tx)
20✔
620

20✔
621
        // Exit early if the tx is valid.
20✔
622
        if err == nil {
31✔
623
                return sweepCtx, nil
11✔
624
        }
11✔
625

626
        // Print an error log if the chain backend doesn't support the mempool
627
        // acceptance test RPC.
628
        if errors.Is(err, rpcclient.ErrBackendVersion) {
9✔
629
                log.Errorf("TestMempoolAccept not supported by backend, " +
×
630
                        "consider upgrading it to a newer version")
×
631
                return sweepCtx, nil
×
632
        }
×
633

634
        // We are running on a backend that doesn't implement the RPC
635
        // testmempoolaccept, eg, neutrino, so we'll skip the check.
636
        if errors.Is(err, chain.ErrUnimplemented) {
9✔
UNCOV
637
                log.Debug("Skipped testmempoolaccept due to not implemented")
×
UNCOV
638
                return sweepCtx, nil
×
UNCOV
639
        }
×
640

641
        return sweepCtx, fmt.Errorf("tx=%v failed mempool check: %w",
9✔
642
                sweepCtx.tx.TxHash(), err)
9✔
643
}
644

645
// broadcast takes a monitored tx and publishes it to the network. Prior to the
646
// broadcast, it will subscribe the tx's confirmation notification and attach
647
// the event channel to the record. Any broadcast-related errors will not be
648
// returned here, instead, they will be put inside the `BumpResult` and
649
// returned to the caller.
650
func (t *TxPublisher) broadcast(record *monitorRecord) (*BumpResult, error) {
8✔
651
        txid := record.tx.TxHash()
8✔
652

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

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

668
        // Set the event, and change it to TxFailed if the wallet fails to
669
        // publish it.
670
        event := TxPublished
8✔
671

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

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

8✔
700
        return result, nil
8✔
701
}
702

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

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

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

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

11✔
731
        var txid chainhash.Hash
11✔
732
        if result.Tx != nil {
20✔
733
                txid = result.Tx.TxHash()
9✔
734
        }
9✔
735

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

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

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

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

4✔
762
                return
4✔
763
        }
764

765
        t.records.Delete(id)
7✔
766
        t.subscriberChans.Delete(id)
7✔
767
}
768

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

8✔
776
        // Remove the record if it's failed or confirmed.
8✔
777
        t.removeResult(result)
8✔
778
}
8✔
779

780
// monitorRecord is used to keep track of the tx being monitored by the
781
// publisher internally.
782
type monitorRecord struct {
783
        // requestID is the ID of the request that created this record.
784
        requestID uint64
785

786
        // tx is the tx being monitored.
787
        tx *wire.MsgTx
788

789
        // req is the original request.
790
        req *BumpRequest
791

792
        // feeFunction is the fee bumping algorithm used by the publisher.
793
        feeFunction FeeFunction
794

795
        // fee is the fee paid by the tx.
796
        fee btcutil.Amount
797

798
        // outpointToTxIndex is a map of outpoint to tx index.
799
        outpointToTxIndex map[wire.OutPoint]int
800
}
801

802
// Start starts the publisher by subscribing to block epoch updates and kicking
803
// off the monitor loop.
UNCOV
804
func (t *TxPublisher) Start(beat chainio.Blockbeat) error {
×
UNCOV
805
        log.Info("TxPublisher starting...")
×
UNCOV
806

×
UNCOV
807
        if t.started.Swap(true) {
×
808
                return fmt.Errorf("TxPublisher started more than once")
×
809
        }
×
810

811
        // Set the current height.
UNCOV
812
        t.currentHeight.Store(beat.Height())
×
UNCOV
813

×
UNCOV
814
        t.wg.Add(1)
×
UNCOV
815
        go t.monitor()
×
UNCOV
816

×
UNCOV
817
        log.Debugf("TxPublisher started")
×
UNCOV
818

×
UNCOV
819
        return nil
×
820
}
821

822
// Stop stops the publisher and waits for the monitor loop to exit.
UNCOV
823
func (t *TxPublisher) Stop() error {
×
UNCOV
824
        log.Info("TxPublisher stopping...")
×
UNCOV
825

×
UNCOV
826
        if t.stopped.Swap(true) {
×
827
                return fmt.Errorf("TxPublisher stopped more than once")
×
828
        }
×
829

UNCOV
830
        close(t.quit)
×
UNCOV
831
        t.wg.Wait()
×
UNCOV
832

×
UNCOV
833
        log.Debug("TxPublisher stopped")
×
UNCOV
834

×
UNCOV
835
        return nil
×
836
}
837

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

×
UNCOV
846
        for {
×
UNCOV
847
                select {
×
UNCOV
848
                case beat := <-t.BlockbeatChan:
×
UNCOV
849
                        height := beat.Height()
×
UNCOV
850
                        log.Debugf("TxPublisher received new block: %v", height)
×
UNCOV
851

×
UNCOV
852
                        // Update the best known height for the publisher.
×
UNCOV
853
                        t.currentHeight.Store(height)
×
UNCOV
854

×
UNCOV
855
                        // Check all monitored txns to see if any of them needs
×
UNCOV
856
                        // to be bumped.
×
UNCOV
857
                        t.processRecords()
×
UNCOV
858

×
UNCOV
859
                        // Notify we've processed the block.
×
UNCOV
860
                        t.NotifyBlockProcessed(beat, nil)
×
861

UNCOV
862
                case <-t.quit:
×
UNCOV
863
                        log.Debug("Fee bumper stopped, exit monitor")
×
UNCOV
864
                        return
×
865
                }
866
        }
867
}
868

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

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

1✔
879
        // failedRecords stores a map of records which has inputs being spent
1✔
880
        // by a third party.
1✔
881
        //
1✔
882
        // NOTE: this is only used for neutrino backend.
1✔
883
        failedRecords := make(map[uint64]*monitorRecord)
1✔
884

1✔
885
        // initialRecords stores a map of records which are being created and
1✔
886
        // published for the first time.
1✔
887
        initialRecords := make(map[uint64]*monitorRecord)
1✔
888

1✔
889
        // visitor is a helper closure that visits each record and divides them
1✔
890
        // into two groups.
1✔
891
        visitor := func(requestID uint64, r *monitorRecord) error {
3✔
892
                if r.tx == nil {
2✔
UNCOV
893
                        initialRecords[requestID] = r
×
UNCOV
894
                        return nil
×
UNCOV
895
                }
×
896

897
                log.Tracef("Checking monitor recordID=%v for tx=%v", requestID,
2✔
898
                        r.tx.TxHash())
2✔
899

2✔
900
                // If the tx is already confirmed, we can stop monitoring it.
2✔
901
                if t.isConfirmed(r.tx.TxHash()) {
3✔
902
                        confirmedRecords[requestID] = r
1✔
903

1✔
904
                        // Move to the next record.
1✔
905
                        return nil
1✔
906
                }
1✔
907

908
                // Check whether the inputs has been spent by a third party.
909
                //
910
                // NOTE: this check is only done for neutrino backend.
911
                if t.isThirdPartySpent(r.tx.TxHash(), r.req.Inputs) {
1✔
UNCOV
912
                        failedRecords[requestID] = r
×
UNCOV
913

×
UNCOV
914
                        // Move to the next record.
×
UNCOV
915
                        return nil
×
UNCOV
916
                }
×
917

918
                feeBumpRecords[requestID] = r
1✔
919

1✔
920
                // Return nil to move to the next record.
1✔
921
                return nil
1✔
922
        }
923

924
        // Iterate through all the records and divide them into four groups.
925
        t.records.ForEach(visitor)
1✔
926

1✔
927
        // Handle the initial broadcast.
1✔
928
        for _, r := range initialRecords {
1✔
UNCOV
929
                t.handleInitialBroadcast(r)
×
UNCOV
930
        }
×
931

932
        // For records that are confirmed, we'll notify the caller about this
933
        // result.
934
        for _, r := range confirmedRecords {
2✔
935
                log.Debugf("Tx=%v is confirmed", r.tx.TxHash())
1✔
936
                t.wg.Add(1)
1✔
937
                go t.handleTxConfirmed(r)
1✔
938
        }
1✔
939

940
        // Get the current height to be used in the following goroutines.
941
        currentHeight := t.currentHeight.Load()
1✔
942

1✔
943
        // For records that are not confirmed, we perform a fee bump if needed.
1✔
944
        for _, r := range feeBumpRecords {
2✔
945
                log.Debugf("Attempting to fee bump Tx=%v", r.tx.TxHash())
1✔
946
                t.wg.Add(1)
1✔
947
                go t.handleFeeBumpTx(r, currentHeight)
1✔
948
        }
1✔
949

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

960
// handleTxConfirmed is called when a monitored tx is confirmed. It will
961
// notify the subscriber then remove the record from the maps .
962
//
963
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
964
func (t *TxPublisher) handleTxConfirmed(r *monitorRecord) {
2✔
965
        defer t.wg.Done()
2✔
966

2✔
967
        // Create a result that will be sent to the resultChan which is
2✔
968
        // listened by the caller.
2✔
969
        result := &BumpResult{
2✔
970
                Event:     TxConfirmed,
2✔
971
                Tx:        r.tx,
2✔
972
                requestID: r.requestID,
2✔
973
                Fee:       r.fee,
2✔
974
                FeeRate:   r.feeFunction.FeeRate(),
2✔
975
        }
2✔
976

2✔
977
        // Notify that this tx is confirmed and remove the record from the map.
2✔
978
        t.handleResult(result)
2✔
979
}
2✔
980

981
// handleInitialTxError takes the error from `initializeTx` and decides the
982
// bump event. It will construct a BumpResult and handles it.
983
func (t *TxPublisher) handleInitialTxError(requestID uint64, err error) {
2✔
984
        // We now decide what type of event to send.
2✔
985
        var event BumpEvent
2✔
986

2✔
987
        switch {
2✔
988
        // When the error is due to a dust output, we'll send a TxFailed so
989
        // these inputs can be retried with a different group in the next
990
        // block.
UNCOV
991
        case errors.Is(err, ErrTxNoOutput):
×
UNCOV
992
                event = TxFailed
×
993

994
        // When the error is due to budget being used up, we'll send a TxFailed
995
        // so these inputs can be retried with a different group in the next
996
        // block.
UNCOV
997
        case errors.Is(err, ErrMaxPosition):
×
UNCOV
998
                event = TxFailed
×
999

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

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

1016
        result := &BumpResult{
2✔
1017
                Event:     event,
2✔
1018
                Err:       err,
2✔
1019
                requestID: requestID,
2✔
1020
        }
2✔
1021

2✔
1022
        t.handleResult(result)
2✔
1023
}
1024

1025
// handleInitialBroadcast is called when a new request is received. It will
1026
// handle the initial tx creation and broadcast. In details,
1027
// 1. init a fee function based on the given strategy.
1028
// 2. create an RBF-compliant tx and monitor it for confirmation.
1029
// 3. notify the initial broadcast result back to the caller.
1030
func (t *TxPublisher) handleInitialBroadcast(r *monitorRecord) {
4✔
1031
        log.Debugf("Initial broadcast for requestID=%v", r.requestID)
4✔
1032

4✔
1033
        var (
4✔
1034
                result *BumpResult
4✔
1035
                err    error
4✔
1036
        )
4✔
1037

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

2✔
1046
                // We now handle the initialization error and exit.
2✔
1047
                t.handleInitialTxError(r.requestID, err)
2✔
1048

2✔
1049
                return
2✔
1050
        }
2✔
1051

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

1066
        t.handleResult(result)
2✔
1067
}
1068

1069
// handleFeeBumpTx checks if the tx needs to be bumped, and if so, it will
1070
// attempt to bump the fee of the tx.
1071
//
1072
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
1073
func (t *TxPublisher) handleFeeBumpTx(r *monitorRecord, currentHeight int32) {
4✔
1074
        defer t.wg.Done()
4✔
1075

4✔
1076
        oldTxid := r.tx.TxHash()
4✔
1077

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

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

1✔
1091
                return
1✔
1092
        }
1✔
1093

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

1✔
1099
                return
1✔
1100
        }
1✔
1101

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

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

1113
// handleThirdPartySpent is called when the inputs in an unconfirmed tx is
1114
// spent. It will notify the subscriber then remove the record from the maps
1115
// and send a TxFailed event to the subscriber.
1116
//
1117
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
UNCOV
1118
func (t *TxPublisher) handleThirdPartySpent(r *monitorRecord) {
×
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: r.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(
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: r.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
        record := t.updateRecord(r, sweepCtx)
4✔
1199

4✔
1200
        // Attempt to broadcast this new tx.
4✔
1201
        result, err := t.broadcast(record)
4✔
1202
        if err != nil {
4✔
1203
                log.Infof("Failed to broadcast replacement tx %v: %v",
×
1204
                        sweepCtx.tx.TxHash(), err)
×
1205

×
1206
                return fn.None[BumpResult]()
×
1207
        }
×
1208

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

×
UNCOV
1217
                log.Debugf("Failed to bump tx %v: %v", oldTx.TxHash(), err)
×
UNCOV
1218
                return fn.None[BumpResult]()
×
UNCOV
1219
        }
×
1220

1221
        // A successful replacement tx is created, attach the old tx.
1222
        result.ReplacedTx = oldTx
4✔
1223

4✔
1224
        // If the new tx failed to be published, we will return the result so
4✔
1225
        // the caller can handle it.
4✔
1226
        if result.Event == TxFailed {
5✔
1227
                return fn.Some(*result)
1✔
1228
        }
1✔
1229

1230
        log.Infof("Replaced tx=%v with new tx=%v", oldTx.TxHash(),
3✔
1231
                sweepCtx.tx.TxHash())
3✔
1232

3✔
1233
        // Otherwise, it's a successful RBF, set the event and return.
3✔
1234
        result.Event = TxReplaced
3✔
1235

3✔
1236
        return fn.Some(*result)
3✔
1237
}
1238

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

1247
        return details.NumConfirmations > 0
2✔
1248
}
1249

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

1✔
1259
        // Skip this check for if this is not neutrino backend.
1✔
1260
        if !t.isNeutrinoBackend() {
2✔
1261
                return false
1✔
1262
        }
1✔
1263

1264
        // Iterate all the inputs and check if they have been spent already.
UNCOV
1265
        for _, inp := range inputs {
×
UNCOV
1266
                op := inp.OutPoint()
×
UNCOV
1267

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

×
UNCOV
1275
                        continue
×
1276
                }
1277

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

1289
                // Remove the subscription when exit.
UNCOV
1290
                defer spendEvent.Cancel()
×
UNCOV
1291

×
UNCOV
1292
                // Do a non-blocking read to see if the output has been spent.
×
UNCOV
1293
                select {
×
UNCOV
1294
                case spend, ok := <-spendEvent.Spend:
×
UNCOV
1295
                        if !ok {
×
1296
                                log.Debugf("Spend ntfn for %v canceled", op)
×
1297
                                return false
×
1298
                        }
×
1299

UNCOV
1300
                        spendingTxID := spend.SpendingTx.TxHash()
×
UNCOV
1301

×
UNCOV
1302
                        // If the spending tx is the same as the sweeping tx
×
UNCOV
1303
                        // then we are good.
×
UNCOV
1304
                        if spendingTxID == txid {
×
UNCOV
1305
                                continue
×
1306
                        }
1307

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

×
UNCOV
1311
                        return true
×
1312

1313
                // Move to the next input.
UNCOV
1314
                default:
×
1315
                }
1316
        }
1317

UNCOV
1318
        return false
×
1319
}
1320

1321
// calcCurrentConfTarget calculates the current confirmation target based on
1322
// the deadline height. The conf target is capped at 0 if the deadline has
1323
// already been past.
1324
func calcCurrentConfTarget(currentHeight, deadline int32) uint32 {
12✔
1325
        var confTarget uint32
12✔
1326

12✔
1327
        // Calculate how many blocks left until the deadline.
12✔
1328
        deadlineDelta := deadline - currentHeight
12✔
1329

12✔
1330
        // If we are already past the deadline, we will set the conf target to
12✔
1331
        // be 1.
12✔
1332
        if deadlineDelta < 0 {
16✔
1333
                log.Warnf("Deadline is %d blocks behind current height %v",
4✔
1334
                        -deadlineDelta, currentHeight)
4✔
1335

4✔
1336
                confTarget = 0
4✔
1337
        } else {
12✔
1338
                confTarget = uint32(deadlineDelta)
8✔
1339
        }
8✔
1340

1341
        return confTarget
12✔
1342
}
1343

1344
// sweepTxCtx houses a sweep transaction with additional context.
1345
type sweepTxCtx struct {
1346
        tx *wire.MsgTx
1347

1348
        fee btcutil.Amount
1349

1350
        extraTxOut fn.Option[SweepOutput]
1351

1352
        // outpointToTxIndex maps the outpoint of the inputs to their index in
1353
        // the sweep transaction.
1354
        outpointToTxIndex map[wire.OutPoint]int
1355
}
1356

1357
// createSweepTx creates a sweeping tx based on the given inputs, change
1358
// address and fee rate.
1359
func (t *TxPublisher) createSweepTx(inputs []input.Input,
1360
        changePkScript lnwallet.AddrWithKey,
1361
        feeRate chainfee.SatPerKWeight) (*sweepTxCtx, error) {
22✔
1362

22✔
1363
        // Validate and calculate the fee and change amount.
22✔
1364
        txFee, changeOutputsOpt, locktimeOpt, err := prepareSweepTx(
22✔
1365
                inputs, changePkScript, feeRate, t.currentHeight.Load(),
22✔
1366
                t.cfg.AuxSweeper,
22✔
1367
        )
22✔
1368
        if err != nil {
22✔
UNCOV
1369
                return nil, err
×
UNCOV
1370
        }
×
1371

1372
        var (
22✔
1373
                // Create the sweep transaction that we will be building. We
22✔
1374
                // use version 2 as it is required for CSV.
22✔
1375
                sweepTx = wire.NewMsgTx(2)
22✔
1376

22✔
1377
                // We'll add the inputs as we go so we know the final ordering
22✔
1378
                // of inputs to sign.
22✔
1379
                idxs []input.Input
22✔
1380
        )
22✔
1381

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

UNCOV
1391
                idxs = append(idxs, o)
×
UNCOV
1392
                sweepTx.AddTxIn(&wire.TxIn{
×
UNCOV
1393
                        PreviousOutPoint: o.OutPoint(),
×
UNCOV
1394
                        Sequence:         o.BlocksToMaturity(),
×
UNCOV
1395
                })
×
UNCOV
1396
                sweepTx.AddTxOut(o.RequiredTxOut())
×
UNCOV
1397

×
UNCOV
1398
                outpointToTxIndex[o.OutPoint()] = len(sweepTx.TxOut) - 1
×
1399
        }
1400

1401
        // Sum up the value contained in the remaining inputs, and add them to
1402
        // the sweep transaction.
1403
        for _, o := range inputs {
44✔
1404
                if o.RequiredTxOut() != nil {
22✔
UNCOV
1405
                        continue
×
1406
                }
1407

1408
                idxs = append(idxs, o)
22✔
1409
                sweepTx.AddTxIn(&wire.TxIn{
22✔
1410
                        PreviousOutPoint: o.OutPoint(),
22✔
1411
                        Sequence:         o.BlocksToMaturity(),
22✔
1412
                })
22✔
1413
        }
1414

1415
        // If we have change outputs to add, then add it the sweep transaction
1416
        // here.
1417
        changeOutputsOpt.WhenSome(func(changeOuts []SweepOutput) {
44✔
1418
                for i := range changeOuts {
66✔
1419
                        sweepTx.AddTxOut(&changeOuts[i].TxOut)
44✔
1420
                }
44✔
1421
        })
1422

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

22✔
1427
        prevInputFetcher, err := input.MultiPrevOutFetcher(inputs)
22✔
1428
        if err != nil {
22✔
1429
                return nil, fmt.Errorf("error creating prev input fetcher "+
×
1430
                        "for hash cache: %v", err)
×
1431
        }
×
1432
        hashCache := txscript.NewTxSigHashes(sweepTx, prevInputFetcher)
22✔
1433

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

1444
                sweepTx.TxIn[idx].Witness = inputScript.Witness
22✔
1445

22✔
1446
                if len(inputScript.SigScript) == 0 {
44✔
1447
                        return nil
22✔
1448
                }
22✔
1449

1450
                sweepTx.TxIn[idx].SignatureScript = inputScript.SigScript
×
1451

×
1452
                return nil
×
1453
        }
1454

1455
        for idx, inp := range idxs {
44✔
1456
                if err := addInputScript(idx, inp); err != nil {
22✔
1457
                        return nil, err
×
1458
                }
×
1459
        }
1460

1461
        log.Debugf("Created sweep tx %v for inputs:\n%v", sweepTx.TxHash(),
22✔
1462
                inputTypeSummary(inputs))
22✔
1463

22✔
1464
        // Try to locate the extra change output, though there might be None.
22✔
1465
        extraTxOut := fn.MapOption(
22✔
1466
                func(sweepOuts []SweepOutput) fn.Option[SweepOutput] {
44✔
1467
                        for _, sweepOut := range sweepOuts {
66✔
1468
                                if !sweepOut.IsExtra {
88✔
1469
                                        continue
44✔
1470
                                }
1471

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

×
1482
                                return fn.Some(sweepOut)
×
1483
                        }
1484

1485
                        return fn.None[SweepOutput]()
22✔
1486
                },
1487
        )(changeOutputsOpt)
1488

1489
        return &sweepTxCtx{
22✔
1490
                tx:                sweepTx,
22✔
1491
                fee:               txFee,
22✔
1492
                extraTxOut:        fn.FlattenOption(extraTxOut),
22✔
1493
                outpointToTxIndex: outpointToTxIndex,
22✔
1494
        }, nil
22✔
1495
}
1496

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

22✔
1509
        noChange := fn.None[[]SweepOutput]()
22✔
1510
        noLocktime := fn.None[int32]()
22✔
1511

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

22✔
1517
        var extraChangeOut fn.Option[SweepOutput]
22✔
1518
        err := fn.MapOptionZ(
22✔
1519
                auxSweeper, func(aux AuxSweeper) error {
44✔
1520
                        extraOut := aux.DeriveSweepAddr(inputs, changePkScript)
22✔
1521
                        if err := extraOut.Err(); err != nil {
22✔
1522
                                return err
×
1523
                        }
×
1524

1525
                        extraChangeOut = extraOut.LeftToSome()
22✔
1526

22✔
1527
                        return nil
22✔
1528
                },
1529
        )
1530
        if err != nil {
22✔
1531
                return 0, noChange, noLocktime, err
×
1532
        }
×
1533

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

1544
        txFee := estimator.fee()
22✔
1545

22✔
1546
        var (
22✔
1547
                // Track whether any of the inputs require a certain locktime.
22✔
1548
                locktime = int32(-1)
22✔
1549

22✔
1550
                // We keep track of total input amount, and required output
22✔
1551
                // amount to use for calculating the change amount below.
22✔
1552
                totalInput     btcutil.Amount
22✔
1553
                requiredOutput btcutil.Amount
22✔
1554
        )
22✔
1555

22✔
1556
        // If we have an extra change output, then we'll add it as a required
22✔
1557
        // output amt.
22✔
1558
        extraChangeOut.WhenSome(func(o SweepOutput) {
44✔
1559
                requiredOutput += btcutil.Amount(o.Value)
22✔
1560
        })
22✔
1561

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

1573
                // Update the total input amount.
1574
                totalInput += btcutil.Amount(o.SignDesc().Output.Value)
22✔
1575

22✔
1576
                lt, ok := o.RequiredLockTime()
22✔
1577

22✔
1578
                // Skip if the input doesn't require a lock time.
22✔
1579
                if !ok {
44✔
1580
                        continue
22✔
1581
                }
1582

1583
                // Check if the lock time has reached
UNCOV
1584
                if lt > uint32(currentHeight) {
×
1585
                        return 0, noChange, noLocktime,
×
1586
                                fmt.Errorf("%w: current height is %v, "+
×
1587
                                        "locktime is %v", ErrLocktimeImmature,
×
1588
                                        currentHeight, lt)
×
1589
                }
×
1590

1591
                // If another input commits to a different locktime, they
1592
                // cannot be combined in the same transaction.
UNCOV
1593
                if locktime != -1 && locktime != int32(lt) {
×
1594
                        return 0, noChange, noLocktime, ErrLocktimeConflict
×
1595
                }
×
1596

1597
                // Update the locktime for next iteration.
UNCOV
1598
                locktime = int32(lt)
×
1599
        }
1600

1601
        // Make sure total output amount is less than total input amount.
1602
        if requiredOutput+txFee > totalInput {
22✔
1603
                return 0, noChange, noLocktime, fmt.Errorf("insufficient "+
×
1604
                        "input to create sweep tx: input_sum=%v, "+
×
1605
                        "output_sum=%v", totalInput, requiredOutput+txFee)
×
1606
        }
×
1607

1608
        // The value remaining after the required output and fees is the
1609
        // change output.
1610
        changeAmt := totalInput - requiredOutput - txFee
22✔
1611
        changeOuts := make([]SweepOutput, 0, 2)
22✔
1612

22✔
1613
        extraChangeOut.WhenSome(func(o SweepOutput) {
44✔
1614
                changeOuts = append(changeOuts, o)
22✔
1615
        })
22✔
1616

1617
        // We'll calculate the dust limit for the given changePkScript since it
1618
        // is variable.
1619
        changeFloor := lnwallet.DustLimitForSize(
22✔
1620
                len(changePkScript.DeliveryAddress),
22✔
1621
        )
22✔
1622

22✔
1623
        switch {
22✔
1624
        // If the change amount is dust, we'll move it into the fees, and
1625
        // ignore it.
UNCOV
1626
        case changeAmt < changeFloor:
×
UNCOV
1627
                log.Infof("Change amt %v below dustlimit %v, not adding "+
×
UNCOV
1628
                        "change output", changeAmt, changeFloor)
×
UNCOV
1629

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

1638
                // The dust amount is added to the fee.
1639
                txFee += changeAmt
×
1640

1641
        // Otherwise, we'll actually recognize it as a change output.
1642
        default:
22✔
1643
                changeOuts = append(changeOuts, SweepOutput{
22✔
1644
                        TxOut: wire.TxOut{
22✔
1645
                                Value:    int64(changeAmt),
22✔
1646
                                PkScript: changePkScript.DeliveryAddress,
22✔
1647
                        },
22✔
1648
                        IsExtra:     false,
22✔
1649
                        InternalKey: changePkScript.InternalKey,
22✔
1650
                })
22✔
1651
        }
1652

1653
        // Optionally set the locktime.
1654
        locktimeOpt := fn.Some(locktime)
22✔
1655
        if locktime == -1 {
44✔
1656
                locktimeOpt = noLocktime
22✔
1657
        }
22✔
1658

1659
        var changeOutsOpt fn.Option[[]SweepOutput]
22✔
1660
        if len(changeOuts) > 0 {
44✔
1661
                changeOutsOpt = fn.Some(changeOuts)
22✔
1662
        }
22✔
1663

1664
        log.Debugf("Creating sweep tx for %v inputs (%s) using %v, "+
22✔
1665
                "tx_weight=%v, tx_fee=%v, locktime=%v, parents_count=%v, "+
22✔
1666
                "parents_fee=%v, parents_weight=%v, current_height=%v",
22✔
1667
                len(inputs), inputTypeSummary(inputs), feeRate,
22✔
1668
                estimator.weight(), txFee, locktimeOpt, len(estimator.parents),
22✔
1669
                estimator.parentsFee, estimator.parentsWeight, currentHeight)
22✔
1670

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