From 7a2b5cef76d3c85bd6d43f2dd149b6facdab63ef Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Feb 2019 12:59:11 -0800 Subject: [PATCH] wallet/wallet: remove invalid transactions when broadcast fails In this commit, we rework how publishTransaction works in order to correctly handle removing invalid transactions from the wallet's unconfirmed transaction store. This is crucial as otherwise, invalid transactions can remain within the wallet and be used for further transactions, causing a chain of inaccurate transactions. publishTransaction will now only return an error if the transaction fails to be broadcast and it has not been previously seen in the mempool/chain. This is intended in order to provide an easier API to callers. Any other errors when broadcasting the transaction will cause it to be removed from the wallet's unconfirmed transaction store to ensure it maintains an accurate view of the chain. --- wallet/wallet.go | 69 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/wallet/wallet.go b/wallet/wallet.go index bd16d71..1a11602 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3444,24 +3444,39 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { case err == nil: return txid, nil - // The following are errors returned from btcd's mempool. - case strings.Contains(err.Error(), "spent"): - fallthrough - case strings.Contains(err.Error(), "orphan"): - fallthrough - case strings.Contains(err.Error(), "conflict"): + // Since we have different backends that can be used with the wallet, + // we'll need to check specific errors for each one. + // + // If the transaction is already in the mempool, we can just return now. + // + // This error is returned when broadcasting/sending a transaction to a + // btcd node that already has it in their mempool. + case strings.Contains(err.Error(), "already have transaction"): fallthrough - // The following errors are returned from bitcoind's mempool. - case strings.Contains(err.Error(), "fee not met"): + // This error is returned when broadcasting a transaction to a bitcoind + // node that already has it in their mempool. + case strings.Contains(err.Error(), "txn-already-in-mempool"): + return txid, nil + + // If the transaction has already confirmed, we can safely remove it + // from the unconfirmed store as it should already exist within the + // confirmed store. We'll avoid returning an error as the broadcast was + // in a sense successful. + // + // This error is returned when broadcasting/sending a transaction that + // has already confirmed to a btcd node. + case strings.Contains(err.Error(), "transaction already exists"): fallthrough - case strings.Contains(err.Error(), "Missing inputs"): + + // This error is returned when broadcasting a transaction that has + // already confirmed to a bitcoind node. + case strings.Contains(err.Error(), "txn-already-known"): fallthrough - case strings.Contains(err.Error(), "already in block chain"): - // If the transaction was rejected, then we'll remove it from - // the txstore, as otherwise, we'll attempt to continually - // re-broadcast it, and the utxo state of the wallet won't be - // accurate. + + // This error is returned when sending a transaction that has already + // confirmed to a bitcoind node over RPC. + case strings.Contains(err.Error(), "transaction already in block chain"): dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) @@ -3471,13 +3486,33 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) }) if dbErr != nil { - return nil, fmt.Errorf("unable to broadcast tx: %v, "+ - "unable to remove invalid tx: %v", err, dbErr) + log.Warnf("Unable to remove confirmed transaction %v "+ + "from unconfirmed store: %v", tx.TxHash(), dbErr) } - return nil, err + return txid, nil + // If the transaction was rejected for whatever other reason, then we'll + // remove it from the transaction store, as otherwise, we'll attempt to + // continually re-broadcast it, and the UTXO state of the wallet won't + // be accurate. default: + dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { + txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) + txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) + if err != nil { + return err + } + return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) + }) + if dbErr != nil { + log.Warnf("Unable to remove invalid transaction %v: %v", + tx.TxHash(), dbErr) + } else { + log.Infof("Removed invalid transaction: %v", + spew.Sdump(tx)) + } + return nil, err } }