mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
sweep: catch third party spent in fee bumper for neutrino
This commit adds a new check for neutrino backend - when the inputs in the sweeping tx are spent by a third party, we will send back a `TxFailed` event to free the rest of the inputs for re-grouping.
This commit is contained in:
parent
563a5caed5
commit
19a599a1a9
@ -37,6 +37,10 @@ var (
|
|||||||
// ErrTxNoOutput is returned when an output cannot be created during tx
|
// ErrTxNoOutput is returned when an output cannot be created during tx
|
||||||
// preparation, usually due to the output being dust.
|
// preparation, usually due to the output being dust.
|
||||||
ErrTxNoOutput = errors.New("tx has no output")
|
ErrTxNoOutput = errors.New("tx has no output")
|
||||||
|
|
||||||
|
// ErrThirdPartySpent is returned when a third party has spent the
|
||||||
|
// input in the sweeping tx.
|
||||||
|
ErrThirdPartySpent = errors.New("third party spent the output")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bumper defines an interface that can be used by other subsystems for fee
|
// Bumper defines an interface that can be used by other subsystems for fee
|
||||||
@ -290,6 +294,11 @@ func NewTxPublisher(cfg TxPublisherConfig) *TxPublisher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNeutrinoBackend checks if the wallet backend is neutrino.
|
||||||
|
func (t *TxPublisher) isNeutrinoBackend() bool {
|
||||||
|
return t.cfg.Wallet.BackEnd() == "neutrino"
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast is used to publish the tx created from the given inputs. It will,
|
// Broadcast is used to publish the tx created from the given inputs. It will,
|
||||||
// 1. init a fee function based on the given strategy.
|
// 1. init a fee function based on the given strategy.
|
||||||
// 2. create an RBF-compliant tx and monitor it for confirmation.
|
// 2. create an RBF-compliant tx and monitor it for confirmation.
|
||||||
@ -724,6 +733,12 @@ func (t *TxPublisher) processRecords() {
|
|||||||
// feeBumpRecords stores a map of the records which need to be bumped.
|
// feeBumpRecords stores a map of the records which need to be bumped.
|
||||||
feeBumpRecords := make(map[uint64]*monitorRecord)
|
feeBumpRecords := make(map[uint64]*monitorRecord)
|
||||||
|
|
||||||
|
// failedRecords stores a map of the records which has inputs being
|
||||||
|
// spent by a third party.
|
||||||
|
//
|
||||||
|
// NOTE: this is only used for neutrino backend.
|
||||||
|
failedRecords := make(map[uint64]*monitorRecord)
|
||||||
|
|
||||||
// visitor is a helper closure that visits each record and divides them
|
// visitor is a helper closure that visits each record and divides them
|
||||||
// into two groups.
|
// into two groups.
|
||||||
visitor := func(requestID uint64, r *monitorRecord) error {
|
visitor := func(requestID uint64, r *monitorRecord) error {
|
||||||
@ -738,6 +753,16 @@ func (t *TxPublisher) processRecords() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check whether the inputs has been spent by a third party.
|
||||||
|
//
|
||||||
|
// NOTE: this check is only done for neutrino backend.
|
||||||
|
if t.isThirdPartySpent(r.tx.TxHash(), r.req.Inputs) {
|
||||||
|
failedRecords[requestID] = r
|
||||||
|
|
||||||
|
// Move to the next record.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
feeBumpRecords[requestID] = r
|
feeBumpRecords[requestID] = r
|
||||||
|
|
||||||
// Return nil to move to the next record.
|
// Return nil to move to the next record.
|
||||||
@ -768,6 +793,17 @@ func (t *TxPublisher) processRecords() {
|
|||||||
t.wg.Add(1)
|
t.wg.Add(1)
|
||||||
go t.handleFeeBumpTx(requestID, rec, currentHeight)
|
go t.handleFeeBumpTx(requestID, rec, currentHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For records that are failed, we'll notify the caller about this
|
||||||
|
// result.
|
||||||
|
for requestID, r := range failedRecords {
|
||||||
|
rec := r
|
||||||
|
|
||||||
|
log.Debugf("Tx=%v has inputs been spent by a third party, "+
|
||||||
|
"failing it now", r.tx.TxHash())
|
||||||
|
t.wg.Add(1)
|
||||||
|
go t.handleThirdPartySpent(rec, requestID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTxConfirmed is called when a monitored tx is confirmed. It will
|
// handleTxConfirmed is called when a monitored tx is confirmed. It will
|
||||||
@ -837,6 +873,33 @@ func (t *TxPublisher) handleFeeBumpTx(requestID uint64, r *monitorRecord,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleThirdPartySpent is called when the inputs in an unconfirmed tx is
|
||||||
|
// spent. It will notify the subscriber then remove the record from the maps
|
||||||
|
// and send a TxFailed event to the subscriber.
|
||||||
|
//
|
||||||
|
// NOTE: Must be run as a goroutine to avoid blocking on sending the result.
|
||||||
|
func (t *TxPublisher) handleThirdPartySpent(r *monitorRecord,
|
||||||
|
requestID uint64) {
|
||||||
|
|
||||||
|
defer t.wg.Done()
|
||||||
|
|
||||||
|
// Create a result that will be sent to the resultChan which is
|
||||||
|
// listened by the caller.
|
||||||
|
//
|
||||||
|
// TODO(yy): create a new state `TxThirdPartySpent` to notify the
|
||||||
|
// sweeper to remove the input, hence moving the monitoring of inputs
|
||||||
|
// spent inside the fee bumper.
|
||||||
|
result := &BumpResult{
|
||||||
|
Event: TxFailed,
|
||||||
|
Tx: r.tx,
|
||||||
|
requestID: requestID,
|
||||||
|
Err: ErrThirdPartySpent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify that this tx is confirmed and remove the record from the map.
|
||||||
|
t.handleResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
// createAndPublishTx creates a new tx with a higher fee rate and publishes it
|
// createAndPublishTx creates a new tx with a higher fee rate and publishes it
|
||||||
// to the network. It will update the record with the new tx and fee rate if
|
// to the network. It will update the record with the new tx and fee rate if
|
||||||
// successfully created, and return the result when published successfully.
|
// successfully created, and return the result when published successfully.
|
||||||
@ -953,6 +1016,66 @@ func (t *TxPublisher) isConfirmed(txid chainhash.Hash) bool {
|
|||||||
return details.NumConfirmations > 0
|
return details.NumConfirmations > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isThirdPartySpent checks whether the inputs of the tx has already been spent
|
||||||
|
// by a third party. When a tx is not confirmed, yet its inputs has been spent,
|
||||||
|
// then it must be spent by a different tx other than the sweeping tx here.
|
||||||
|
//
|
||||||
|
// NOTE: this check is only performed for neutrino backend as it has no
|
||||||
|
// reliable way to tell a tx has been replaced.
|
||||||
|
func (t *TxPublisher) isThirdPartySpent(txid chainhash.Hash,
|
||||||
|
inputs []input.Input) bool {
|
||||||
|
|
||||||
|
// Skip this check for if this is not neutrino backend.
|
||||||
|
if !t.isNeutrinoBackend() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate all the inputs and check if they have been spent already.
|
||||||
|
for _, inp := range inputs {
|
||||||
|
op := inp.OutPoint()
|
||||||
|
|
||||||
|
// If the input has already been spent after the height hint, a
|
||||||
|
// spend event is sent back immediately.
|
||||||
|
spendEvent, err := t.cfg.Notifier.RegisterSpendNtfn(
|
||||||
|
&op, inp.SignDesc().Output.PkScript, inp.HeightHint(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Criticalf("Failed to register spend ntfn: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the subscription when exit.
|
||||||
|
defer spendEvent.Cancel()
|
||||||
|
|
||||||
|
// Do a non-blocking read to see if the output has been spent.
|
||||||
|
select {
|
||||||
|
case spend, ok := <-spendEvent.Spend:
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("Spend ntfn for %v canceled", op)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
spendingTxID := spend.SpendingTx.TxHash()
|
||||||
|
|
||||||
|
// If the spending tx is the same as the sweeping tx
|
||||||
|
// then we are good.
|
||||||
|
if spendingTxID == txid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Detected third party spent of output=%v "+
|
||||||
|
"in tx=%v", op, spend.SpendingTx.TxHash())
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
// Move to the next input.
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// calcCurrentConfTarget calculates the current confirmation target based on
|
// calcCurrentConfTarget calculates the current confirmation target based on
|
||||||
// the deadline height. The conf target is capped at 0 if the deadline has
|
// the deadline height. The conf target is capped at 0 if the deadline has
|
||||||
// already been past.
|
// already been past.
|
||||||
|
@ -1358,6 +1358,7 @@ func TestProcessRecords(t *testing.T) {
|
|||||||
NumConfirmations: 0,
|
NumConfirmations: 0,
|
||||||
}, nil,
|
}, nil,
|
||||||
).Once()
|
).Once()
|
||||||
|
m.wallet.On("BackEnd").Return("test-backend").Once()
|
||||||
|
|
||||||
// Setup the initial publisher state by adding the records to the maps.
|
// Setup the initial publisher state by adding the records to the maps.
|
||||||
subscriberConfirmed := make(chan *BumpResult, 1)
|
subscriberConfirmed := make(chan *BumpResult, 1)
|
||||||
|
@ -51,4 +51,9 @@ type Wallet interface {
|
|||||||
// its transaction hash.
|
// its transaction hash.
|
||||||
GetTransactionDetails(txHash *chainhash.Hash) (
|
GetTransactionDetails(txHash *chainhash.Hash) (
|
||||||
*lnwallet.TransactionDetail, error)
|
*lnwallet.TransactionDetail, error)
|
||||||
|
|
||||||
|
// BackEnd returns a name for the wallet's backing chain service,
|
||||||
|
// which could be e.g. btcd, bitcoind, neutrino, or another consensus
|
||||||
|
// service.
|
||||||
|
BackEnd() string
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,10 @@ func newMockBackend(t *testing.T, notifier *MockNotifier) *mockBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *mockBackend) BackEnd() string {
|
||||||
|
return "mockbackend"
|
||||||
|
}
|
||||||
|
|
||||||
func (b *mockBackend) CheckMempoolAcceptance(tx *wire.MsgTx) error {
|
func (b *mockBackend) CheckMempoolAcceptance(tx *wire.MsgTx) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -357,6 +361,14 @@ type MockWallet struct {
|
|||||||
// Compile-time constraint to ensure MockWallet implements Wallet.
|
// Compile-time constraint to ensure MockWallet implements Wallet.
|
||||||
var _ Wallet = (*MockWallet)(nil)
|
var _ Wallet = (*MockWallet)(nil)
|
||||||
|
|
||||||
|
// BackEnd returns a name for the wallet's backing chain service, which could
|
||||||
|
// be e.g. btcd, bitcoind, neutrino, or another consensus service.
|
||||||
|
func (m *MockWallet) BackEnd() string {
|
||||||
|
args := m.Called()
|
||||||
|
|
||||||
|
return args.String(0)
|
||||||
|
}
|
||||||
|
|
||||||
// CheckMempoolAcceptance checks if the transaction can be accepted to the
|
// CheckMempoolAcceptance checks if the transaction can be accepted to the
|
||||||
// mempool.
|
// mempool.
|
||||||
func (m *MockWallet) CheckMempoolAcceptance(tx *wire.MsgTx) error {
|
func (m *MockWallet) CheckMempoolAcceptance(tx *wire.MsgTx) error {
|
||||||
|
Loading…
Reference in New Issue
Block a user