Skip to content

sweep: fix expected spending events being missed #10060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
Estimator: cc.FeeEstimator,
Notifier: cc.ChainNotifier,
AuxSweeper: s.implCfg.AuxSweeper,
ChainIO: cc.ChainIO,
})

s.sweeper = sweep.New(&sweep.UtxoSweeperConfig{
Expand Down
74 changes: 63 additions & 11 deletions sweep/fee_bumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"sync"
"sync/atomic"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
Expand All @@ -20,10 +21,15 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/tlv"
)

// spentNotificationTimeout defines the time to wait for a spending event from
// `RegisterSpendNtfn` when an immediate response is expected.
const spentNotificationTimeout = 1 * time.Second

var (
// ErrInvalidBumpResult is returned when the bump result is invalid.
ErrInvalidBumpResult = errors.New("invalid bump result")
Expand Down Expand Up @@ -111,8 +117,8 @@ const (
// error, which means they cannot be retried with increased budget.
TxFatal

// sentinalEvent is used to check if an event is unknown.
sentinalEvent
// sentinelEvent is used to check if an event is unknown.
sentinelEvent
)

// String returns a human-readable string for the event.
Expand All @@ -137,13 +143,13 @@ func (e BumpEvent) String() string {

// Unknown returns true if the event is unknown.
func (e BumpEvent) Unknown() bool {
return e >= sentinalEvent
return e >= sentinelEvent
}

// BumpRequest is used by the caller to give the Bumper the necessary info to
// create and manage potential fee bumps for a set of inputs.
type BumpRequest struct {
// Budget givens the total amount that can be used as fees by these
// Budget gives the total amount that can be used as fees by these
// inputs.
Budget btcutil.Amount

Expand Down Expand Up @@ -344,6 +350,10 @@ type TxPublisherConfig struct {
// Notifier is used to monitor the confirmation status of the tx.
Notifier chainntnfs.ChainNotifier

// ChainIO represents an abstraction over a source that can query the
// blockchain.
ChainIO lnwallet.BlockChainIO

// AuxSweeper is an optional interface that can be used to modify the
// way sweep transaction are generated.
AuxSweeper fn.Option[AuxSweeper]
Expand Down Expand Up @@ -589,7 +599,7 @@ func (t *TxPublisher) createRBFCompliantTx(
// used up the budget, we will return an error
// indicating this tx cannot be made. The
// sweeper should handle this error and try to
// cluster these inputs differetly.
// cluster these inputs differently.
increased, err = f.Increment()
if err != nil {
return nil, err
Expand Down Expand Up @@ -1332,7 +1342,7 @@ func (t *TxPublisher) createAndPublishTx(
// the fee bumper retry it at next block.
//
// NOTE: we may get this error if we've bypassed the mempool check,
// which means we are suing neutrino backend.
// which means we are using neutrino backend.
if errors.Is(result.Err, chain.ErrInsufficientFee) ||
errors.Is(result.Err, lnwallet.ErrMempoolFee) {

Expand Down Expand Up @@ -1415,6 +1425,38 @@ func (t *TxPublisher) getSpentInputs(
"%v", op, heightHint)
}

// Check whether the input has been spent or not.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the GetUtxo call is probably just added here to save us time? because I noticed RegisterSpendNtfn also checks this internally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah correct, it creates a shortcut here so we don't need to make unnecessary subscriptions. We only attempt to subscribe for spending when we know it's not in the utxo set, which means either the input has been spent or it's an orphan.

utxo, err := t.cfg.ChainIO.GetUtxo(
&op, inp.SignDesc().Output.PkScript, heightHint, t.quit,
)
if err != nil {
// GetUtxo will return `ErrOutputSpent` when the input
// has already been spent. In that case, the returned
// `utxo` must be nil, which will move us to subscribe
// its spending event below.
if !errors.Is(err, btcwallet.ErrOutputSpent) {
log.Errorf("Failed to get utxo for input=%v: "+
"%v", op, err)

// If this is an unexpected error, move to check
// the next input.
continue
}

log.Tracef("GetUtxo for input=%v, err: %v", op, err)
}

// If a non-nil utxo is returned it means this input is still
// unspent. Thus we can continue to the next input as there's no
// need to register spend notification for it.
if utxo != nil {
log.Tracef("Input=%v not spent yet", op)
continue
}

log.Debugf("Input=%v already spent, fetching its spending "+
"tx...", op)

// If the input has already been spent after the height hint, a
// spend event is sent back immediately.
spendEvent, err := t.cfg.Notifier.RegisterSpendNtfn(
Expand All @@ -1424,13 +1466,13 @@ func (t *TxPublisher) getSpentInputs(
log.Criticalf("Failed to register spend ntfn for "+
"input=%v: %v", op, err)

return nil
return spentInputs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So initially we return nil, and looking at the 2 instances this method is used, there is a check for the length of what was returned if len(spends) == 0 {. That would have caused LND to panic, right?.

A follow-up question is: what happens when we have multiple inputs (I guess that's a possibility), and one fails? Does that affect where we call the method since no error will be returned, and the only check I see is for the length of the returned result?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning nil here actually returns an empty map, so the nil is actually a zero-value map, thus calling len won't panic.

what happens when we have multiple inputs (I guess that's a possibility), and one fails?

What do you mean one fails? If there's a failure here, then we'd shut down lnd due to Criticalf.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean one fails? If there's a failure here, then we'd shut down lnd due to Criticalf.

Ah, I now understand that Criticalf sends a shutdown request after logging the error.

}

// Remove the subscription when exit.
defer spendEvent.Cancel()

// Do a non-blocking read to see if the output has been spent.
// Do a blocking read to receive the spent event.
select {
case spend, ok := <-spendEvent.Spend:
if !ok {
Expand All @@ -1446,9 +1488,19 @@ func (t *TxPublisher) getSpentInputs(

spentInputs[op] = spendingTx

// Move to the next input.
default:
log.Tracef("Input %v not spent yet", op)
// The above spent event should be returned immediately, yet we
// still perform a timeout check here in case it blocks forever.
//
// TODO(yy): The proper way to fix this is to redesign the area
// so we use the async flow for checking whether a given input
// is spent or not. A better approach is to implement a new
// synchronous method to check for spending, which should be
// attempted when implementing SQL into btcwallet.
case <-time.After(spentNotificationTimeout):
log.Warnf("Input is reported as spent by GetUtxo, "+
"but spending notification is not returned "+
"immediately: input=%v, heightHint=%v", op,
heightHint)
}
}

Expand Down
Loading
Loading