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.
This commit is contained in:
Josh Rickmar 2014-02-28 13:03:23 -05:00
parent 76c6379a54
commit 2e76bcd159
7 changed files with 296 additions and 147 deletions

View file

@ -43,6 +43,7 @@ type AccountManager struct {
// binary semaphore channel to prevent incorrect access.
bsem chan struct{}
openAccounts chan struct{}
accessAccount chan *accessAccountRequest
accessAll chan *accessAllRequest
add chan *Account
@ -55,6 +56,7 @@ type AccountManager struct {
func NewAccountManager() *AccountManager {
am := &AccountManager{
bsem: make(chan struct{}, 1),
openAccounts: make(chan struct{}, 1),
accessAccount: make(chan *accessAccountRequest),
accessAll: make(chan *accessAllRequest),
add: make(chan *Account),
@ -81,6 +83,20 @@ func (am *AccountManager) Start() {
for {
select {
case <-am.openAccounts:
// Write all old accounts before proceeding.
for e := l.Front(); e != nil; e = e.Next() {
a := e.Value.(*Account)
am.ds.FlushAccount(a)
}
m = OpenAccounts()
l.Init()
for _, a := range m {
l.PushBack(a)
}
case access := <-am.accessAccount:
a, ok := m[access.name]
access.resp <- &accessAccountResponse{
@ -129,6 +145,10 @@ func (am *AccountManager) Release() {
am.bsem <- struct{}{}
}
func (am *AccountManager) OpenAccounts() {
am.openAccounts <- struct{}{}
}
type accessAccountRequest struct {
name string
resp chan *accessAccountResponse
@ -196,7 +216,7 @@ func (am *AccountManager) RegisterNewAccount(a *Account) error {
// Rollback rolls back each managed Account to the state before the block
// specified by height and hash was connected to the main chain.
func (am *AccountManager) Rollback(height int32, hash *btcwire.ShaHash) {
log.Debugf("Rolling back tx history since block height %v", height)
log.Infof("Rolling back tx history since block height %v", height)
for _, a := range am.AllAccounts() {
a.TxStore.Rollback(height)
@ -204,13 +224,6 @@ func (am *AccountManager) Rollback(height int32, hash *btcwire.ShaHash) {
}
}
// Rollback reverts each stored Account to a state before the block
// with the passed chainheight and block hash was connected to the main
// chain. This is used to remove transactions and utxos for each wallet
// that occured on a chain no longer considered to be the main chain.
func (a *Account) Rollback(height int32, hash *btcwire.ShaHash) {
}
// BlockNotify notifies all frontends of any changes from the new block,
// including changed balances. Each account is then set to be synced
// with the latest block.

28
cmd.go
View file

@ -146,10 +146,9 @@ func main() {
// Check and update any old file locations.
updateOldFileLocations()
// Start account manager and open accounts.
go AcctMgr.Start()
// Open all account saved to disk.
OpenAccounts()
AcctMgr.OpenAccounts()
// Read CA file to verify a btcd TLS connection.
cafile, err := ioutil.ReadFile(cfg.CAFile)
@ -323,7 +322,7 @@ func OpenSavedAccount(name string, cfg *config) (*Account, error) {
}
// OpenAccounts attempts to open all saved accounts.
func OpenAccounts() {
func OpenAccounts() map[string]*Account {
// If the network (account) directory is missing, but the temporary
// directory exists, move it. This is unlikely to happen, but possible,
// if writing out every account file at once to a tmp directory (as is
@ -335,7 +334,7 @@ func OpenAccounts() {
if !fileExists(netDir) && fileExists(tmpNetDir) {
if err := Rename(tmpNetDir, netDir); err != nil {
log.Errorf("Cannot move temporary network dir: %v", err)
return
return nil
}
}
@ -346,13 +345,15 @@ func OpenAccounts() {
switch err.(type) {
case *WalletOpenError:
log.Errorf("Default account wallet file unreadable: %v", err)
return
return nil
default:
log.Warnf("Non-critical problem opening an account file: %v", err)
}
}
AcctMgr.AddAccount(a)
accounts := map[string]*Account{
"": a,
}
// Read all filenames in the account directory, and look for any
// filenames matching '*-wallet.bin'. These are wallets for
@ -361,7 +362,7 @@ func OpenAccounts() {
if err != nil {
// Can't continue.
log.Errorf("Unable to open account directory: %v", err)
return
return nil
}
defer accountDir.Close()
fileNames, err := accountDir.Readdirnames(0)
@ -370,20 +371,20 @@ func OpenAccounts() {
// at least try to open some accounts.
log.Errorf("Unable to read all account files: %v", err)
}
var accounts []string
var accountNames []string
for _, file := range fileNames {
if strings.HasSuffix(file, "-wallet.bin") {
name := strings.TrimSuffix(file, "-wallet.bin")
accounts = append(accounts, name)
accountNames = append(accountNames, name)
}
}
// Open all additional accounts.
for _, a := range accounts {
for _, acctName := range accountNames {
// Log txstore/utxostore errors as these will be recovered
// from with a rescan, but wallet errors must be returned
// to the caller.
a, err := OpenSavedAccount(a, cfg)
a, err := OpenSavedAccount(acctName, cfg)
if err != nil {
switch err.(type) {
case *WalletOpenError:
@ -393,9 +394,10 @@ func OpenAccounts() {
log.Warnf("Non-critical error opening an account file: %v", err)
}
} else {
AcctMgr.AddAccount(a)
accounts[acctName] = a
}
}
return accounts
}
var accessServer = make(chan *AccessCurrentServerConn)

View file

@ -20,6 +20,10 @@ package main
import (
"encoding/hex"
"fmt"
"sync"
"time"
"github.com/conformal/btcjson"
"github.com/conformal/btcscript"
"github.com/conformal/btcutil"
@ -27,8 +31,6 @@ import (
"github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire"
"github.com/conformal/btcws"
"sync"
"time"
)
func parseBlock(block *btcws.BlockDetails) (*tx.BlockDetails, error) {
@ -47,7 +49,7 @@ func parseBlock(block *btcws.BlockDetails) (*tx.BlockDetails, error) {
}, nil
}
type notificationHandler func(btcjson.Cmd)
type notificationHandler func(btcjson.Cmd) error
var notificationHandlers = map[string]notificationHandler{
btcws.BlockConnectedNtfnMethod: NtfnBlockConnected,
@ -57,36 +59,31 @@ var notificationHandlers = map[string]notificationHandler{
}
// NtfnRecvTx handles the btcws.RecvTxNtfn notification.
func NtfnRecvTx(n btcjson.Cmd) {
func NtfnRecvTx(n btcjson.Cmd) error {
rtx, ok := n.(*btcws.RecvTxNtfn)
if !ok {
log.Errorf("%v handler: unexpected type", n.Method())
return
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
bs, err := GetCurBlock()
if err != nil {
log.Errorf("%v handler: cannot get current block: %v", n.Method(), err)
return
return fmt.Errorf("%v handler: cannot get current block: %v", n.Method(), err)
}
rawTx, err := hex.DecodeString(rtx.HexTx)
if err != nil {
log.Errorf("%v handler: bad hexstring: err", n.Method(), err)
return
return fmt.Errorf("%v handler: bad hexstring: err", n.Method(), err)
}
tx_, err := btcutil.NewTxFromBytes(rawTx)
if err != nil {
log.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err)
return
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 {
log.Errorf("%v handler: bad block: %v", n.Method(), err)
return
return fmt.Errorf("%v handler: bad block: %v", n.Method(), err)
}
}
@ -131,7 +128,10 @@ func NtfnRecvTx(n btcjson.Cmd) {
}
for _, a := range accounts {
record := a.TxStore.InsertRecvTxOut(tx_, uint32(outIdx), false, received, block)
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
@ -163,6 +163,8 @@ func NtfnRecvTx(n btcjson.Cmd) {
NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed)
}
}
return nil
}
// NtfnBlockConnected handles btcd notifications resulting from newly
@ -172,16 +174,14 @@ func NtfnRecvTx(n btcjson.Cmd) {
// 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) {
func NtfnBlockConnected(n btcjson.Cmd) error {
bcn, ok := n.(*btcws.BlockConnectedNtfn)
if !ok {
log.Errorf("%v handler: unexpected type", n.Method())
return
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
hash, err := btcwire.NewShaHashFromStr(bcn.Hash)
if err != nil {
log.Errorf("%v handler: invalid hash string", n.Method())
return
return fmt.Errorf("%v handler: invalid hash string", n.Method())
}
// Update the blockstamp for the newly-connected block.
@ -211,21 +211,21 @@ func NtfnBlockConnected(n btcjson.Cmd) {
// 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) {
func NtfnBlockDisconnected(n btcjson.Cmd) error {
bdn, ok := n.(*btcws.BlockDisconnectedNtfn)
if !ok {
log.Errorf("%v handler: unexpected type", n.Method())
return
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
hash, err := btcwire.NewShaHashFromStr(bdn.Hash)
if err != nil {
log.Errorf("%v handler: invalid hash string", n.Method())
return
return fmt.Errorf("%v handler: invalid hash string", n.Method())
}
// Rollback Utxo and Tx data stores.
@ -234,32 +234,32 @@ func NtfnBlockDisconnected(n btcjson.Cmd) {
// 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) {
func NtfnRedeemingTx(n btcjson.Cmd) error {
cn, ok := n.(*btcws.RedeemingTxNtfn)
if !ok {
log.Errorf("%v handler: unexpected type", n.Method())
return
return fmt.Errorf("%v handler: unexpected type", n.Method())
}
rawTx, err := hex.DecodeString(cn.HexTx)
if err != nil {
log.Errorf("%v handler: bad hexstring: err", n.Method(), err)
return
return fmt.Errorf("%v handler: bad hexstring: err", n.Method(), err)
}
tx_, err := btcutil.NewTxFromBytes(rawTx)
if err != nil {
log.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err)
return
return fmt.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err)
}
block, err := parseBlock(cn.Block)
if err != nil {
log.Errorf("%v handler: bad block: %v", n.Method(), err)
return
return fmt.Errorf("%v handler: bad block: %v", n.Method(), err)
}
AcctMgr.RecordSpendingTx(tx_, block)
return nil
}

View file

@ -110,6 +110,15 @@ func (btcd *BtcdRPCConn) Connected() bool {
}
}
// Close forces closing the current btcd connection.
func (btcd *BtcdRPCConn) Close() {
select {
case <-btcd.closed:
default:
close(btcd.closed)
}
}
// AddRPCRequest is used to add an RPCRequest to the pool of requests
// being manaaged by a btcd RPC connection.
type AddRPCRequest struct {

View file

@ -207,8 +207,31 @@ func WalletRequestProcessor() {
case n := <-handleNtfn:
if f, ok := notificationHandlers[n.Method()]; ok {
AcctMgr.Grab()
f(n)
err := f(n)
AcctMgr.Release()
switch err {
case nil:
// ignore
case tx.ErrInconsistantStore:
// Likely due to a mis-ordered btcd notification.
// To recover, close server connection and reopen
// all accounts from their last good state saved
// to disk. This will trigger the handshake on
// next connect, and a rescan of one or two blocks
// to catch up rather than throwing away all tx
// history and rescanning everything.
s := CurrentServerConn()
if btcd, ok := s.(*BtcdRPCConn); ok {
AcctMgr.Grab()
btcd.Close()
AcctMgr.OpenAccounts()
AcctMgr.Release()
}
default: // other non-nil
log.Warn(err)
}
}
}
}
@ -1341,7 +1364,11 @@ func SendBeforeReceiveHistorySync(add, done, remove chan btcwire.ShaHash,
func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo *CreatedTx) (interface{}, *btcjson.Error) {
// Add to transaction store.
stx := a.TxStore.InsertSignedTx(txInfo.tx, nil)
stx, err := a.TxStore.InsertSignedTx(txInfo.tx, nil)
if err != nil {
log.Warnf("Error adding sent tx history: %v", err)
return nil, &btcjson.ErrInternal
}
AcctMgr.ds.ScheduleTxStoreWrite(a)
// Notify frontends of new SendTx.

View file

@ -42,6 +42,10 @@ var (
// object is marked with a version that is no longer supported
// during deserialization.
ErrUnsupportedVersion = errors.New("version no longer supported")
// ErrInconsistantStore represents an error for when an inconsistancy
// is detected during inserting or returning transaction records.
ErrInconsistantStore = errors.New("inconsistant transaction store")
)
// Record is a common interface shared by SignedTx and RecvTxOut transaction
@ -264,7 +268,7 @@ func (s *Store) ReadFrom(r io.Reader) (int64, error) {
// It is an error for the backing transaction to have
// not already been read.
if _, ok := s.txs[rtx.blockTx()]; !ok {
return n64, errors.New("missing backing transaction")
return n64, ErrInconsistantStore
}
// Add entries to store.
@ -290,7 +294,7 @@ func (s *Store) ReadFrom(r io.Reader) (int64, error) {
// It is an error for the backing transaction to have
// not already been read.
if _, ok := s.txs[stx.blockTx()]; !ok {
return n64, errors.New("missing backing transaction")
return n64, ErrInconsistantStore
}
// Add entries to store.
@ -371,7 +375,7 @@ func (s *Store) WriteTo(w io.Writer) (int64, error) {
// store, returning the record. Duplicates and double spend correction is
// handled automatically. Transactions may be added without block details,
// and later added again with block details once the tx has been mined.
func (s *Store) InsertSignedTx(tx *btcutil.Tx, block *BlockDetails) *SignedTx {
func (s *Store) InsertSignedTx(tx *btcutil.Tx, block *BlockDetails) (*SignedTx, error) {
var created time.Time
if block == nil {
created = time.Now()
@ -387,8 +391,11 @@ func (s *Store) InsertSignedTx(tx *btcutil.Tx, block *BlockDetails) *SignedTx {
block: block,
}
s.insertTx(tx, st)
return st.record(s).(*SignedTx)
err := s.insertTx(tx, st)
if err != nil {
return nil, ErrInconsistantStore
}
return st.record(s).(*SignedTx), nil
}
// Rollback removes block details for all transactions at or beyond a
@ -398,33 +405,36 @@ func (s *Store) InsertSignedTx(tx *btcutil.Tx, block *BlockDetails) *SignedTx {
// chain are added to the store.
func (s *Store) Rollback(height int32) {
for e := s.sorted.Front(); e != nil; e = e.Next() {
tx := e.Value.(txRecord)
if details := tx.Block(); details != nil {
txSha := tx.TxSha()
oldKey := blockTx{*txSha, details.Height}
if details.Height >= height {
tx.setBlock(nil)
record := e.Value.(txRecord)
block := record.Block()
if block == nil {
// Unmined, no block details to remove.
continue
}
txSha := record.TxSha()
if block.Height >= height {
oldKey := blockTx{*txSha, block.Height}
record.setBlock(nil)
switch v := tx.(type) {
case *signedTx:
k := oldKey
delete(s.signed, k)
k.height = -1
s.signed[k] = v
switch v := record.(type) {
case *signedTx:
k := oldKey
delete(s.signed, k)
k.height = -1
s.signed[k] = v
case *recvTxOut:
k := blockOutPoint{v.outpoint, details.Height}
delete(s.recv, k)
k.height = -1
s.recv[k] = v
}
case *recvTxOut:
k := blockOutPoint{v.outpoint, block.Height}
delete(s.recv, k)
k.height = -1
s.recv[k] = v
}
if utx, ok := s.txs[oldKey]; ok {
k := oldKey
delete(s.txs, k)
k.height = -1
s.txs[k] = utx
}
if utx, ok := s.txs[oldKey]; ok {
k := oldKey
delete(s.txs, k)
k.height = -1
s.txs[k] = utx
}
}
}
@ -449,7 +459,7 @@ func (s *Store) UnminedSignedTxs() []*btcutil.Tx {
// with non-nil BlockDetails to update the record and all other records
// using the transaction with the block.
func (s *Store) InsertRecvTxOut(tx *btcutil.Tx, outIdx uint32,
change bool, received time.Time, block *BlockDetails) *RecvTxOut {
change bool, received time.Time, block *BlockDetails) (*RecvTxOut, error) {
rt := &recvTxOut{
outpoint: *btcwire.NewOutPoint(tx.Sha(), outIdx),
@ -457,17 +467,27 @@ func (s *Store) InsertRecvTxOut(tx *btcutil.Tx, outIdx uint32,
received: received,
block: block,
}
s.insertTx(tx, rt)
return rt.record(s).(*RecvTxOut)
err := s.insertTx(tx, rt)
if err != nil {
return nil, err
}
return rt.record(s).(*RecvTxOut), nil
}
func (s *Store) insertTx(utx *btcutil.Tx, record txRecord) {
func (s *Store) insertTx(utx *btcutil.Tx, record txRecord) error {
if ds := s.findDoubleSpend(utx); ds != nil {
switch {
case ds.txSha == *utx.Sha(): // identical tx
if ds.height != record.Height() {
s.setTxBlock(utx.Sha(), record.Block())
return
// Detect insert inconsistancies. If matching
// tx was found, but this record's block is unset,
// a rollback was missed.
block := record.Block()
if block == nil {
return ErrInconsistantStore
}
s.setTxBlock(utx.Sha(), block)
return nil
}
default:
@ -479,6 +499,7 @@ func (s *Store) insertTx(utx *btcutil.Tx, record txRecord) {
}
s.insertUniqueTx(utx, record)
return nil
}
func (s *Store) insertUniqueTx(utx *btcutil.Tx, record txRecord) {
@ -605,11 +626,6 @@ func (s *Store) removeDoubleSpends(oldKey *blockTx) {
}
func (s *Store) setTxBlock(txSha *btcwire.ShaHash, block *BlockDetails) {
if block == nil {
// Nothing to update.
return
}
// Lookup unmined backing tx.
prevKey := blockTx{*txSha, -1}
tx := s.txs[prevKey]

View file

@ -78,16 +78,18 @@ func TestTxStore(t *testing.T) {
tests := []struct {
name string
f func(*Store) *Store
f func(*Store) (*Store, error)
err error
bal, unc int64
unspents map[btcwire.OutPoint]struct{}
unmined map[btcwire.ShaHash]struct{}
}{
{
name: "new store",
f: func(_ *Store) *Store {
return NewStore()
f: func(_ *Store) (*Store, error) {
return NewStore(), nil
},
err: nil,
bal: 0,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{},
@ -95,10 +97,36 @@ func TestTxStore(t *testing.T) {
},
{
name: "txout insert",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil)
if err != nil {
return nil, err
}
// If the above succeeded, try using the record. This will
// dereference the tx and panic if the above didn't catch
// an inconsistant insert.
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: 0,
unc: TstRecvTx.MsgTx().TxOut[0].Value,
unspents: map[btcwire.OutPoint]struct{}{
*btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{},
},
unmined: map[btcwire.ShaHash]struct{}{},
},
{
name: "insert duplicate unconfirmed",
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: 0,
unc: TstRecvTx.MsgTx().TxOut[0].Value,
unspents: map[btcwire.OutPoint]struct{}{
@ -108,10 +136,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "confirmed txout insert",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: TstRecvTx.MsgTx().TxOut[0].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
@ -121,10 +154,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert duplicate confirmed",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: TstRecvTx.MsgTx().TxOut[0].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
@ -134,23 +172,27 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert duplicate unconfirmed",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
bal: TstRecvTx.MsgTx().TxOut[0].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
*btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{},
},
unmined: map[btcwire.ShaHash]struct{}{},
err: ErrInconsistantStore,
},
{
name: "insert double spend with new txout value",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstDoubleSpendTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstDoubleSpendTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: TstDoubleSpendTx.MsgTx().TxOut[0].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
@ -160,10 +202,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert unconfirmed signed tx",
f: func(s *Store) *Store {
s.InsertSignedTx(TstSpendingTx, nil)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertSignedTx(TstSpendingTx, nil)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: 0,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{},
@ -173,10 +220,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert unconfirmed signed tx again",
f: func(s *Store) *Store {
s.InsertSignedTx(TstSpendingTx, nil)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertSignedTx(TstSpendingTx, nil)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: 0,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{},
@ -186,10 +238,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert change (index 0)",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstSpendingTx, 0, true, time.Now(), nil)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstSpendingTx, 0, true, time.Now(), nil)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: 0,
unc: TstSpendingTx.MsgTx().TxOut[0].Value,
unspents: map[btcwire.OutPoint]struct{}{
@ -201,10 +258,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert output back to this own wallet (index 1)",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstSpendingTx, 1, true, time.Now(), nil)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstSpendingTx, 1, true, time.Now(), nil)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: 0,
unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
unspents: map[btcwire.OutPoint]struct{}{
@ -217,10 +279,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "confirmed signed tx",
f: func(s *Store) *Store {
s.InsertSignedTx(TstSpendingTx, TstSignedTxBlockDetails)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertSignedTx(TstSpendingTx, TstSignedTxBlockDetails)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
@ -231,10 +298,11 @@ func TestTxStore(t *testing.T) {
},
{
name: "rollback after spending tx",
f: func(s *Store) *Store {
f: func(s *Store) (*Store, error) {
s.Rollback(TstSignedTxBlockDetails.Height + 1)
return s
return s, nil
},
err: nil,
bal: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
@ -245,10 +313,11 @@ func TestTxStore(t *testing.T) {
},
{
name: "rollback spending tx block",
f: func(s *Store) *Store {
f: func(s *Store) (*Store, error) {
s.Rollback(TstSignedTxBlockDetails.Height)
return s
return s, nil
},
err: nil,
bal: 0,
unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
unspents: map[btcwire.OutPoint]struct{}{
@ -261,10 +330,11 @@ func TestTxStore(t *testing.T) {
},
{
name: "rollback double spend tx block",
f: func(s *Store) *Store {
f: func(s *Store) (*Store, error) {
s.Rollback(TstRecvTxBlockDetails.Height)
return s
return s, nil
},
err: nil,
bal: 0,
unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
unspents: map[btcwire.OutPoint]struct{}{
@ -277,10 +347,15 @@ func TestTxStore(t *testing.T) {
},
{
name: "insert original recv txout",
f: func(s *Store) *Store {
s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
return s
f: func(s *Store) (*Store, error) {
r, err := s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails)
if err != nil {
return nil, err
}
_ = r.TxInfo("", 100, btcwire.MainNet)
return s, nil
},
err: nil,
bal: TstRecvTx.MsgTx().TxOut[0].Value,
unc: 0,
unspents: map[btcwire.OutPoint]struct{}{
@ -292,10 +367,17 @@ func TestTxStore(t *testing.T) {
var s *Store
for _, test := range tests {
s = test.f(s)
tmpStore, err := test.f(s)
if err != test.err {
t.Fatalf("%s: error mismatch: expected: %v, got: %v", test.name, test.err, err)
}
if test.err != nil {
continue
}
s = tmpStore
bal := s.Balance(1, TstRecvCurrentHeight)
if bal != test.bal {
t.Errorf("%s: balance mismatch: expected %d, got %d", test.name, test.bal, bal)
t.Errorf("%s: balance mismatch: expected: %d, got: %d", test.name, test.bal, bal)
}
unc := s.Balance(0, TstRecvCurrentHeight) - bal
if unc != test.unc {