Skip to content

Commit 74161f0

Browse files
committed
sweep: make sure recovered inputs are retried
Previously, when a given input is found spent in the mempool, we'd mark it as Published and never offer it to the fee bumper. This is dangerous as the input will never be fee bumped. We now fix it by always initializing the input with state Init, and only use mempool to check for fee and fee rate. This changes the current restart behavior - as previously when a sweeping tx is broadcast, the node shuts down, when it starts again, the input will be offered to the sweeper again, but not to the fee bumper, which means the sweeping tx will stay in the mempool with the last-tried fee rate. After this change, after a restart, the input will be swept again, and the fee bumper will monitor its status. The restart will also behave like a fee bump if there's already an existing sweeping tx in the mempool.
1 parent 4bd1a34 commit 74161f0

File tree

2 files changed

+32
-33
lines changed

2 files changed

+32
-33
lines changed

sweep/sweeper.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,21 +1230,26 @@ func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error {
12301230
}
12311231

12321232
// This is a new input, and we want to query the mempool to see if this
1233-
// input has already been spent. If so, we'll start the input with
1234-
// state Published and attach the RBFInfo.
1235-
state, rbfInfo := s.decideStateAndRBFInfo(input.input.OutPoint())
1233+
// input has already been spent. If so, we'll start the input with the
1234+
// RBFInfo.
1235+
rbfInfo := s.decideRBFInfo(input.input.OutPoint())
12361236

12371237
// Create a new pendingInput and initialize the listeners slice with
12381238
// the passed in result channel. If this input is offered for sweep
12391239
// again, the result channel will be appended to this slice.
12401240
pi = &SweeperInput{
1241-
state: state,
1241+
state: Init,
12421242
listeners: []chan Result{input.resultChan},
12431243
Input: input.input,
12441244
params: input.params,
12451245
rbf: rbfInfo,
12461246
}
12471247

