2e76bcd159
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.
433 lines
14 KiB
Go
433 lines
14 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.
|
|
*/
|
|
|
|
package tx_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/conformal/btcutil"
|
|
. "github.com/conformal/btcwallet/tx"
|
|
"github.com/conformal/btcwire"
|
|
)
|
|
|
|
// Received transaction output for mainnet outpoint
|
|
// 61d3696de4c888730cbe06b0ad8ecb6d72d6108e893895aa9bc067bd7eba3fad:0
|
|
var (
|
|
TstRecvSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000")
|
|
TstRecvTx, _ = btcutil.NewTxFromBytes(TstRecvSerializedTx)
|
|
TstRecvTxSpendingTxBlockHash, _ = btcwire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4")
|
|
TstRecvAmt = int64(10000000)
|
|
TstRecvTxBlockDetails = &BlockDetails{
|
|
Height: 276425,
|
|
Hash: *TstRecvTxSpendingTxBlockHash,
|
|
Index: 684,
|
|
Time: time.Unix(1387737310, 0),
|
|
}
|
|
|
|
TstRecvCurrentHeight = int32(284498) // mainnet blockchain height at time of writing
|
|
TstRecvTxOutConfirms = 8074 // hardcoded number of confirmations given the above block height
|
|
|
|
TstSpendingTxBlockHeight = int32(279143)
|
|
TstSignedTxBlockHash, _ = btcwire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4")
|
|
TstSignedTxBlockDetails = &BlockDetails{
|
|
Height: TstSpendingTxBlockHeight,
|
|
Hash: *TstSignedTxBlockHash,
|
|
Index: 123,
|
|
Time: time.Unix(1389114091, 0),
|
|
}
|
|
)
|
|
|
|
func TestTxStore(t *testing.T) {
|
|
// Create a double spend of the received blockchain transaction.
|
|
dupRecvTx, _ := btcutil.NewTxFromBytes(TstRecvSerializedTx)
|
|
// Switch txout amount to 1 BTC. Transaction store doesn't
|
|
// validate txs, so this is fine for testing a double spend
|
|
// removal.
|
|
TstDupRecvAmount := int64(1e8)
|
|
newDupMsgTx := dupRecvTx.MsgTx()
|
|
newDupMsgTx.TxOut[0].Value = TstDupRecvAmount
|
|
TstDoubleSpendTx := btcutil.NewTx(newDupMsgTx)
|
|
|
|
// Create a "signed" (with invalid sigs) tx that spends output 0 of
|
|
// the double spend.
|
|
spendingTx := btcwire.NewMsgTx()
|
|
spendingTxIn := btcwire.NewTxIn(btcwire.NewOutPoint(TstDoubleSpendTx.Sha(), 0), []byte{0, 1, 2, 3, 4})
|
|
spendingTx.AddTxIn(spendingTxIn)
|
|
spendingTxOut1 := btcwire.NewTxOut(1e7, []byte{5, 6, 7, 8, 9})
|
|
spendingTxOut2 := btcwire.NewTxOut(9e7, []byte{10, 11, 12, 13, 14})
|
|
spendingTx.AddTxOut(spendingTxOut1)
|
|
spendingTx.AddTxOut(spendingTxOut2)
|
|
TstSpendingTx := btcutil.NewTx(spendingTx)
|
|
|
|
tests := []struct {
|
|
name string
|
|
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, error) {
|
|
return NewStore(), nil
|
|
},
|
|
err: nil,
|
|
bal: 0,
|
|
unc: 0,
|
|
unspents: map[btcwire.OutPoint]struct{}{},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
{
|
|
name: "txout insert",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
{
|
|
name: "confirmed txout insert",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
{
|
|
name: "insert duplicate confirmed",
|
|
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{}{
|
|
*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: ErrInconsistantStore,
|
|
},
|
|
{
|
|
name: "insert double spend with new txout value",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstDoubleSpendTx.Sha(), 0): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
{
|
|
name: "insert unconfirmed signed tx",
|
|
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{}{},
|
|
unmined: map[btcwire.ShaHash]struct{}{
|
|
*TstSpendingTx.Sha(): struct{}{},
|
|
},
|
|
},
|
|
{
|
|
name: "insert unconfirmed signed tx again",
|
|
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{}{},
|
|
unmined: map[btcwire.ShaHash]struct{}{
|
|
*TstSpendingTx.Sha(): struct{}{},
|
|
},
|
|
},
|
|
{
|
|
name: "insert change (index 0)",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{
|
|
*TstSpendingTx.Sha(): struct{}{},
|
|
},
|
|
},
|
|
{
|
|
name: "insert output back to this own wallet (index 1)",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{},
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{
|
|
*TstSpendingTx.Sha(): struct{}{},
|
|
},
|
|
},
|
|
{
|
|
name: "confirmed signed tx",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{},
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
{
|
|
name: "rollback after spending tx",
|
|
f: func(s *Store) (*Store, error) {
|
|
s.Rollback(TstSignedTxBlockDetails.Height + 1)
|
|
return s, nil
|
|
},
|
|
err: nil,
|
|
bal: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
|
|
unc: 0,
|
|
unspents: map[btcwire.OutPoint]struct{}{
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{},
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
{
|
|
name: "rollback spending tx block",
|
|
f: func(s *Store) (*Store, error) {
|
|
s.Rollback(TstSignedTxBlockDetails.Height)
|
|
return s, nil
|
|
},
|
|
err: nil,
|
|
bal: 0,
|
|
unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
|
|
unspents: map[btcwire.OutPoint]struct{}{
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{},
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{
|
|
*TstSpendingTx.Sha(): struct{}{},
|
|
},
|
|
},
|
|
{
|
|
name: "rollback double spend tx block",
|
|
f: func(s *Store) (*Store, error) {
|
|
s.Rollback(TstRecvTxBlockDetails.Height)
|
|
return s, nil
|
|
},
|
|
err: nil,
|
|
bal: 0,
|
|
unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value,
|
|
unspents: map[btcwire.OutPoint]struct{}{
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{},
|
|
*btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{
|
|
*TstSpendingTx.Sha(): struct{}{},
|
|
},
|
|
},
|
|
{
|
|
name: "insert original recv txout",
|
|
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{}{
|
|
*btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{},
|
|
},
|
|
unmined: map[btcwire.ShaHash]struct{}{},
|
|
},
|
|
}
|
|
|
|
var s *Store
|
|
for _, test := range tests {
|
|
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)
|
|
}
|
|
unc := s.Balance(0, TstRecvCurrentHeight) - bal
|
|
if unc != test.unc {
|
|
t.Errorf("%s: unconfimred balance mismatch: expected %d, got %d", test.name, test.unc, unc)
|
|
}
|
|
|
|
// Check that unspent outputs match expected.
|
|
for _, record := range s.UnspentOutputs() {
|
|
if record.Spent() {
|
|
t.Errorf("%s: unspent record marked as spent", test.name)
|
|
}
|
|
|
|
op := *record.OutPoint()
|
|
if _, ok := test.unspents[op]; !ok {
|
|
t.Errorf("%s: unexpected unspent output: %v", test.name, op)
|
|
}
|
|
delete(test.unspents, op)
|
|
}
|
|
if len(test.unspents) != 0 {
|
|
t.Errorf("%s: missing expected unspent output(s)", test.name)
|
|
}
|
|
|
|
// Check that unmined signed txs match expected.
|
|
for _, tx := range s.UnminedSignedTxs() {
|
|
if _, ok := test.unmined[*tx.Sha()]; !ok {
|
|
t.Errorf("%s: unexpected unmined signed tx: %v", test.name, *tx.Sha())
|
|
}
|
|
delete(test.unmined, *tx.Sha())
|
|
}
|
|
if len(test.unmined) != 0 {
|
|
t.Errorf("%s: missing expected unmined signed tx(s)", test.name)
|
|
}
|
|
|
|
// Pass a re-serialized version of the store to each next test.
|
|
buf := new(bytes.Buffer)
|
|
nWritten, err := s.WriteTo(buf)
|
|
if err != nil {
|
|
t.Fatalf("%v: serialization failed: %v (wrote %v bytes)", test.name, err, nWritten)
|
|
}
|
|
if nWritten != int64(buf.Len()) {
|
|
t.Errorf("%v: wrote %v bytes but buffer has %v", test.name, nWritten, buf.Len())
|
|
}
|
|
nRead, err := s.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("%v: deserialization failed: %v (read %v bytes after writing %v)",
|
|
test.name, err, nRead, nWritten)
|
|
}
|
|
if nWritten != nRead {
|
|
t.Errorf("%v: number of bytes written (%v) does not match those read (%v)",
|
|
test.name, nWritten, nRead)
|
|
}
|
|
}
|
|
}
|