Skip to content

Commit b184afe

Browse files
committed
sweep: handle missing inputs during fee bumping
This commit handles the case when the input is missing during the RBF process, which could happen when the bumped tx has inputs being spent by a third party. Normally we should be able to catch the spend early via the spending notification and never attempt to fee bump the record. However, due to the possible race between block notification and spend notification, this cannot be guaranteed. Thus, we need to handle the case during the RBF when seeing a `ErrMissingInputs`, which can only happen when the inputs are spent by others.
1 parent 4f469de commit b184afe

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

contractcourt/anchor_resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) {
108108

109109
// Anchor was swept by someone else. This is possible after the
110110
// 16 block csv lock.
111-
case sweep.ErrRemoteSpend:
111+
case sweep.ErrRemoteSpend, sweep.ErrInputMissing:
112112
c.log.Warnf("our anchor spent by someone else")
113113
outcome = channeldb.ResolverOutcomeUnclaimed
114114

sweep/fee_bumper.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ var (
4343
// ErrUnknownSpent is returned when an unknown tx has spent an input in
4444
// the sweeping tx.
4545
ErrUnknownSpent = errors.New("unknown spend of input")
46+
47+
// ErrInputMissing is returned when a given input no longer exists,
48+
// e.g., spending from an orphan tx.
49+
ErrInputMissing = errors.New("input no longer exists")
4650
)
4751

4852
var (
@@ -653,10 +657,68 @@ func (t *TxPublisher) createAndCheckTx(r *monitorRecord) (*sweepTxCtx, error) {
653657
return sweepCtx, nil
654658
}
655659

660+
// If the inputs are spent by another tx, we will exit with the latest
661+
// sweepCtx and an error.
662+
if errors.Is(err, chain.ErrMissingInputs) {
663+
log.Debugf("Tx %v missing inputs, it's likely the input has "+
664+
"been spent by others", sweepCtx.tx.TxHash())
665+
666+
// Make sure to update the record with the latest attempt.
667+
t.updateRecord(r, sweepCtx)
668+
669+
return sweepCtx, ErrInputMissing
670+
}
671+
656672
return sweepCtx, fmt.Errorf("tx=%v failed mempool check: %w",
657673
sweepCtx.tx.TxHash(), err)
658674
}
659675

676+
// handleMissingInputs handles the case when the chain backend reports back a
677+
// missing inputs error, which could happen when one of the input has been spent
678+
// in another tx, or the input is referencing an orphan. When the input is
679+
// spent, it will be handled via the TxUnknownSpend flow by creating a
680+
// TxUnknownSpend bump result, otherwise, a TxFatal bump result is returned.
681+
func (t *TxPublisher) handleMissingInputs(r *monitorRecord) *BumpResult {
682+
// Get the spending txns.
683+
spends := t.getSpentInputs(r)
684+
685+
// Attach the spending txns.
686+
r.spentInputs = spends
687+
688+
// If there are no spending txns found and the input is missing, the
689+
// input is referencing an orphan tx that's no longer valid, e.g., the
690+
// spending the anchor output from the remote commitment after the local
691+
// commitment has confirmed. In this case we will mark it as fatal and
692+
// exit.
693+
if len(spends) == 0 {
694+
log.Warnf("Failing record=%v: found orphan inputs: %v\n",
695+
r.requestID, inputTypeSummary(r.req.Inputs))
696+
697+
// Create a result that will be sent to the resultChan which is
698+
// listened by the caller.
699+
result := &BumpResult{
700+
Event: TxFatal,
701+
Tx: r.tx,
702+
requestID: r.requestID,
703+
Err: ErrInputMissing,
704+
}
705+
706+
return result
707+
}
708+
709+
// Check that the spending tx matches the sweeping tx - given that the
710+
// current sweeping tx has been failed due to missing inputs, the
711+
// spending tx must be a different tx, thus it should NOT be matched. We
712+
// perform a sanity check here to catch the unexpected state.
713+
if !t.isUnknownSpent(r, spends) {
714+
log.Errorf("Sweeping tx %v has missing inputs, yet the "+
715+
"spending tx is the sweeping tx itself: %v",
716+
r.tx.TxHash(), r.spentInputs)
717+
}
718+
719+
return t.createUnknownSpentBumpResult(r)
720+
}
721+
660722
// broadcast takes a monitored tx and publishes it to the network. Prior to the
661723
// broadcast, it will subscribe the tx's confirmation notification and attach
662724
// the event channel to the record. Any broadcast-related errors will not be
@@ -1052,6 +1114,11 @@ func (t *TxPublisher) handleInitialTxError(r *monitorRecord, err error) {
10521114
case errors.Is(err, ErrZeroFeeRateDelta):
10531115
result.Event = TxFailed
10541116

1117+
// When there are missing inputs, we'll create a TxUnknownSpend bump
1118+
// result here so the rest of the inputs can be retried.
1119+
case errors.Is(err, ErrInputMissing):
1120+
result = t.handleMissingInputs(r)
1121+
10551122
// Otherwise this is not a fee-related error and the tx cannot be
10561123
// retried. In that case we will fail ALL the inputs in this tx, which
10571124
// means they will be removed from the sweeper and never be tried
@@ -1782,6 +1849,16 @@ func (t *TxPublisher) handleReplacementTxError(r *monitorRecord,
17821849
return fn.None[BumpResult]()
17831850
}
17841851

1852+
// At least one of the inputs is missing, which means it has already
1853+
// been spent by another tx and confirmed. In this case we will handle
1854+
// it by returning a TxUnknownSpend bump result.
1855+
if errors.Is(err, ErrInputMissing) {
1856+
log.Warnf("Fail to fee bump tx %v: %v", oldTx.TxHash(), err)
1857+
bumpResult := t.handleMissingInputs(r)
1858+
1859+
return fn.Some(*bumpResult)
1860+
}
1861+
17851862
// If the error is not fee related, we will return a `TxFailed` event
17861863
// so this input can be retried.
17871864
result := fn.Some(BumpResult{

0 commit comments

Comments
 (0)