diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 81abefbdf..431c9f885 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -237,3 +237,11 @@ func (w *WalletController) Start() error { func (w *WalletController) Stop() error { return nil } + +func (w *WalletController) FetchTx(chainhash.Hash) (*wire.MsgTx, error) { + return nil, nil +} + +func (w *WalletController) RemoveDescendants(*wire.MsgTx) error { + return nil +} diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index c71693ff8..42198b3d8 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -56,6 +56,10 @@ var ( // stored within the top-level waleltdb buckets of btcwallet. waddrmgrNamespaceKey = []byte("waddrmgr") + // wtxmgrNamespaceKey is the namespace key that the wtxmgr state is + // stored within the top-level waleltdb buckets of btcwallet. + wtxmgrNamespaceKey = []byte("wtxmgr") + // lightningAddrSchema is the scope addr schema for all keys that we // derive. We'll treat them all as p2wkh addresses, as atm we must // specify a particular type. @@ -1400,3 +1404,46 @@ func (b *BtcWallet) GetRecoveryInfo() (bool, float64, error) { return isRecoveryMode, progress, nil } + +// FetchTx attempts to fetch a transaction in the wallet's database identified +// by the passed transaction hash. If the transaction can't be found, then a +// nil pointer is returned. +func (b *BtcWallet) FetchTx(txHash chainhash.Hash) (*wire.MsgTx, error) { + var targetTx *wtxmgr.TxDetails + err := walletdb.View(b.db, func(tx walletdb.ReadTx) error { + wtxmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + txDetails, err := b.wallet.TxStore.TxDetails(wtxmgrNs, &txHash) + if err != nil { + return err + } + + targetTx = txDetails + + return nil + }) + if err != nil { + return nil, err + } + + if targetTx == nil { + return nil, nil + } + + return &targetTx.TxRecord.MsgTx, nil +} + +// RemoveDescendants attempts to remove any transaction from the wallet's tx +// store (that may be unconfirmed) that spends outputs created by the passed +// transaction. This remove propagates recursively down the chain of descendent +// transactions. +func (b *BtcWallet) RemoveDescendants(tx *wire.MsgTx) error { + txRecord, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) + if err != nil { + return err + } + + return walletdb.Update(b.db, func(tx walletdb.ReadWriteTx) error { + wtxmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + return b.wallet.TxStore.RemoveUnminedTx(wtxmgrNs, txRecord) + }) +} diff --git a/lnwallet/interface.go b/lnwallet/interface.go index b19066583..c74573c2d 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -346,6 +346,17 @@ type WalletController interface { // is set. Labels must not be empty, and they are limited to 500 chars. LabelTransaction(hash chainhash.Hash, label string, overwrite bool) error + // FetchTx attempts to fetch a transaction in the wallet's database + // identified by the passed transaction hash. If the transaction can't + // be found, then a nil pointer is returned. + FetchTx(chainhash.Hash) (*wire.MsgTx, error) + + // RemoveDescendants attempts to remove any transaction from the + // wallet's tx store (that may be unconfirmed) that spends outputs + // created by the passed transaction. This remove propagates + // recursively down the chain of descendent transactions. + RemoveDescendants(*wire.MsgTx) error + // FundPsbt creates a fully populated PSBT packet that contains enough // inputs to fund the outputs specified in the passed in packet with the // specified fee rate. If there is change left, a change output from the diff --git a/sweep/backend_mock_test.go b/sweep/backend_mock_test.go index 16cfb1cef..6abce171d 100644 --- a/sweep/backend_mock_test.go +++ b/sweep/backend_mock_test.go @@ -151,3 +151,11 @@ func (b *mockBackend) mine() { func (b *mockBackend) isDone() bool { return len(b.unconfirmedTxes) == 0 } + +func (b *mockBackend) RemoveDescendants(*wire.MsgTx) error { + return nil +} + +func (b *mockBackend) FetchTx(chainhash.Hash) (*wire.MsgTx, error) { + return nil, nil +} diff --git a/sweep/interface.go b/sweep/interface.go index df7e00aa7..d9a1e6094 100644 --- a/sweep/interface.go +++ b/sweep/interface.go @@ -1,6 +1,7 @@ package sweep import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/lnwallet" ) @@ -25,4 +26,13 @@ type Wallet interface { // ability to execute a function closure under an exclusive coin // selection lock. WithCoinSelectLock(f func() error) error + + // RemoveDescendants removes any wallet transactions that spends + // outputs created by the specified transaction. + RemoveDescendants(*wire.MsgTx) error + + // FetchTx returns the transaction that corresponds to the transaction + // hash passed in. If the transaction can't be found then a nil + // transaction pointer is returned. + FetchTx(chainhash.Hash) (*wire.MsgTx, error) } diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 7bc561542..74f8df9cb 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -510,6 +510,81 @@ func (s *UtxoSweeper) feeRateForPreference( return feeRate, nil } +// removeLastSweepDescendants removes any transactions from the wallet that +// spend outputs produced by the passed spendingTx. This needs to be done in +// cases where we're not the only ones that can sweep an output, but there may +// exist unconfirmed spends that spend outputs created by a sweep transaction. +// The most common case for this is when someone sweeps our anchor outputs +// after 16 blocks. +func (s *UtxoSweeper) removeLastSweepDescendants(spendingTx *wire.MsgTx) error { + // Obtain all the past sweeps that we've done so far. We'll need these + // to ensure that if the spendingTx spends any of the same inputs, then + // we remove any transaction that may be spending those inputs from the + // wallet. + // + // TODO(roasbeef): can be last sweep here if we remove anything confirmed + // from the store? + pastSweepHashes, err := s.cfg.Store.ListSweeps() + if err != nil { + return err + } + + log.Debugf("Attempting to remove descendant txns invalidated by "+ + "(txid=%v): %v", spendingTx.TxHash(), spew.Sdump(spendingTx)) + + // Construct a map of the inputs this transaction spends for each look + // up. + inputsSpent := make(map[wire.OutPoint]struct{}, len(spendingTx.TxIn)) + for _, txIn := range spendingTx.TxIn { + inputsSpent[txIn.PreviousOutPoint] = struct{}{} + } + + // We'll now go through each past transaction we published during this + // epoch and cross reference the spent inputs. If there're any inputs + // in common with the inputs the spendingTx spent, then we'll remove + // those. + // + // TODO(roasbeef): need to start to remove all transaction hashes after + // every N blocks (assumed point of no return) + for _, sweepHash := range pastSweepHashes { + sweepTx, err := s.cfg.Wallet.FetchTx(sweepHash) + if err != nil { + return err + } + + // Transaction wasn't found in the wallet, may have already + // been replaced/removed. + if sweepTx == nil { + continue + } + + // Check to see if this past sweep transaction spent any of the + // same inputs as spendingTx. + var isConflicting bool + for _, txIn := range sweepTx.TxIn { + if _, ok := inputsSpent[txIn.PreviousOutPoint]; ok { + isConflicting = true + break + } + } + + // If it did, then we'll signal the wallet to remove all the + // transactions that are descendants of outputs created by the + // sweepTx. + if isConflicting { + log.Debugf("Removing sweep txid=%v from wallet: %v", + sweepTx.TxHash(), spew.Sdump(sweepTx)) + + err := s.cfg.Wallet.RemoveDescendants(sweepTx) + if err != nil { + log.Warnf("unable to remove descendants: %v", err) + } + } + } + + return nil +} + // collector is the sweeper main loop. It processes new inputs, spend // notifications and counts down to publication of the sweep tx. func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { @@ -619,12 +694,29 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { continue } - log.Debugf("Detected spend related to in flight inputs "+ - "(is_ours=%v): %v", - newLogClosure(func() string { - return spew.Sdump(spend.SpendingTx) - }), isOurTx, - ) + // If this isn't our transaction, it means someone else + // swept outputs that we were attempting to sweep. This + // can happen for anchor outputs as well as justice + // transactions. In this case, we'll notify the wallet + // to remove any spends that a descent from this + // output. + if !isOurTx { + err := s.removeLastSweepDescendants( + spend.SpendingTx, + ) + if err != nil { + log.Warnf("unable to remove descendant "+ + "transactions due to tx %v: ", + spendHash) + } + + log.Debugf("Detected spend related to in flight inputs "+ + "(is_ours=%v): %v", + newLogClosure(func() string { + return spew.Sdump(spend.SpendingTx) + }), isOurTx, + ) + } // Signal sweep results for inputs in this confirmed // tx. diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index 08a0ff4d3..19934213d 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -220,7 +220,9 @@ func createSweepTx(inputs []input.Input, outputs []*wire.TxOut, } if requiredOutput+txFee > totalInput { - return nil, fmt.Errorf("insufficient input to create sweep tx") + return nil, fmt.Errorf("insufficient input to create sweep "+ + "tx: input_sum=%v, output_sum=%v", totalInput, + requiredOutput+txFee) } // The value remaining after the required output and fees, go to