1248+
// Set the starting fee rate if a previous sweeping tx is found.
1249+
rbfInfo.WhenSome(func(info RBFInfo) {
1250+
pi.params.StartingFeeRate = fn.Some(info.FeeRate)
1251+
})
1252+
12481253
// Set the acutal deadline height.
12491254
pi.DeadlineHeight = input.params.DeadlineHeight.UnwrapOr(
12501255
s.calculateDefaultDeadline(pi),
@@ -1277,13 +1282,12 @@ func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error {
12771282
return nil
12781283
}
12791284

1280-
// decideStateAndRBFInfo queries the mempool to see whether the given input has
1281-
// already been spent. If so, the state Published will be returned, otherwise
1282-
// state Init. When spent, it will query the sweeper store to fetch the fee
1283-
// info of the spending transction, and construct an RBFInfo based on it.
1284-
// Suppose an error occurs, fn.None is returned.
1285-
func (s *UtxoSweeper) decideStateAndRBFInfo(op wire.OutPoint) (
1286-
SweepState, fn.Option[RBFInfo]) {
1285+
// decideRBFInfo queries the mempool to see whether the given input has already
1286+
// been spent. When spent, it will query the sweeper store to fetch the fee info
1287+
// of the spending transction, and construct an RBFInfo based on it. Suppose an
1288+
// error occurs, fn.None is returned.
1289+
func (s *UtxoSweeper) decideRBFInfo(
1290+
op wire.OutPoint) fn.Option[RBFInfo] {
12871291

12881292
// Check if we can find the spending tx of this input in mempool.
12891293
txOption := s.mempoolLookup(op)
@@ -1301,7 +1305,7 @@ func (s *UtxoSweeper) decideStateAndRBFInfo(op wire.OutPoint) (
13011305
// - for neutrino we don't have a mempool.
13021306
// - for btcd below v0.24.1 we don't have `gettxspendingprevout`.
13031307
if tx == nil {
1304-
return Init, fn.None[RBFInfo]()
1308+
return fn.None[RBFInfo]()
13051309
}
13061310

13071311
// Otherwise the input is already spent in the mempool, so eventually
@@ -1313,20 +1317,23 @@ func (s *UtxoSweeper) decideStateAndRBFInfo(op wire.OutPoint) (
13131317
txid := tx.TxHash()
13141318
tr, err := s.cfg.Store.GetTx(txid)
13151319

1320+
log.Debugf("Found spending tx %v in mempool for input %v", tx.TxHash(),
1321+
op)
1322+
13161323
// If the tx is not found in the store, it means it's not broadcast by
13171324
// us, hence we can't find the fee info. This is fine as, later on when
13181325
// this tx is confirmed, we will remove the input from our inputs.
13191326
if errors.Is(err, ErrTxNotFound) {
13201327
log.Warnf("Spending tx %v not found in sweeper store", txid)
1321-
return Published, fn.None[RBFInfo]()
1328+
return fn.None[RBFInfo]()
13221329
}
13231330

13241331
// Exit if we get an db error.
13251332
if err != nil {
13261333
log.Errorf("Unable to get tx %v from sweeper store: %v",
13271334
txid, err)
13281335

1329-
return Published, fn.None[RBFInfo]()
1336+
return fn.None[RBFInfo]()
13301337
}
13311338

13321339
// Prepare the fee info and return it.
@@ -1336,7 +1343,7 @@ func (s *UtxoSweeper) decideStateAndRBFInfo(op wire.OutPoint) (
13361343
FeeRate: chainfee.SatPerKWeight(tr.FeeRate),
13371344
})
13381345

1339-
return Published, rbf
1346+
return rbf
13401347
}
13411348

13421349
// handleExistingInput processes an input that is already known to the sweeper.

sweep/sweeper_test.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -497,10 +497,9 @@ func TestUpdateSweeperInputs(t *testing.T) {
497497
require.Equal(expectedInputs, s.inputs)
498498
}
499499

500-
// TestDecideStateAndRBFInfo checks that the expected state and RBFInfo are
501-
// returned based on whether this input can be found both in mempool and the
502-
// sweeper store.
503-
func TestDecideStateAndRBFInfo(t *testing.T) {
500+
// TestDecideRBFInfo checks that the expected RBFInfo is returned based on
501+
// whether this input can be found both in mempool and the sweeper store.
502+
func TestDecideRBFInfo(t *testing.T) {
504503
t.Parallel()
505504

506505
require := require.New(t)
@@ -524,11 +523,9 @@ func TestDecideStateAndRBFInfo(t *testing.T) {
524523
mockMempool.On("LookupInputMempoolSpend", op).Return(
525524
fn.None[wire.MsgTx]()).Once()
526525

527-
// Since the mempool lookup failed, we exepect state Init and no
528-
// RBFInfo.
529-
state, rbf := s.decideStateAndRBFInfo(op)
526+
// Since the mempool lookup failed, we expect no RBFInfo.
527+
rbf := s.decideRBFInfo(op)
530528
require.True(rbf.IsNone())
531-
require.Equal(Init, state)
532529

533530
// Mock the mempool lookup to return a tx three times as we are calling
534531
// attachAvailableRBFInfo three times.
@@ -539,19 +536,17 @@ func TestDecideStateAndRBFInfo(t *testing.T) {
539536
// Mock the store to return an error saying the tx cannot be found.
540537
mockStore.On("GetTx", tx.TxHash()).Return(nil, ErrTxNotFound).Once()
541538

542-
// Although the db lookup failed, we expect the state to be Published.
543-
state, rbf = s.decideStateAndRBFInfo(op)
539+
// The db lookup failed, we expect no RBFInfo.
540+
rbf = s.decideRBFInfo(op)
544541
require.True(rbf.IsNone())
545-
require.Equal(Published, state)
546542

547543
// Mock the store to return a db error.
548544
dummyErr := errors.New("dummy error")
549545
mockStore.On("GetTx", tx.TxHash()).Return(nil, dummyErr).Once()
550546

551-
// Although the db lookup failed, we expect the state to be Published.
552-
state, rbf = s.decideStateAndRBFInfo(op)
547+
// The db lookup failed, we expect no RBFInfo.
548+
rbf = s.decideRBFInfo(op)
553549
require.True(rbf.IsNone())
554-
require.Equal(Published, state)
555550

556551
// Mock the store to return a record.
557552
tr := &TxRecord{
@@ -561,7 +556,7 @@ func TestDecideStateAndRBFInfo(t *testing.T) {
561556
mockStore.On("GetTx", tx.TxHash()).Return(tr, nil).Once()
562557

563558
// Call the method again.
564-
state, rbf = s.decideStateAndRBFInfo(op)
559+
rbf = s.decideRBFInfo(op)
565560

566561
// Assert that the RBF info is returned.
567562
rbfInfo := fn.Some(RBFInfo{
@@ -570,9 +565,6 @@ func TestDecideStateAndRBFInfo(t *testing.T) {
570565
FeeRate: chainfee.SatPerKWeight(tr.FeeRate),
571566
})
572567
require.Equal(rbfInfo, rbf)
573-
574-
// Assert the state is updated.
575-
require.Equal(Published, state)
576568
}
577569

578570
// TestMarkInputFatal checks that the input is marked as expected.

0 commit comments

Comments
 (0)