|
43 | 43 | // ErrUnknownSpent is returned when an unknown tx has spent an input in
|
44 | 44 | // the sweeping tx.
|
45 | 45 | 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") |
46 | 50 | )
|
47 | 51 |
|
48 | 52 | var (
|
@@ -653,10 +657,68 @@ func (t *TxPublisher) createAndCheckTx(r *monitorRecord) (*sweepTxCtx, error) {
|
653 | 657 | return sweepCtx, nil
|
654 | 658 | }
|
655 | 659 |
|
| 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 | + |
656 | 672 | return sweepCtx, fmt.Errorf("tx=%v failed mempool check: %w",
|
657 | 673 | sweepCtx.tx.TxHash(), err)
|
658 | 674 | }
|
659 | 675 |
|
| 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 | + |
660 | 722 | // broadcast takes a monitored tx and publishes it to the network. Prior to the
|
661 | 723 | // broadcast, it will subscribe the tx's confirmation notification and attach
|
662 | 724 | // 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) {
|
1052 | 1114 | case errors.Is(err, ErrZeroFeeRateDelta):
|
1053 | 1115 | result.Event = TxFailed
|
1054 | 1116 |
|
| 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 | + |
1055 | 1122 | // Otherwise this is not a fee-related error and the tx cannot be
|
1056 | 1123 | // retried. In that case we will fail ALL the inputs in this tx, which
|
1057 | 1124 | // means they will be removed from the sweeper and never be tried
|
@@ -1782,6 +1849,16 @@ func (t *TxPublisher) handleReplacementTxError(r *monitorRecord,
|
1782 | 1849 | return fn.None[BumpResult]()
|
1783 | 1850 | }
|
1784 | 1851 |
|
| 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 | + |
1785 | 1862 | // If the error is not fee related, we will return a `TxFailed` event
|
1786 | 1863 | // so this input can be retried.
|
1787 | 1864 | result := fn.Some(BumpResult{
|
|
0 commit comments