lbcwallet/ntfns.go
Josh Rickmar 2e76bcd159 Handle out-of-order notifications from btcd.
Notifications ariving from btcd were being reordered (each handled by
its own goroutine, rather then being always sent in the order they
originated).  This was breaking the new transaction store by inserting
transaction records in an 'impossible' manner, that is, inserting txs
without block info after the store already held records of the same tx
with block info, without first performing a rollback.

This is handled by the transaction store insert methods by checking
for identical transactions (double spends with the same tx sha), but
where the block heights mismatch and the new record does not have a
block set.  The error is returned all the way up to the goroutine
running each rpc request/notification handler, and if hit, the btcd
connection is closed and all accounts are reopened from disk.  This is
not optimal, but it allows us to use the connect logic to correctly
catch us up to the best chain with the last good state of all accounts
while only rescanning a few blocks.

Fixes #72.
2014-02-28 15:43:50 -05:00

265 lines
8.1 KiB
Go

/*
* Copyright (c) 2013, 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// This file implements the notification handlers for btcd-side notifications.
package main
import (
"encoding/hex"
"fmt"
"sync"
"time"
"github.com/conformal/btcjson"
"github.com/conformal/btcscript"
"github.com/conformal/btcutil"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire"
"github.com/conformal/btcws"
)
func parseBlock(block *btcws.BlockDetails) (*tx.BlockDetails, error) {
if block == nil {
return nil, nil
}
blksha, err := btcwire.NewShaHashFromStr(block.Hash)
if err != nil {
return nil, err
}
return &tx.BlockDetails{
Height: block.Height,
Hash: *blksha,
Index: int32(block.Index),
Time: time.Unix(block.Time, 0),
}, nil
}
type notificationHandler func(btcjson.Cmd) error
var notificationHandlers = map[string]notificationHandler{
btcws.BlockConnectedNtfnMethod: NtfnBlockConnected,
btcws.BlockDisconnectedNtfnMethod: NtfnBlockDisconnected,
btcws.RecvTxNtfnMethod: NtfnRecvTx,
btcws.RedeemingTxNtfnMethod: NtfnRedeemingTx,
}
// NtfnRecvTx handles the btcws.RecvTxNtfn notification.
func NtfnRecvTx(n btcjson.Cmd) error {
rtx, ok := n.(*btcws.RecvTxNtfn)
if !ok {
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
bs, err := GetCurBlock()
if err != nil {
return fmt.Errorf("%v handler: cannot get current block: %v", n.Method(), err)
}
rawTx, err := hex.DecodeString(rtx.HexTx)
if err != nil {
return fmt.Errorf("%v handler: bad hexstring: err", n.Method(), err)
}
tx_, err := btcutil.NewTxFromBytes(rawTx)
if err != nil {
return fmt.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err)
}
var block *tx.BlockDetails
if rtx.Block != nil {
block, err = parseBlock(rtx.Block)
if err != nil {
return fmt.Errorf("%v handler: bad block: %v", n.Method(), err)
}
}
// For transactions originating from this wallet, the sent tx history should
// be recorded before the received history. If wallet created this tx, wait
// for the sent history to finish being recorded before continuing.
//
// TODO(jrick) this is wrong due to tx malleability. Cannot safely use the
// txsha as an identifier.
req := SendTxHistSyncRequest{
txsha: *tx_.Sha(),
response: make(chan SendTxHistSyncResponse),
}
SendTxHistSyncChans.access <- req
resp := <-req.response
if resp.ok {
// Wait until send history has been recorded.
<-resp.c
SendTxHistSyncChans.remove <- *tx_.Sha()
}
// For every output, find all accounts handling that output address (if any)
// and record the received txout.
for outIdx, txout := range tx_.MsgTx().TxOut {
var accounts []*Account
var received time.Time
_, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, cfg.Net())
for _, addr := range addrs {
aname, err := LookupAccountByAddress(addr.EncodeAddress())
if err == ErrNotFound {
continue
}
// This cannot reasonably fail if the above succeeded.
a, _ := AcctMgr.Account(aname)
accounts = append(accounts, a)
if block != nil {
received = block.Time
} else {
received = time.Now()
}
}
for _, a := range accounts {
record, err := a.TxStore.InsertRecvTxOut(tx_, uint32(outIdx), false, received, block)
if err != nil {
return err
}
AcctMgr.ds.ScheduleTxStoreWrite(a)
// Notify frontends of tx. If the tx is unconfirmed, it is always
// notified and the outpoint is marked as notified. If the outpoint
// has already been notified and is now in a block, a txmined notifiction
// should be sent once to let frontends that all previous send/recvs
// for this unconfirmed tx are now confirmed.
recvTxOP := btcwire.NewOutPoint(tx_.Sha(), uint32(outIdx))
previouslyNotifiedReq := NotifiedRecvTxRequest{
op: *recvTxOP,
response: make(chan NotifiedRecvTxResponse),
}
NotifiedRecvTxChans.access <- previouslyNotifiedReq
if <-previouslyNotifiedReq.response {
NotifiedRecvTxChans.remove <- *recvTxOP
} else {
// Notify frontends of new recv tx and mark as notified.
NotifiedRecvTxChans.add <- *recvTxOP
// need access to the RecvTxOut to get the json info object
NotifyNewTxDetails(allClients, a.Name(),
record.TxInfo(a.Name(), bs.Height, a.Wallet.Net())[0])
}
// Notify frontends of new account balance.
confirmed := a.CalculateBalance(1)
unconfirmed := a.CalculateBalance(0) - confirmed
NotifyWalletBalance(allClients, a.name, confirmed)
NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed)
}
}
return nil
}
// NtfnBlockConnected handles btcd notifications resulting from newly
// connected blocks to the main blockchain.
//
// TODO(jrick): Send block time with notification. This will be used
// to mark wallet files with a possibly-better earliest block height,
// and will greatly reduce rescan times for wallets created with an
// out of sync btcd.
func NtfnBlockConnected(n btcjson.Cmd) error {
bcn, ok := n.(*btcws.BlockConnectedNtfn)
if !ok {
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
hash, err := btcwire.NewShaHashFromStr(bcn.Hash)
if err != nil {
return fmt.Errorf("%v handler: invalid hash string", n.Method())
}
// Update the blockstamp for the newly-connected block.
bs := &wallet.BlockStamp{
Height: bcn.Height,
Hash: *hash,
}
curBlock.Lock()
curBlock.BlockStamp = *bs
curBlock.Unlock()
// btcd notifies btcwallet about transactions first, and then sends
// the new block notification. New balance notifications for txs
// in blocks are therefore sent here after all tx notifications
// have arrived and finished being processed by the handlers.
workers := NotifyBalanceRequest{
block: *hash,
wg: make(chan *sync.WaitGroup),
}
NotifyBalanceSyncerChans.access <- workers
if wg := <-workers.wg; wg != nil {
wg.Wait()
NotifyBalanceSyncerChans.remove <- *hash
}
AcctMgr.BlockNotify(bs)
// Pass notification to frontends too.
marshaled, _ := n.MarshalJSON()
allClients <- marshaled
return nil
}
// NtfnBlockDisconnected handles btcd notifications resulting from
// blocks disconnected from the main chain in the event of a chain
// switch and notifies frontends of the new blockchain height.
func NtfnBlockDisconnected(n btcjson.Cmd) error {
bdn, ok := n.(*btcws.BlockDisconnectedNtfn)
if !ok {
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
hash, err := btcwire.NewShaHashFromStr(bdn.Hash)
if err != nil {
return fmt.Errorf("%v handler: invalid hash string", n.Method())
}
// Rollback Utxo and Tx data stores.
AcctMgr.Rollback(bdn.Height, hash)
// Pass notification to frontends too.
marshaled, _ := n.MarshalJSON()
allClients <- marshaled
return nil
}
// NtfnRedeemingTx handles btcd redeemingtx notifications resulting from a
// transaction spending a watched outpoint.
func NtfnRedeemingTx(n btcjson.Cmd) error {
cn, ok := n.(*btcws.RedeemingTxNtfn)
if !ok {
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
rawTx, err := hex.DecodeString(cn.HexTx)
if err != nil {
return fmt.Errorf("%v handler: bad hexstring: err", n.Method(), err)
}
tx_, err := btcutil.NewTxFromBytes(rawTx)
if err != nil {
return fmt.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err)
}
block, err := parseBlock(cn.Block)
if err != nil {
return fmt.Errorf("%v handler: bad block: %v", n.Method(), err)
}
AcctMgr.RecordSpendingTx(tx_, block)
return nil
}