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

lightningnetwork / lnd / 12343072627

15 Dec 2024 11:09PM UTC coverage: 57.504% (-1.1%) from 58.636%
12343072627

Pull #9315

github

yyforyongyu
contractcourt: offer outgoing htlc one block earlier before its expiry

We need to offer the outgoing htlc one block earlier to make sure when
the expiry height hits, the sweeper will not miss sweeping it in the
same block. This also means the outgoing contest resolver now only does
one thing - watch for preimage spend till height expiry-1, which can
easily be moved into the timeout resolver instead in the future.
Pull Request #9315: Implement `blockbeat`

1445 of 2007 new or added lines in 26 files covered. (72.0%)

19246 existing lines in 249 files now uncovered.

102342 of 177975 relevant lines covered (57.5%)

24772.24 hits per line

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

74.19
/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"
×
NEW
118
        case TxFatal:
×
NEW
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.
NEW
272
func (b *BumpResult) String() string {
×
NEW
273
        desc := fmt.Sprintf("Event=%v", b.Event)
×
NEW
274
        if b.Tx != nil {
×
NEW
275
                desc += fmt.Sprintf(", Tx=%v", b.Tx.TxHash())
×
NEW
276
        }
×
277

NEW
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.
NEW
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.
NEW
806
        t.currentHeight.Store(beat.Height())
×
UNCOV
807

×
UNCOV
808
        t.wg.Add(1)
×
NEW
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.
NEW
837
func (t *TxPublisher) monitor() {
×
UNCOV
838
        defer t.wg.Done()
×
UNCOV
839

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

×
UNCOV
846
                        // Update the best known height for the publisher.
×
NEW
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

×
NEW
853
                        // Notify we've processed the block.
×
NEW
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✔
NEW
887
                        initialRecords[requestID] = r
×
NEW
888
                        return nil
×
NEW
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✔
NEW
923
                t.handleInitialBroadcast(r, requestID)
×
NEW
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)
×
NEW
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.
NEW
985
        case errors.Is(err, ErrTxNoOutput):
×
NEW
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.
NEW
991
        case errors.Is(err, ErrMaxPosition):
×
NEW
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.
NEW
996
        case errors.Is(err, ErrZeroFeeRateDelta):
×
NEW
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✔
NEW
1051
                // The broadcast failed, which can only happen if the tx record
×
NEW
1052
                // cannot be found or the aux sweeper returns an error. In
×
NEW
1053
                // either case, we will send back a TxFail event so these
×
NEW
1054
                // inputs can be retried.
×
NEW
1055
                result = &BumpResult{
×
NEW
1056
                        Event:     TxFailed,
×
NEW
1057
                        Err:       err,
×
NEW
1058
                        requestID: requestID,
×
NEW
1059
                }
×
NEW
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, ErrLocktimeImmature
×
1592
                }
×
1593

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

1600
                // Update the locktime for next iteration.
UNCOV
1601
                locktime = int32(lt)
×
1602
        }
1603

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

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

22✔
1616
        extraChangeOut.WhenSome(func(o SweepOutput) {
44✔
1617
                changeOuts = append(changeOuts, o)
22✔
1618
        })
22✔
1619

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

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

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

1641
                // The dust amount is added to the fee.
1642
                txFee += changeAmt
×
1643

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

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

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

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

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