lbcwallet/wallet/chainntfns.go
Wilmer Paulino aca9bebfab
wallet/chainntfns: avoid notifying txs if not found within the wallet
In this commit, we avoid notifying clients of transactions that we've
received chain.RelevantTx notifications for, but are not found within
the wallet. This can happen as now we'll prevent adding an unconfirmed
transaction to the wallet that already exists as confirmed. Due to this,
UniqueTxDetails will be unable to find the transaction and return nil,
casuing a panic for potential callers.
2018-09-06 19:42:13 -07:00

332 lines
9.3 KiB
Go

// Copyright (c) 2013-2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package wallet
import (
"bytes"
"strings"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
func (w *Wallet) handleChainNotifications() {
defer w.wg.Done()
chainClient, err := w.requireChainClient()
if err != nil {
log.Errorf("handleChainNotifications called without RPC client")
return
}
sync := func(w *Wallet) {
// At the moment there is no recourse if the rescan fails for
// some reason, however, the wallet will not be marked synced
// and many methods will error early since the wallet is known
// to be out of date.
err := w.syncWithChain()
if err != nil && !w.ShuttingDown() {
log.Warnf("Unable to synchronize wallet to chain: %v", err)
}
}
catchUpHashes := func(w *Wallet, client chain.Interface,
height int32) error {
// TODO(aakselrod): There's a race conditon here, which
// happens when a reorg occurs between the
// rescanProgress notification and the last GetBlockHash
// call. The solution when using btcd is to make btcd
// send blockconnected notifications with each block
// the way Neutrino does, and get rid of the loop. The
// other alternative is to check the final hash and,
// if it doesn't match the original hash returned by
// the notification, to roll back and restart the
// rescan.
log.Infof("Catching up block hashes to height %d, this"+
" might take a while", height)
err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
startBlock := w.Manager.SyncedTo()
for i := startBlock.Height + 1; i <= height; i++ {
hash, err := client.GetBlockHash(int64(i))
if err != nil {
return err
}
header, err := chainClient.GetBlockHeader(hash)
if err != nil {
return err
}
bs := waddrmgr.BlockStamp{
Height: i,
Hash: *hash,
Timestamp: header.Timestamp,
}
err = w.Manager.SetSyncedTo(ns, &bs)
if err != nil {
return err
}
}
return nil
})
if err != nil {
log.Errorf("Failed to update address manager "+
"sync state for height %d: %v", height, err)
}
log.Info("Done catching up block hashes")
return err
}
for {
select {
case n, ok := <-chainClient.Notifications():
if !ok {
return
}
var notificationName string
var err error
switch n := n.(type) {
case chain.ClientConnected:
go sync(w)
case chain.BlockConnected:
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
return w.connectBlock(tx, wtxmgr.BlockMeta(n))
})
notificationName = "blockconnected"
case chain.BlockDisconnected:
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
return w.disconnectBlock(tx, wtxmgr.BlockMeta(n))
})
notificationName = "blockdisconnected"
case chain.RelevantTx:
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
return w.addRelevantTx(tx, n.TxRecord, n.Block)
})
notificationName = "recvtx/redeemingtx"
case chain.FilteredBlockConnected:
// Atomically update for the whole block.
if len(n.RelevantTxs) > 0 {
err = walletdb.Update(w.db, func(
tx walletdb.ReadWriteTx) error {
var err error
for _, rec := range n.RelevantTxs {
err = w.addRelevantTx(tx, rec,
n.Block)
if err != nil {
return err
}
}
return nil
})
}
notificationName = "filteredblockconnected"
// The following require some database maintenance, but also
// need to be reported to the wallet's rescan goroutine.
case *chain.RescanProgress:
err = catchUpHashes(w, chainClient, n.Height)
notificationName = "rescanprogress"
select {
case w.rescanNotifications <- n:
case <-w.quitChan():
return
}
case *chain.RescanFinished:
err = catchUpHashes(w, chainClient, n.Height)
notificationName = "rescanprogress"
w.SetChainSynced(true)
select {
case w.rescanNotifications <- n:
case <-w.quitChan():
return
}
}
if err != nil {
// On out-of-sync blockconnected notifications, only
// send a debug message.
errStr := "Failed to process consensus server " +
"notification (name: `%s`, detail: `%v`)"
if notificationName == "blockconnected" &&
strings.Contains(err.Error(),
"couldn't get hash from database") {
log.Debugf(errStr, notificationName, err)
} else {
log.Errorf(errStr, notificationName, err)
}
}
case <-w.quit:
return
}
}
}
// connectBlock handles a chain server notification by marking a wallet
// that's currently in-sync with the chain server as being synced up to
// the passed block.
func (w *Wallet) connectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error {
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
bs := waddrmgr.BlockStamp{
Height: b.Height,
Hash: b.Hash,
Timestamp: b.Time,
}
err := w.Manager.SetSyncedTo(addrmgrNs, &bs)
if err != nil {
return err
}
// Notify interested clients of the connected block.
//
// TODO: move all notifications outside of the database transaction.
w.NtfnServer.notifyAttachedBlock(dbtx, &b)
return nil
}
// disconnectBlock handles a chain server reorganize by rolling back all
// block history from the reorged block for a wallet in-sync with the chain
// server.
func (w *Wallet) disconnectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error {
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey)
if !w.ChainSynced() {
return nil
}
// Disconnect the removed block and all blocks after it if we know about
// the disconnected block. Otherwise, the block is in the future.
if b.Height <= w.Manager.SyncedTo().Height {
hash, err := w.Manager.BlockHash(addrmgrNs, b.Height)
if err != nil {
return err
}
if bytes.Equal(hash[:], b.Hash[:]) {
bs := waddrmgr.BlockStamp{
Height: b.Height - 1,
}
hash, err = w.Manager.BlockHash(addrmgrNs, bs.Height)
if err != nil {
return err
}
b.Hash = *hash
client := w.ChainClient()
header, err := client.GetBlockHeader(hash)
if err != nil {
return err
}
bs.Timestamp = header.Timestamp
err = w.Manager.SetSyncedTo(addrmgrNs, &bs)
if err != nil {
return err
}
err = w.TxStore.Rollback(txmgrNs, b.Height)
if err != nil {
return err
}
}
}
// Notify interested clients of the disconnected block.
w.NtfnServer.notifyDetachedBlock(&b.Hash)
return nil
}
func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error {
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey)
// At the moment all notified transactions are assumed to actually be
// relevant. This assumption will not hold true when SPV support is
// added, but until then, simply insert the transaction because there
// should either be one or more relevant inputs or outputs.
err := w.TxStore.InsertTx(txmgrNs, rec, block)
if err != nil {
return err
}
// Check every output to determine whether it is controlled by a wallet
// key. If so, mark the output as a credit.
for i, output := range rec.MsgTx.TxOut {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript,
w.chainParams)
if err != nil {
// Non-standard outputs are skipped.
continue
}
for _, addr := range addrs {
ma, err := w.Manager.Address(addrmgrNs, addr)
if err == nil {
// TODO: Credits should be added with the
// account they belong to, so wtxmgr is able to
// track per-account balances.
err = w.TxStore.AddCredit(txmgrNs, rec, block, uint32(i),
ma.Internal())
if err != nil {
return err
}
err = w.Manager.MarkUsed(addrmgrNs, addr)
if err != nil {
return err
}
log.Debugf("Marked address %v used", addr)
continue
}
// Missing addresses are skipped. Other errors should
// be propagated.
if !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
return err
}
}
}
// Send notification of mined or unmined transaction to any interested
// clients.
//
// TODO: Avoid the extra db hits.
if block == nil {
details, err := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, nil)
if err != nil {
log.Errorf("Cannot query transaction details for notification: %v", err)
}
// It's possible that the transaction was not found within the
// wallet's set of unconfirmed transactions due to it already
// being confirmed, so we'll avoid notifying it.
//
// TODO(wilmer): ideally we should find the culprit to why we're
// receiving an additional unconfirmed chain.RelevantTx
// notification from the chain backend.
if details != nil {
w.NtfnServer.notifyUnminedTransaction(dbtx, details)
}
} else {
details, err := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, &block.Block)
if err != nil {
log.Errorf("Cannot query transaction details for notification: %v", err)
}
// We'll only notify the transaction if it was found within the
// wallet's set of confirmed transactions.
if details != nil {
w.NtfnServer.notifyMinedTransaction(dbtx, details, block)
}
}
return nil
}