-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
base: master
Are you sure you want to change the base?
Changes from all commits
940d317
1a26723
96a6857
5502d91
ea6c132
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -5,6 +5,7 @@ import ( | |||
"fmt" | ||||
"sync" | ||||
"sync/atomic" | ||||
"time" | ||||
|
||||
"github.com/btcsuite/btcd/btcutil" | ||||
"github.com/btcsuite/btcd/chaincfg/chainhash" | ||||
|
@@ -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") | ||||
|
@@ -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. | ||||
|
@@ -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 | ||||
|
||||
|
@@ -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] | ||||
|
@@ -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 | ||||
|
@@ -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) { | ||||
|
||||
|
@@ -1415,6 +1425,38 @@ func (t *TxPublisher) getSpentInputs( | |||
"%v", op, heightHint) | ||||
} | ||||
|
||||
// Check whether the input has been spent or not. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, will this also populate the spend cache for neutrino backends? Otherwise, this can be a very expensive filter rescan depending on how far back they are. In other words, this'll block for neutrno backends. Would need to check for behavior with backends that have the txindex off. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about just moving back to the spend channel/goroutine? That way it's always active, always watching, and we can handle the notification async when needed. It would allow us to remove all these other default select cases for spend ntfns. I recall I pointed out a possibility of missed events when this change was originally added. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
FWIW this is already used in lnd/chainntnfs/neutrinonotify/neutrino.go Line 864 in 4389067
Can try that route, meanwhile there's #10117 that fixes this issue using an alternative approach. I will see if it's possible to make a new sync method when implementing SQL into btcwallet. |
||||
&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( | ||||
|
@@ -1424,13 +1466,13 @@ func (t *TxPublisher) getSpentInputs( | |||
log.Criticalf("Failed to register spend ntfn for "+ | ||||
"input=%v: %v", op, err) | ||||
yyforyongyu marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
||||
return nil | ||||
return spentInputs | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So initially we return 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning
What do you mean one fails? If there's a failure here, then we'd shut down There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I now understand that |
||||
} | ||||
|
||||
// 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 { | ||||
|
@@ -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): | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the assumption here is not quite right, a spend event from Why do we need the spending transactions here, it looks like this is only used for logging/sanity checks, right? The docstring on |
||||
log.Warnf("Input is reported as spent by GetUtxo, "+ | ||||
"but spending notification is not returned "+ | ||||
"immediately: input=%v, heightHint=%v", op, | ||||
heightHint) | ||||
} | ||||
} | ||||
|
||||
|
Uh oh!
There was an error while loading. Please reload this page.