2017-01-12 15:47:46 +01:00
|
|
|
// Copyright (c) 2015-2017 The btcsuite developers
|
2015-12-01 19:44:58 +01:00
|
|
|
// Use of this source code is governed by an ISC
|
|
|
|
// license that can be found in the LICENSE file.
|
2015-04-06 21:18:04 +02:00
|
|
|
|
|
|
|
package wtxmgr_test
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/binary"
|
2018-08-30 01:56:25 +02:00
|
|
|
"errors"
|
2015-04-06 21:18:04 +02:00
|
|
|
"fmt"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2018-05-15 07:11:11 +02:00
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
|
|
. "github.com/btcsuite/btcwallet/wtxmgr"
|
2015-04-06 21:18:04 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
type queryState struct {
|
|
|
|
// slice items are ordered by height, mempool comes last.
|
|
|
|
blocks [][]TxDetails
|
2016-08-08 21:49:09 +02:00
|
|
|
txDetails map[chainhash.Hash][]TxDetails
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func newQueryState() *queryState {
|
|
|
|
return &queryState{
|
2016-08-08 21:49:09 +02:00
|
|
|
txDetails: make(map[chainhash.Hash][]TxDetails),
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (q *queryState) deepCopy() *queryState {
|
|
|
|
cpy := newQueryState()
|
|
|
|
for _, blockDetails := range q.blocks {
|
|
|
|
var cpyDetails []TxDetails
|
|
|
|
for _, detail := range blockDetails {
|
|
|
|
cpyDetails = append(cpyDetails, *deepCopyTxDetails(&detail))
|
|
|
|
}
|
|
|
|
cpy.blocks = append(cpy.blocks, cpyDetails)
|
|
|
|
}
|
2016-08-08 21:49:09 +02:00
|
|
|
cpy.txDetails = make(map[chainhash.Hash][]TxDetails)
|
2015-04-06 21:18:04 +02:00
|
|
|
for txHash, details := range q.txDetails {
|
|
|
|
detailsSlice := make([]TxDetails, len(details))
|
|
|
|
for i, detail := range details {
|
|
|
|
detailsSlice[i] = *deepCopyTxDetails(&detail)
|
|
|
|
}
|
|
|
|
cpy.txDetails[txHash] = detailsSlice
|
|
|
|
}
|
|
|
|
return cpy
|
|
|
|
}
|
|
|
|
|
|
|
|
func deepCopyTxDetails(d *TxDetails) *TxDetails {
|
|
|
|
cpy := *d
|
|
|
|
cpy.MsgTx = *d.MsgTx.Copy()
|
|
|
|
if cpy.SerializedTx != nil {
|
|
|
|
cpy.SerializedTx = make([]byte, len(cpy.SerializedTx))
|
|
|
|
copy(cpy.SerializedTx, d.SerializedTx)
|
|
|
|
}
|
|
|
|
cpy.Credits = make([]CreditRecord, len(d.Credits))
|
|
|
|
copy(cpy.Credits, d.Credits)
|
|
|
|
cpy.Debits = make([]DebitRecord, len(d.Debits))
|
|
|
|
copy(cpy.Debits, d.Debits)
|
|
|
|
return &cpy
|
|
|
|
}
|
|
|
|
|
2018-08-30 01:56:25 +02:00
|
|
|
func (q *queryState) compare(s *Store, ns walletdb.ReadBucket,
|
|
|
|
changeDesc string) error {
|
2015-04-06 21:18:04 +02:00
|
|
|
|
|
|
|
fwdBlocks := q.blocks
|
|
|
|
revBlocks := make([][]TxDetails, len(q.blocks))
|
|
|
|
copy(revBlocks, q.blocks)
|
|
|
|
for i := 0; i < len(revBlocks)/2; i++ {
|
|
|
|
revBlocks[i], revBlocks[len(revBlocks)-1-i] = revBlocks[len(revBlocks)-1-i], revBlocks[i]
|
|
|
|
}
|
|
|
|
checkBlock := func(blocks [][]TxDetails) func([]TxDetails) (bool, error) {
|
|
|
|
return func(got []TxDetails) (bool, error) {
|
|
|
|
if len(fwdBlocks) == 0 {
|
2018-08-30 01:56:25 +02:00
|
|
|
return false, errors.New("entered range " +
|
|
|
|
"when no more details expected")
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
exp := blocks[0]
|
|
|
|
if len(got) != len(exp) {
|
2018-08-30 01:56:25 +02:00
|
|
|
return false, fmt.Errorf("got len(details)=%d "+
|
|
|
|
"in transaction range, expected %d",
|
|
|
|
len(got), len(exp))
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
for i := range got {
|
2018-08-30 01:56:25 +02:00
|
|
|
err := equalTxDetails(&got[i], &exp[i])
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed "+
|
|
|
|
"comparing range of "+
|
|
|
|
"transaction details: %v", err)
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
blocks = blocks[1:]
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
err := s.RangeTransactions(ns, 0, -1, checkBlock(fwdBlocks))
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("%s: failed in RangeTransactions (forwards "+
|
|
|
|
"iteration): %v", changeDesc, err)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
err = s.RangeTransactions(ns, -1, 0, checkBlock(revBlocks))
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("%s: failed in RangeTransactions (reverse "+
|
|
|
|
"iteration): %v", changeDesc, err)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for txHash, details := range q.txDetails {
|
|
|
|
for _, detail := range details {
|
|
|
|
blk := &detail.Block.Block
|
|
|
|
if blk.Height == -1 {
|
|
|
|
blk = nil
|
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
d, err := s.UniqueTxDetails(ns, &txHash, blk)
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return err
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if d == nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("found no matching "+
|
|
|
|
"transaction at height %d",
|
|
|
|
detail.Block.Height)
|
|
|
|
}
|
|
|
|
if err := equalTxDetails(d, &detail); err != nil {
|
|
|
|
return fmt.Errorf("%s: failed querying latest "+
|
|
|
|
"details regarding transaction %v",
|
|
|
|
changeDesc, txHash)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// For the most recent tx with this hash, check that
|
|
|
|
// TxDetails (not looking up a tx at any particular
|
|
|
|
// height) matches the last.
|
|
|
|
detail := &details[len(details)-1]
|
2017-01-21 17:29:06 +01:00
|
|
|
d, err := s.TxDetails(ns, &txHash)
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return err
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
2018-08-30 01:56:25 +02:00
|
|
|
if err := equalTxDetails(d, detail); err != nil {
|
|
|
|
return fmt.Errorf("%s: failed querying latest details "+
|
|
|
|
"regarding transaction %v", changeDesc, txHash)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
}
|
2018-08-30 01:56:25 +02:00
|
|
|
|
|
|
|
return nil
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
2018-08-30 01:56:25 +02:00
|
|
|
func equalTxDetails(got, exp *TxDetails) error {
|
2015-04-06 21:18:04 +02:00
|
|
|
// Need to avoid using reflect.DeepEqual against slices, since it
|
|
|
|
// returns false for nil vs non-nil zero length slices.
|
2018-08-30 01:56:25 +02:00
|
|
|
if err := equalTxs(&got.MsgTx, &exp.MsgTx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
|
|
|
|
if got.Hash != exp.Hash {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("found mismatched hashes: got %v, expected %v",
|
|
|
|
got.Hash, exp.Hash)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if got.Received != exp.Received {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("found mismatched receive time: got %v, "+
|
|
|
|
"expected %v", got.Received, exp.Received)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if !bytes.Equal(got.SerializedTx, exp.SerializedTx) {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("found mismatched serialized txs: got %v, "+
|
|
|
|
"expected %v", got.SerializedTx, exp.SerializedTx)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if got.Block != exp.Block {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("found mismatched block meta: got %v, "+
|
|
|
|
"expected %v", got.Block, exp.Block)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if len(got.Credits) != len(exp.Credits) {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("credit slice lengths differ: got %d, "+
|
|
|
|
"expected %d", len(got.Credits), len(exp.Credits))
|
|
|
|
}
|
|
|
|
for i := range got.Credits {
|
|
|
|
if got.Credits[i] != exp.Credits[i] {
|
|
|
|
return fmt.Errorf("found mismatched credit[%d]: got %v, "+
|
|
|
|
"expected %v", i, got.Credits[i], exp.Credits[i])
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(got.Debits) != len(exp.Debits) {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("debit slice lengths differ: got %d, "+
|
|
|
|
"expected %d", len(got.Debits), len(exp.Debits))
|
|
|
|
}
|
|
|
|
for i := range got.Debits {
|
|
|
|
if got.Debits[i] != exp.Debits[i] {
|
|
|
|
return fmt.Errorf("found mismatched debit[%d]: got %v, "+
|
|
|
|
"expected %v", i, got.Debits[i], exp.Debits[i])
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
}
|
2018-08-30 01:56:25 +02:00
|
|
|
|
|
|
|
return nil
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
2018-08-30 01:56:25 +02:00
|
|
|
func equalTxs(got, exp *wire.MsgTx) error {
|
2015-04-06 21:18:04 +02:00
|
|
|
var bufGot, bufExp bytes.Buffer
|
|
|
|
err := got.Serialize(&bufGot)
|
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return err
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
err = exp.Serialize(&bufExp)
|
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return err
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if !bytes.Equal(bufGot.Bytes(), bufExp.Bytes()) {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("found unexpected wire.MsgTx: got: %v, "+
|
|
|
|
"expected %v", got, exp)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
2018-08-30 01:56:25 +02:00
|
|
|
|
|
|
|
return nil
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Returns time.Now() with seconds resolution, this is what Store saves.
|
|
|
|
func timeNow() time.Time {
|
|
|
|
return time.Unix(time.Now().Unix(), 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a copy of a TxRecord without the serialized tx.
|
|
|
|
func stripSerializedTx(rec *TxRecord) *TxRecord {
|
|
|
|
ret := *rec
|
|
|
|
ret.SerializedTx = nil
|
|
|
|
return &ret
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeBlockMeta(height int32) BlockMeta {
|
|
|
|
if height == -1 {
|
|
|
|
return BlockMeta{Block: Block{Height: -1}}
|
|
|
|
}
|
|
|
|
|
|
|
|
b := BlockMeta{
|
|
|
|
Block: Block{Height: height},
|
|
|
|
Time: timeNow(),
|
|
|
|
}
|
|
|
|
// Give it a fake block hash created from the height and time.
|
|
|
|
binary.LittleEndian.PutUint32(b.Hash[0:4], uint32(height))
|
|
|
|
binary.LittleEndian.PutUint64(b.Hash[4:12], uint64(b.Time.Unix()))
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStoreQueries(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
type queryTest struct {
|
|
|
|
desc string
|
2018-08-30 01:56:25 +02:00
|
|
|
updates func(ns walletdb.ReadWriteBucket) error
|
2015-04-06 21:18:04 +02:00
|
|
|
state *queryState
|
|
|
|
}
|
|
|
|
var tests []queryTest
|
|
|
|
|
|
|
|
// Create the store and test initial state.
|
2017-01-21 17:29:06 +01:00
|
|
|
s, db, teardown, err := testStore()
|
2015-04-06 21:18:04 +02:00
|
|
|
defer teardown()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
lastState := newQueryState()
|
|
|
|
tests = append(tests, queryTest{
|
|
|
|
desc: "initial store",
|
2018-08-30 01:56:25 +02:00
|
|
|
updates: func(walletdb.ReadWriteBucket) error { return nil },
|
2015-04-06 21:18:04 +02:00
|
|
|
state: lastState,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Insert an unmined transaction. Mark no credits yet.
|
2016-08-08 21:49:09 +02:00
|
|
|
txA := spendOutput(&chainhash.Hash{}, 0, 100e8)
|
2018-08-30 01:56:25 +02:00
|
|
|
recA, err := NewTxRecordFromMsgTx(txA, timeNow())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
newState := lastState.deepCopy()
|
|
|
|
newState.blocks = [][]TxDetails{
|
|
|
|
{
|
|
|
|
{
|
|
|
|
TxRecord: *stripSerializedTx(recA),
|
|
|
|
Block: BlockMeta{Block: Block{Height: -1}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
newState.txDetails[recA.Hash] = []TxDetails{
|
|
|
|
newState.blocks[0][0],
|
|
|
|
}
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "insert tx A unmined",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.InsertTx(ns, recA, nil)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
// Add txA:0 as a change credit.
|
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[0][0].Credits = []CreditRecord{
|
|
|
|
{
|
|
|
|
Index: 0,
|
|
|
|
Amount: btcutil.Amount(recA.MsgTx.TxOut[0].Value),
|
|
|
|
Spent: false,
|
|
|
|
Change: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
newState.txDetails[recA.Hash][0].Credits = newState.blocks[0][0].Credits
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "mark unconfirmed txA:0 as credit",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.AddCredit(ns, recA, nil, 0, true)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
// Insert another unmined transaction which spends txA:0, splitting the
|
|
|
|
// amount into outputs of 40 and 60 BTC.
|
|
|
|
txB := spendOutput(&recA.Hash, 0, 40e8, 60e8)
|
2018-08-30 01:56:25 +02:00
|
|
|
recB, err := NewTxRecordFromMsgTx(txB, timeNow())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[0][0].Credits[0].Spent = true
|
|
|
|
newState.blocks[0] = append(newState.blocks[0], TxDetails{
|
|
|
|
TxRecord: *stripSerializedTx(recB),
|
|
|
|
Block: BlockMeta{Block: Block{Height: -1}},
|
|
|
|
Debits: []DebitRecord{
|
|
|
|
{
|
|
|
|
Amount: btcutil.Amount(recA.MsgTx.TxOut[0].Value),
|
|
|
|
Index: 0, // recB.MsgTx.TxIn index
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
newState.txDetails[recA.Hash][0].Credits[0].Spent = true
|
|
|
|
newState.txDetails[recB.Hash] = []TxDetails{newState.blocks[0][1]}
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "insert tx B unmined",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.InsertTx(ns, recB, nil)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[0][1].Credits = []CreditRecord{
|
|
|
|
{
|
|
|
|
Index: 0,
|
|
|
|
Amount: btcutil.Amount(recB.MsgTx.TxOut[0].Value),
|
|
|
|
Spent: false,
|
|
|
|
Change: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
newState.txDetails[recB.Hash][0].Credits = newState.blocks[0][1].Credits
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "mark txB:0 as non-change credit",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.AddCredit(ns, recB, nil, 0, false)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
// Mine tx A at block 100. Leave tx B unmined.
|
|
|
|
b100 := makeBlockMeta(100)
|
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[0] = newState.blocks[0][:1]
|
|
|
|
newState.blocks[0][0].Block = b100
|
|
|
|
newState.blocks = append(newState.blocks, lastState.blocks[0][1:])
|
|
|
|
newState.txDetails[recA.Hash][0].Block = b100
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "mine tx A",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.InsertTx(ns, recA, &b100)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
// Mine tx B at block 101.
|
|
|
|
b101 := makeBlockMeta(101)
|
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[1][0].Block = b101
|
|
|
|
newState.txDetails[recB.Hash][0].Block = b101
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "mine tx B",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.InsertTx(ns, recB, &b101)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
|
|
|
ns := tx.ReadWriteBucket(namespaceKey)
|
2018-08-30 01:56:25 +02:00
|
|
|
if err := tst.updates(ns); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return tst.state.compare(s, ns, tst.desc)
|
2017-01-21 17:29:06 +01:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run some additional query tests with the current store's state:
|
|
|
|
// - Verify that querying for a transaction not in the store returns
|
|
|
|
// nil without failure.
|
|
|
|
// - Verify that querying for a unique transaction at the wrong block
|
|
|
|
// returns nil without failure.
|
|
|
|
// - Verify that breaking early on RangeTransactions stops further
|
|
|
|
// iteration.
|
|
|
|
|
2017-01-21 17:29:06 +01:00
|
|
|
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
|
|
|
ns := tx.ReadWriteBucket(namespaceKey)
|
|
|
|
|
|
|
|
missingTx := spendOutput(&recB.Hash, 0, 40e8)
|
2018-08-30 01:56:25 +02:00
|
|
|
missingRec, err := NewTxRecordFromMsgTx(missingTx, timeNow())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
missingBlock := makeBlockMeta(102)
|
|
|
|
missingDetails, err := s.TxDetails(ns, &missingRec.Hash)
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return err
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if missingDetails != nil {
|
2018-08-30 01:56:25 +02:00
|
|
|
return fmt.Errorf("Expected no details, found details "+
|
|
|
|
"for tx %v", missingDetails.Hash)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
missingUniqueTests := []struct {
|
|
|
|
hash *chainhash.Hash
|
|
|
|
block *Block
|
|
|
|
}{
|
|
|
|
{&missingRec.Hash, &b100.Block},
|
|
|
|
{&missingRec.Hash, &missingBlock.Block},
|
|
|
|
{&missingRec.Hash, nil},
|
|
|
|
{&recB.Hash, &b100.Block},
|
|
|
|
{&recB.Hash, &missingBlock.Block},
|
|
|
|
{&recB.Hash, nil},
|
|
|
|
}
|
|
|
|
for _, tst := range missingUniqueTests {
|
|
|
|
missingDetails, err = s.UniqueTxDetails(ns, tst.hash, tst.block)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if missingDetails != nil {
|
|
|
|
t.Errorf("Expected no details, found details for tx %v", missingDetails.Hash)
|
|
|
|
}
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
|
2017-01-21 17:29:06 +01:00
|
|
|
iterations := 0
|
|
|
|
err = s.RangeTransactions(ns, 0, -1, func([]TxDetails) (bool, error) {
|
|
|
|
iterations++
|
|
|
|
return true, nil
|
|
|
|
})
|
|
|
|
if iterations != 1 {
|
|
|
|
t.Errorf("RangeTransactions (forwards) ran func %d times", iterations)
|
|
|
|
}
|
|
|
|
iterations = 0
|
|
|
|
err = s.RangeTransactions(ns, -1, 0, func([]TxDetails) (bool, error) {
|
|
|
|
iterations++
|
|
|
|
return true, nil
|
|
|
|
})
|
|
|
|
if iterations != 1 {
|
|
|
|
t.Errorf("RangeTransactions (reverse) ran func %d times", iterations)
|
|
|
|
}
|
|
|
|
// Make sure it also breaks early after one iteration through unmined transactions.
|
2018-08-30 01:56:25 +02:00
|
|
|
if err := s.Rollback(ns, b101.Height); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
iterations = 0
|
|
|
|
err = s.RangeTransactions(ns, -1, 0, func([]TxDetails) (bool, error) {
|
|
|
|
iterations++
|
|
|
|
return true, nil
|
|
|
|
})
|
|
|
|
if iterations != 1 {
|
|
|
|
t.Errorf("RangeTransactions (reverse) ran func %d times", iterations)
|
|
|
|
}
|
|
|
|
return nil
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
2017-01-21 17:29:06 +01:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// None of the above tests have tested RangeTransactions with multiple
|
|
|
|
// txs per block, so do that now. Start by moving tx B to block 100
|
|
|
|
// (same block as tx A), and then rollback from block 100 onwards so
|
|
|
|
// both are unmined.
|
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[0] = append(newState.blocks[0], newState.blocks[1]...)
|
|
|
|
newState.blocks[0][1].Block = b100
|
|
|
|
newState.blocks = newState.blocks[:1]
|
|
|
|
newState.txDetails[recB.Hash][0].Block = b100
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests[:0:0], queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "move tx B to block 100",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.InsertTx(ns, recB, &b100)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
newState = lastState.deepCopy()
|
|
|
|
newState.blocks[0][0].Block = makeBlockMeta(-1)
|
|
|
|
newState.blocks[0][1].Block = makeBlockMeta(-1)
|
|
|
|
newState.txDetails[recA.Hash][0].Block = makeBlockMeta(-1)
|
|
|
|
newState.txDetails[recB.Hash][0].Block = makeBlockMeta(-1)
|
|
|
|
lastState = newState
|
|
|
|
tests = append(tests, queryTest{
|
2018-08-30 01:56:25 +02:00
|
|
|
desc: "rollback block 100",
|
|
|
|
updates: func(ns walletdb.ReadWriteBucket) error {
|
|
|
|
return s.Rollback(ns, b100.Height)
|
|
|
|
},
|
|
|
|
state: newState,
|
2015-04-06 21:18:04 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
|
|
|
ns := tx.ReadWriteBucket(namespaceKey)
|
2018-08-30 01:56:25 +02:00
|
|
|
if err := tst.updates(ns); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return tst.state.compare(s, ns, tst.desc)
|
2017-01-21 17:29:06 +01:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPreviousPkScripts(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2017-01-21 17:29:06 +01:00
|
|
|
s, db, teardown, err := testStore()
|
2015-04-06 21:18:04 +02:00
|
|
|
defer teardown()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invalid scripts but sufficient for testing.
|
|
|
|
var (
|
|
|
|
scriptA0 = []byte("tx A output 0")
|
|
|
|
scriptA1 = []byte("tx A output 1")
|
|
|
|
scriptB0 = []byte("tx B output 0")
|
|
|
|
scriptB1 = []byte("tx B output 1")
|
|
|
|
scriptC0 = []byte("tx C output 0")
|
|
|
|
scriptC1 = []byte("tx C output 1")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Create a transaction spending two prevous outputs and generating two
|
|
|
|
// new outputs the passed pkScipts. Spends outputs 0 and 1 from prevHash.
|
2016-08-08 21:49:09 +02:00
|
|
|
buildTx := func(prevHash *chainhash.Hash, script0, script1 []byte) *wire.MsgTx {
|
2015-04-06 21:18:04 +02:00
|
|
|
return &wire.MsgTx{
|
|
|
|
TxIn: []*wire.TxIn{
|
2017-01-12 15:47:46 +01:00
|
|
|
{PreviousOutPoint: wire.OutPoint{
|
2016-03-25 20:11:25 +01:00
|
|
|
Hash: *prevHash,
|
|
|
|
Index: 0,
|
|
|
|
}},
|
2017-01-12 15:47:46 +01:00
|
|
|
{PreviousOutPoint: wire.OutPoint{
|
2016-03-25 20:11:25 +01:00
|
|
|
Hash: *prevHash, Index: 1,
|
|
|
|
}},
|
2015-04-06 21:18:04 +02:00
|
|
|
},
|
|
|
|
TxOut: []*wire.TxOut{
|
2017-01-12 15:47:46 +01:00
|
|
|
{Value: 1e8, PkScript: script0},
|
|
|
|
{Value: 1e8, PkScript: script1},
|
2015-04-06 21:18:04 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
newTxRecordFromMsgTx := func(tx *wire.MsgTx) *TxRecord {
|
|
|
|
rec, err := NewTxRecordFromMsgTx(tx, timeNow())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
return rec
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create transactions with the fake output scripts.
|
|
|
|
var (
|
2016-08-08 21:49:09 +02:00
|
|
|
txA = buildTx(&chainhash.Hash{}, scriptA0, scriptA1)
|
2015-04-06 21:18:04 +02:00
|
|
|
recA = newTxRecordFromMsgTx(txA)
|
|
|
|
txB = buildTx(&recA.Hash, scriptB0, scriptB1)
|
|
|
|
recB = newTxRecordFromMsgTx(txB)
|
|
|
|
txC = buildTx(&recB.Hash, scriptC0, scriptC1)
|
|
|
|
recC = newTxRecordFromMsgTx(txC)
|
|
|
|
txD = buildTx(&recC.Hash, nil, nil)
|
|
|
|
recD = newTxRecordFromMsgTx(txD)
|
|
|
|
)
|
|
|
|
|
2017-01-21 17:29:06 +01:00
|
|
|
insertTx := func(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta) {
|
|
|
|
err := s.InsertTx(ns, rec, block)
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
addCredit := func(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32) {
|
|
|
|
err := s.AddCredit(ns, rec, block, index, false)
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type scriptTest struct {
|
|
|
|
rec *TxRecord
|
|
|
|
block *Block
|
|
|
|
scripts [][]byte
|
|
|
|
}
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest := func(ns walletdb.ReadWriteBucket, tst *scriptTest) {
|
|
|
|
scripts, err := s.PreviousPkScripts(ns, tst.rec, tst.block)
|
2015-04-06 21:18:04 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
height := int32(-1)
|
|
|
|
if tst.block != nil {
|
|
|
|
height = tst.block.Height
|
|
|
|
}
|
|
|
|
if len(scripts) != len(tst.scripts) {
|
|
|
|
t.Errorf("Transaction %v height %d: got len(scripts)=%d, expected %d",
|
|
|
|
tst.rec.Hash, height, len(scripts), len(tst.scripts))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for i := range scripts {
|
|
|
|
if !bytes.Equal(scripts[i], tst.scripts[i]) {
|
|
|
|
// Format scripts with %s since they are (should be) ascii.
|
|
|
|
t.Errorf("Transaction %v height %d script %d: got '%s' expected '%s'",
|
|
|
|
tst.rec.Hash, height, i, scripts[i], tst.scripts[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-21 17:29:06 +01:00
|
|
|
dbtx, err := db.BeginReadWriteTx()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer dbtx.Commit()
|
|
|
|
ns := dbtx.ReadWriteBucket(namespaceKey)
|
|
|
|
|
2015-04-06 21:18:04 +02:00
|
|
|
// Insert transactions A-C unmined, but mark no credits yet. Until
|
|
|
|
// these are marked as credits, PreviousPkScripts should not return
|
|
|
|
// them.
|
2017-01-21 17:29:06 +01:00
|
|
|
insertTx(ns, recA, nil)
|
|
|
|
insertTx(ns, recB, nil)
|
|
|
|
insertTx(ns, recC, nil)
|
2015-04-06 21:18:04 +02:00
|
|
|
|
|
|
|
b100 := makeBlockMeta(100)
|
|
|
|
b101 := makeBlockMeta(101)
|
|
|
|
|
|
|
|
tests := []scriptTest{
|
|
|
|
{recA, nil, nil},
|
|
|
|
{recA, &b100.Block, nil},
|
|
|
|
{recB, nil, nil},
|
|
|
|
{recB, &b100.Block, nil},
|
|
|
|
{recC, nil, nil},
|
|
|
|
{recC, &b100.Block, nil},
|
|
|
|
}
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest(ns, &tst)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if t.Failed() {
|
|
|
|
t.Fatal("Failed after unmined tx inserts")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mark credits. Tx C output 1 not marked as a credit: tx D will spend
|
|
|
|
// both later but when C is mined, output 1's script should not be
|
|
|
|
// returned.
|
2017-01-21 17:29:06 +01:00
|
|
|
addCredit(ns, recA, nil, 0)
|
|
|
|
addCredit(ns, recA, nil, 1)
|
|
|
|
addCredit(ns, recB, nil, 0)
|
|
|
|
addCredit(ns, recB, nil, 1)
|
|
|
|
addCredit(ns, recC, nil, 0)
|
2015-04-06 21:18:04 +02:00
|
|
|
tests = []scriptTest{
|
|
|
|
{recA, nil, nil},
|
|
|
|
{recA, &b100.Block, nil},
|
|
|
|
{recB, nil, [][]byte{scriptA0, scriptA1}},
|
|
|
|
{recB, &b100.Block, nil},
|
|
|
|
{recC, nil, [][]byte{scriptB0, scriptB1}},
|
|
|
|
{recC, &b100.Block, nil},
|
|
|
|
}
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest(ns, &tst)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if t.Failed() {
|
|
|
|
t.Fatal("Failed after marking unmined credits")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mine tx A in block 100. Test results should be identical.
|
2017-01-21 17:29:06 +01:00
|
|
|
insertTx(ns, recA, &b100)
|
2015-04-06 21:18:04 +02:00
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest(ns, &tst)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if t.Failed() {
|
|
|
|
t.Fatal("Failed after mining tx A")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mine tx B in block 101.
|
2017-01-21 17:29:06 +01:00
|
|
|
insertTx(ns, recB, &b101)
|
2015-04-06 21:18:04 +02:00
|
|
|
tests = []scriptTest{
|
|
|
|
{recA, nil, nil},
|
|
|
|
{recA, &b100.Block, nil},
|
|
|
|
{recB, nil, nil},
|
|
|
|
{recB, &b101.Block, [][]byte{scriptA0, scriptA1}},
|
|
|
|
{recC, nil, [][]byte{scriptB0, scriptB1}},
|
|
|
|
{recC, &b101.Block, nil},
|
|
|
|
}
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest(ns, &tst)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if t.Failed() {
|
|
|
|
t.Fatal("Failed after mining tx B")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mine tx C in block 101 (same block as tx B) to test debits from the
|
|
|
|
// same block.
|
2017-01-21 17:29:06 +01:00
|
|
|
insertTx(ns, recC, &b101)
|
2015-04-06 21:18:04 +02:00
|
|
|
tests = []scriptTest{
|
|
|
|
{recA, nil, nil},
|
|
|
|
{recA, &b100.Block, nil},
|
|
|
|
{recB, nil, nil},
|
|
|
|
{recB, &b101.Block, [][]byte{scriptA0, scriptA1}},
|
|
|
|
{recC, nil, nil},
|
|
|
|
{recC, &b101.Block, [][]byte{scriptB0, scriptB1}},
|
|
|
|
}
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest(ns, &tst)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if t.Failed() {
|
|
|
|
t.Fatal("Failed after mining tx C")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Insert tx D, which spends C:0 and C:1. However, only C:0 is marked
|
|
|
|
// as a credit, and only that output script should be returned.
|
2017-01-21 17:29:06 +01:00
|
|
|
insertTx(ns, recD, nil)
|
2015-04-06 21:18:04 +02:00
|
|
|
tests = append(tests, scriptTest{recD, nil, [][]byte{scriptC0}})
|
|
|
|
tests = append(tests, scriptTest{recD, &b101.Block, nil})
|
|
|
|
for _, tst := range tests {
|
2017-01-21 17:29:06 +01:00
|
|
|
runTest(ns, &tst)
|
2015-04-06 21:18:04 +02:00
|
|
|
}
|
|
|
|
if t.Failed() {
|
|
|
|
t.Fatal("Failed after inserting tx D")
|
|
|
|
}
|
|
|
|
}
|