// Copyright (c) 2015 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package wtxmgr_test import ( "bytes" "encoding/binary" "fmt" "testing" "time" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" . "github.com/btcsuite/btcwallet/wtxmgr" ) type queryState struct { // slice items are ordered by height, mempool comes last. blocks [][]TxDetails txDetails map[wire.ShaHash][]TxDetails } func newQueryState() *queryState { return &queryState{ txDetails: make(map[wire.ShaHash][]TxDetails), } } 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) } cpy.txDetails = make(map[wire.ShaHash][]TxDetails) 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 } func (q *queryState) compare(t *testing.T, s *Store, changeDesc string) { defer func() { if t.Failed() { t.Fatalf("Store state queries failed after '%s'", changeDesc) } }() 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 { return false, fmt.Errorf("entered range when no more details expected") } exp := blocks[0] if len(got) != len(exp) { return false, fmt.Errorf("got len(details)=%d in transaction range, expected %d", len(got), len(exp)) } for i := range got { equalTxDetails(t, &got[i], &exp[i]) } if t.Failed() { return false, fmt.Errorf("Failed comparing range of transaction details") } blocks = blocks[1:] return false, nil } } err := s.RangeTransactions(0, -1, checkBlock(fwdBlocks)) if err != nil { t.Fatalf("Failed in RangeTransactions (forwards iteration): %v", err) } err = s.RangeTransactions(-1, 0, checkBlock(revBlocks)) if err != nil { t.Fatalf("Failed in RangeTransactions (reverse iteration): %v", err) } for txHash, details := range q.txDetails { for _, detail := range details { blk := &detail.Block.Block if blk.Height == -1 { blk = nil } d, err := s.UniqueTxDetails(&txHash, blk) if err != nil { t.Fatal(err) } if d == nil { t.Errorf("Found no matching transaction at height %d", detail.Block.Height) continue } equalTxDetails(t, d, &detail) } if t.Failed() { t.Fatalf("Failed querying unique details regarding transaction %v", txHash) } // 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] d, err := s.TxDetails(&txHash) if err != nil { t.Fatal(err) } equalTxDetails(t, d, detail) if t.Failed() { t.Fatalf("Failed querying latest details regarding transaction %v", txHash) } } } func equalTxDetails(t *testing.T, got, exp *TxDetails) { // Need to avoid using reflect.DeepEqual against slices, since it // returns false for nil vs non-nil zero length slices. equalTxs(t, &got.MsgTx, &exp.MsgTx) if got.Hash != exp.Hash { t.Errorf("Found mismatched hashes") t.Errorf("Got: %v", got.Hash) t.Errorf("Expected: %v", exp.Hash) } if got.Received != exp.Received { t.Errorf("Found mismatched receive time") t.Errorf("Got: %v", got.Received) t.Errorf("Expected: %v", exp.Received) } if !bytes.Equal(got.SerializedTx, exp.SerializedTx) { t.Errorf("Found mismatched serialized txs") t.Errorf("Got: %x", got.SerializedTx) t.Errorf("Expected: %x", exp.SerializedTx) } if got.Block != exp.Block { t.Errorf("Found mismatched block meta") t.Errorf("Got: %v", got.Block) t.Errorf("Expected: %v", exp.Block) } if len(got.Credits) != len(exp.Credits) { t.Errorf("Credit slice lengths differ: Got %d Expected %d", len(got.Credits), len(exp.Credits)) } else { for i := range got.Credits { if got.Credits[i] != exp.Credits[i] { t.Errorf("Found mismatched Credit[%d]", i) t.Errorf("Got: %v", got.Credits[i]) t.Errorf("Expected: %v", exp.Credits[i]) } } } if len(got.Debits) != len(exp.Debits) { t.Errorf("Debit slice lengths differ: Got %d Expected %d", len(got.Debits), len(exp.Debits)) } else { for i := range got.Debits { if got.Debits[i] != exp.Debits[i] { t.Errorf("Found mismatched Debit[%d]", i) t.Errorf("Got: %v", got.Debits[i]) t.Errorf("Expected: %v", exp.Debits[i]) } } } } func equalTxs(t *testing.T, got, exp *wire.MsgTx) { var bufGot, bufExp bytes.Buffer err := got.Serialize(&bufGot) if err != nil { t.Fatal(err) } err = exp.Serialize(&bufExp) if err != nil { t.Fatal(err) } if !bytes.Equal(bufGot.Bytes(), bufExp.Bytes()) { t.Errorf("Found unexpected wire.MsgTx:") t.Errorf("Got: %v", got) t.Errorf("Expected: %v", exp) } } // 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 updates func() // Unwinds from t.Fatal if the update errors. state *queryState } var tests []queryTest // Create the store and test initial state. s, teardown, err := testStore() defer teardown() if err != nil { t.Fatal(err) } lastState := newQueryState() tests = append(tests, queryTest{ desc: "initial store", updates: func() {}, state: lastState, }) // simplify error handling insertTx := func(rec *TxRecord, block *BlockMeta) { err := s.InsertTx(rec, block) if err != nil { t.Fatal(err) } } addCredit := func(s *Store, rec *TxRecord, block *BlockMeta, index uint32, change bool) { err := s.AddCredit(rec, block, index, change) if err != nil { t.Fatal(err) } } newTxRecordFromMsgTx := func(tx *wire.MsgTx, received time.Time) *TxRecord { rec, err := NewTxRecordFromMsgTx(tx, received) if err != nil { t.Fatal(err) } return rec } rollback := func(height int32) { err := s.Rollback(height) if err != nil { t.Fatal(err) } } // Insert an unmined transaction. Mark no credits yet. txA := spendOutput(&wire.ShaHash{}, 0, 100e8) recA := newTxRecordFromMsgTx(txA, timeNow()) 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{ desc: "insert tx A unmined", updates: func() { insertTx(recA, nil) }, state: newState, }) // 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{ desc: "mark unconfirmed txA:0 as credit", updates: func() { addCredit(s, recA, nil, 0, true) }, state: newState, }) // 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) recB := newTxRecordFromMsgTx(txB, timeNow()) 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{ desc: "insert tx B unmined", updates: func() { insertTx(recB, nil) }, state: newState, }) 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{ desc: "mark txB:0 as non-change credit", updates: func() { addCredit(s, recB, nil, 0, false) }, state: newState, }) // 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{ desc: "mine tx A", updates: func() { insertTx(recA, &b100) }, state: newState, }) // 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{ desc: "mine tx B", updates: func() { insertTx(recB, &b101) }, state: newState, }) for _, tst := range tests { tst.updates() tst.state.compare(t, s, tst.desc) } // 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. missingTx := spendOutput(&recB.Hash, 0, 40e8) missingRec := newTxRecordFromMsgTx(missingTx, timeNow()) missingBlock := makeBlockMeta(102) missingDetails, err := s.TxDetails(&missingRec.Hash) if err != nil { t.Fatal(err) } if missingDetails != nil { t.Errorf("Expected no details, found details for tx %v", missingDetails.Hash) } missingUniqueTests := []struct { hash *wire.ShaHash 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(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) } } iterations := 0 err = s.RangeTransactions(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(-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. rollback(b101.Height) iterations = 0 err = s.RangeTransactions(-1, 0, func([]TxDetails) (bool, error) { iterations++ return true, nil }) if iterations != 1 { t.Errorf("RangeTransactions (reverse) ran func %d times", iterations) } // 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{ desc: "move tx B to block 100", updates: func() { insertTx(recB, &b100) }, state: newState, }) 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{ desc: "rollback block 100", updates: func() { rollback(b100.Height) }, state: newState, }) // None of the above tests have tested transactions with colliding // hashes, so mine tx A in block 100, and then insert tx A again // unmined. Also mine tx A in block 101 (this moves it from unmined). // This is a valid test because the store does not perform signature // validation or keep a full utxo set, and duplicated transaction hashes // from different blocks are allowed so long as all previous outputs are // spent. newState = lastState.deepCopy() newState.blocks = append(newState.blocks, newState.blocks[0][1:]) newState.blocks[0] = newState.blocks[0][:1:1] newState.blocks[0][0].Block = b100 newState.blocks[1] = []TxDetails{ { TxRecord: *stripSerializedTx(recA), Block: makeBlockMeta(-1), }, newState.blocks[1][0], } newState.txDetails[recA.Hash][0].Block = b100 newState.txDetails[recA.Hash] = append(newState.txDetails[recA.Hash], newState.blocks[1][0]) lastState = newState tests = append(tests, queryTest{ desc: "insert duplicate tx A", updates: func() { insertTx(recA, &b100); insertTx(recA, nil) }, state: newState, }) newState = lastState.deepCopy() newState.blocks = [][]TxDetails{ newState.blocks[0], []TxDetails{newState.blocks[1][0]}, []TxDetails{newState.blocks[1][1]}, } newState.blocks[1][0].Block = b101 newState.txDetails[recA.Hash][1].Block = b101 lastState = newState tests = append(tests, queryTest{ desc: "mine duplicate tx A", updates: func() { insertTx(recA, &b101) }, state: newState, }) for _, tst := range tests { tst.updates() tst.state.compare(t, s, tst.desc) } } func TestPreviousPkScripts(t *testing.T) { t.Parallel() s, teardown, err := testStore() 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. buildTx := func(prevHash *wire.ShaHash, script0, script1 []byte) *wire.MsgTx { return &wire.MsgTx{ TxIn: []*wire.TxIn{ &wire.TxIn{PreviousOutPoint: wire.OutPoint{*prevHash, 0}}, &wire.TxIn{PreviousOutPoint: wire.OutPoint{*prevHash, 1}}, }, TxOut: []*wire.TxOut{ &wire.TxOut{Value: 1e8, PkScript: script0}, &wire.TxOut{Value: 1e8, PkScript: script1}, }, } } 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 ( txA = buildTx(&wire.ShaHash{}, scriptA0, scriptA1) 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) ) insertTx := func(rec *TxRecord, block *BlockMeta) { err := s.InsertTx(rec, block) if err != nil { t.Fatal(err) } } addCredit := func(rec *TxRecord, block *BlockMeta, index uint32) { err := s.AddCredit(rec, block, index, false) if err != nil { t.Fatal(err) } } type scriptTest struct { rec *TxRecord block *Block scripts [][]byte } runTest := func(tst *scriptTest) { scripts, err := s.PreviousPkScripts(tst.rec, tst.block) 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]) } } } // Insert transactions A-C unmined, but mark no credits yet. Until // these are marked as credits, PreviousPkScripts should not return // them. insertTx(recA, nil) insertTx(recB, nil) insertTx(recC, nil) 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 { runTest(&tst) } 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. addCredit(recA, nil, 0) addCredit(recA, nil, 1) addCredit(recB, nil, 0) addCredit(recB, nil, 1) addCredit(recC, nil, 0) 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 { runTest(&tst) } if t.Failed() { t.Fatal("Failed after marking unmined credits") } // Mine tx A in block 100. Test results should be identical. insertTx(recA, &b100) for _, tst := range tests { runTest(&tst) } if t.Failed() { t.Fatal("Failed after mining tx A") } // Mine tx B in block 101. insertTx(recB, &b101) 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 { runTest(&tst) } 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. insertTx(recC, &b101) 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 { runTest(&tst) } 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. insertTx(recD, nil) tests = append(tests, scriptTest{recD, nil, [][]byte{scriptC0}}) tests = append(tests, scriptTest{recD, &b101.Block, nil}) for _, tst := range tests { runTest(&tst) } if t.Failed() { t.Fatal("Failed after inserting tx D") } }