lbcd/database/ldb/operational_test.go
Dave Collins d574a3af6d Import btcdb repo into database directory.
This commit contains the entire btcdb repository along with several
changes needed to move all of the files into the database directory in
order to prepare it for merging.  This does NOT update btcd or any of the
other packages to use the new location as that will be done separately.

- All import paths in the old btcdb test files have been changed to the
  new location
- All references to btcdb as the package name have been chagned to
  database
- The coveralls badge has been removed since it unfortunately doesn't
  support coverage of sub-packages

This is ongoing work toward #214.
2015-01-27 13:15:15 -06:00

597 lines
16 KiB
Go

// Copyright (c) 2013-2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package ldb_test
import (
"bytes"
"compress/bzip2"
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/btcsuite/btcd/database"
"github.com/btcsuite/btcnet"
"github.com/btcsuite/btcscript"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwire"
"golang.org/x/crypto/ripemd160"
)
var network = btcwire.MainNet
// testDb is used to store db related context for a running test.
// the `cleanUpFunc` *must* be called after each test to maintain db
// consistency across tests.
type testDb struct {
db database.Db
blocks []*btcutil.Block
dbName string
dbNameVer string
cleanUpFunc func()
}
func setUpTestDb(t *testing.T) (*testDb, error) {
// Ignore db remove errors since it means we didn't have an old one.
dbname := fmt.Sprintf("tstdbop1")
dbnamever := dbname + ".ver"
_ = os.RemoveAll(dbname)
_ = os.RemoveAll(dbnamever)
db, err := database.CreateDB("leveldb", dbname)
if err != nil {
return nil, err
}
testdatafile := filepath.Join("..", "testdata", "blocks1-256.bz2")
blocks, err := loadBlocks(t, testdatafile)
if err != nil {
return nil, err
}
cleanUp := func() {
db.Close()
os.RemoveAll(dbname)
os.RemoveAll(dbnamever)
}
return &testDb{
db: db,
blocks: blocks,
dbName: dbname,
dbNameVer: dbnamever,
cleanUpFunc: cleanUp,
}, nil
}
func TestOperational(t *testing.T) {
testOperationalMode(t)
}
// testAddrIndexOperations ensures that all normal operations concerning
// the optional address index function correctly.
func testAddrIndexOperations(t *testing.T, db database.Db, newestBlock *btcutil.Block, newestSha *btcwire.ShaHash, newestBlockIdx int64) {
// Metadata about the current addr index state should be unset.
sha, height, err := db.FetchAddrIndexTip()
if err != database.ErrAddrIndexDoesNotExist {
t.Fatalf("Address index metadata shouldn't be in db, hasn't been built up yet.")
}
var zeroHash btcwire.ShaHash
if !sha.IsEqual(&zeroHash) {
t.Fatalf("AddrIndexTip wrong hash got: %s, want %s", sha, &zeroHash)
}
if height != -1 {
t.Fatalf("Addrindex not built up, yet a block index tip has been set to: %d.", height)
}
// Test enforcement of constraints for "limit" and "skip"
var fakeAddr btcutil.Address
_, err = db.FetchTxsForAddr(fakeAddr, -1, 0)
if err == nil {
t.Fatalf("Negative value for skip passed, should return an error")
}
_, err = db.FetchTxsForAddr(fakeAddr, 0, -1)
if err == nil {
t.Fatalf("Negative value for limit passed, should return an error")
}
// Simple test to index outputs(s) of the first tx.
testIndex := make(database.BlockAddrIndex)
testTx, err := newestBlock.Tx(0)
if err != nil {
t.Fatalf("Block has no transactions, unable to test addr "+
"indexing, err %v", err)
}
// Extract the dest addr from the tx.
_, testAddrs, _, err := btcscript.ExtractPkScriptAddrs(testTx.MsgTx().TxOut[0].PkScript, &btcnet.MainNetParams)
if err != nil {
t.Fatalf("Unable to decode tx output, err %v", err)
}
// Extract the hash160 from the output script.
var hash160Bytes [ripemd160.Size]byte
testHash160 := testAddrs[0].(*btcutil.AddressPubKey).AddressPubKeyHash().ScriptAddress()
copy(hash160Bytes[:], testHash160[:])
// Create a fake index.
blktxLoc, _ := newestBlock.TxLoc()
testIndex[hash160Bytes] = []*btcwire.TxLoc{&blktxLoc[0]}
// Insert our test addr index into the DB.
err = db.UpdateAddrIndexForBlock(newestSha, newestBlockIdx, testIndex)
if err != nil {
t.Fatalf("UpdateAddrIndexForBlock: failed to index"+
" addrs for block #%d (%s) "+
"err %v", newestBlockIdx, newestSha, err)
}
// Chain Tip of address should've been updated.
assertAddrIndexTipIsUpdated(db, t, newestSha, newestBlockIdx)
// Check index retrieval.
txReplies, err := db.FetchTxsForAddr(testAddrs[0], 0, 1000)
if err != nil {
t.Fatalf("FetchTxsForAddr failed to correctly fetch txs for an "+
"address, err %v", err)
}
// Should have one reply.
if len(txReplies) != 1 {
t.Fatalf("Failed to properly index tx by address.")
}
// Our test tx and indexed tx should have the same sha.
indexedTx := txReplies[0]
if !bytes.Equal(indexedTx.Sha.Bytes(), testTx.Sha().Bytes()) {
t.Fatalf("Failed to fetch proper indexed tx. Expected sha %v, "+
"fetched %v", testTx.Sha(), indexedTx.Sha)
}
// Shut down DB.
db.Sync()
db.Close()
// Re-Open, tip still should be updated to current height and sha.
db, err = database.OpenDB("leveldb", "tstdbop1")
if err != nil {
t.Fatalf("Unable to re-open created db, err %v", err)
}
assertAddrIndexTipIsUpdated(db, t, newestSha, newestBlockIdx)
// Delete the entire index.
err = db.DeleteAddrIndex()
if err != nil {
t.Fatalf("Couldn't delete address index, err %v", err)
}
// Former index should no longer exist.
txReplies, err = db.FetchTxsForAddr(testAddrs[0], 0, 1000)
if err != nil {
t.Fatalf("Unable to fetch transactions for address: %v", err)
}
if len(txReplies) != 0 {
t.Fatalf("Address index was not successfully deleted. "+
"Should have 0 tx's indexed, %v were returned.",
len(txReplies))
}
// Tip should be blanked out.
if _, _, err := db.FetchAddrIndexTip(); err != database.ErrAddrIndexDoesNotExist {
t.Fatalf("Address index was not fully deleted.")
}
}
func assertAddrIndexTipIsUpdated(db database.Db, t *testing.T, newestSha *btcwire.ShaHash, newestBlockIdx int64) {
// Safe to ignore error, since height will be < 0 in "error" case.
sha, height, _ := db.FetchAddrIndexTip()
if newestBlockIdx != height {
t.Fatalf("Height of address index tip failed to update, "+
"expected %v, got %v", newestBlockIdx, height)
}
if !bytes.Equal(newestSha.Bytes(), sha.Bytes()) {
t.Fatalf("Sha of address index tip failed to update, "+
"expected %v, got %v", newestSha, sha)
}
}
func testOperationalMode(t *testing.T) {
// simplified basic operation is:
// 1) fetch block from remote server
// 2) look up all txin (except coinbase in db)
// 3) insert block
// 4) exercise the optional addridex
testDb, err := setUpTestDb(t)
defer testDb.cleanUpFunc()
if err != nil {
t.Errorf("Unable to load blocks from test data: %v", err)
return
}
err = nil
out:
for height := int64(0); height < int64(len(testDb.blocks)); height++ {
block := testDb.blocks[height]
mblock := block.MsgBlock()
var txneededList []*btcwire.ShaHash
for _, tx := range mblock.Transactions {
for _, txin := range tx.TxIn {
if txin.PreviousOutPoint.Index == uint32(4294967295) {
continue
}
origintxsha := &txin.PreviousOutPoint.Hash
txneededList = append(txneededList, origintxsha)
exists, err := testDb.db.ExistsTxSha(origintxsha)
if err != nil {
t.Errorf("ExistsTxSha: unexpected error %v ", err)
}
if !exists {
t.Errorf("referenced tx not found %v ", origintxsha)
}
_, err = testDb.db.FetchTxBySha(origintxsha)
if err != nil {
t.Errorf("referenced tx not found %v err %v ", origintxsha, err)
}
}
}
txlist := testDb.db.FetchUnSpentTxByShaList(txneededList)
for _, txe := range txlist {
if txe.Err != nil {
t.Errorf("tx list fetch failed %v err %v ", txe.Sha, txe.Err)
break out
}
}
newheight, err := testDb.db.InsertBlock(block)
if err != nil {
t.Errorf("failed to insert block %v err %v", height, err)
break out
}
if newheight != height {
t.Errorf("height mismatch expect %v returned %v", height, newheight)
break out
}
newSha, blkid, err := testDb.db.NewestSha()
if err != nil {
t.Errorf("failed to obtain latest sha %v %v", height, err)
}
if blkid != height {
t.Errorf("height does not match latest block height %v %v %v", blkid, height, err)
}
blkSha, _ := block.Sha()
if *newSha != *blkSha {
t.Errorf("Newest block sha does not match freshly inserted one %v %v %v ", newSha, blkSha, err)
}
}
// now that the db is populated, do some additional tests
testFetchHeightRange(t, testDb.db, testDb.blocks)
// Ensure all operations dealing with the optional address index behave
// correctly.
newSha, blkid, err := testDb.db.NewestSha()
testAddrIndexOperations(t, testDb.db, testDb.blocks[len(testDb.blocks)-1], newSha, blkid)
}
func TestBackout(t *testing.T) {
testBackout(t)
}
func testBackout(t *testing.T) {
// simplified basic operation is:
// 1) fetch block from remote server
// 2) look up all txin (except coinbase in db)
// 3) insert block
testDb, err := setUpTestDb(t)
defer testDb.cleanUpFunc()
if err != nil {
t.Errorf("Failed to open test database %v", err)
return
}
if len(testDb.blocks) < 120 {
t.Errorf("test data too small")
return
}
err = nil
for height := int64(0); height < int64(len(testDb.blocks)); height++ {
if height == 100 {
t.Logf("Syncing at block height 100")
testDb.db.Sync()
}
if height == 120 {
t.Logf("Simulating unexpected application quit")
// Simulate unexpected application quit
testDb.db.RollbackClose()
break
}
block := testDb.blocks[height]
newheight, err := testDb.db.InsertBlock(block)
if err != nil {
t.Errorf("failed to insert block %v err %v", height, err)
return
}
if newheight != height {
t.Errorf("height mismatch expect %v returned %v", height, newheight)
return
}
}
// db was closed at height 120, so no cleanup is possible.
// reopen db
testDb.db, err = database.OpenDB("leveldb", testDb.dbName)
if err != nil {
t.Errorf("Failed to open test database %v", err)
return
}
defer func() {
if err := testDb.db.Close(); err != nil {
t.Errorf("Close: unexpected error: %v", err)
}
}()
sha, err := testDb.blocks[99].Sha()
if err != nil {
t.Errorf("failed to get block 99 sha err %v", err)
return
}
if _, err := testDb.db.ExistsSha(sha); err != nil {
t.Errorf("ExistsSha: unexpected error: %v", err)
}
_, err = testDb.db.FetchBlockBySha(sha)
if err != nil {
t.Errorf("failed to load block 99 from db %v", err)
return
}
sha, err = testDb.blocks[119].Sha()
if err != nil {
t.Errorf("failed to get block 110 sha err %v", err)
return
}
if _, err := testDb.db.ExistsSha(sha); err != nil {
t.Errorf("ExistsSha: unexpected error: %v", err)
}
_, err = testDb.db.FetchBlockBySha(sha)
if err != nil {
t.Errorf("loaded block 119 from db")
return
}
block := testDb.blocks[119]
mblock := block.MsgBlock()
txsha, err := mblock.Transactions[0].TxSha()
exists, err := testDb.db.ExistsTxSha(&txsha)
if err != nil {
t.Errorf("ExistsTxSha: unexpected error %v ", err)
}
if !exists {
t.Errorf("tx %v not located db\n", txsha)
}
_, err = testDb.db.FetchTxBySha(&txsha)
if err != nil {
t.Errorf("tx %v not located db\n", txsha)
return
}
}
var savedblocks []*btcutil.Block
func loadBlocks(t *testing.T, file string) (blocks []*btcutil.Block, err error) {
if len(savedblocks) != 0 {
blocks = savedblocks
return
}
testdatafile := filepath.Join("..", "testdata", "blocks1-256.bz2")
var dr io.Reader
var fi io.ReadCloser
fi, err = os.Open(testdatafile)
if err != nil {
t.Errorf("failed to open file %v, err %v", testdatafile, err)
return
}
if strings.HasSuffix(testdatafile, ".bz2") {
z := bzip2.NewReader(fi)
dr = z
} else {
dr = fi
}
defer func() {
if err := fi.Close(); err != nil {
t.Errorf("failed to close file %v %v", testdatafile, err)
}
}()
// Set the first block as the genesis block.
genesis := btcutil.NewBlock(btcnet.MainNetParams.GenesisBlock)
blocks = append(blocks, genesis)
var block *btcutil.Block
err = nil
for height := int64(1); err == nil; height++ {
var rintbuf uint32
err = binary.Read(dr, binary.LittleEndian, &rintbuf)
if err == io.EOF {
// hit end of file at expected offset: no warning
height--
err = nil
break
}
if err != nil {
t.Errorf("failed to load network type, err %v", err)
break
}
if rintbuf != uint32(network) {
t.Errorf("Block doesn't match network: %v expects %v",
rintbuf, network)
break
}
err = binary.Read(dr, binary.LittleEndian, &rintbuf)
blocklen := rintbuf
rbytes := make([]byte, blocklen)
// read block
dr.Read(rbytes)
block, err = btcutil.NewBlockFromBytes(rbytes)
if err != nil {
t.Errorf("failed to parse block %v", height)
return
}
blocks = append(blocks, block)
}
savedblocks = blocks
return
}
func testFetchHeightRange(t *testing.T, db database.Db, blocks []*btcutil.Block) {
var testincrement int64 = 50
var testcnt int64 = 100
shanames := make([]*btcwire.ShaHash, len(blocks))
nBlocks := int64(len(blocks))
for i := range blocks {
blockSha, err := blocks[i].Sha()
if err != nil {
t.Errorf("FetchHeightRange: unexpected failure computing block sah %v", err)
}
shanames[i] = blockSha
}
for startheight := int64(0); startheight < nBlocks; startheight += testincrement {
endheight := startheight + testcnt
if endheight > nBlocks {
endheight = database.AllShas
}
shalist, err := db.FetchHeightRange(startheight, endheight)
if err != nil {
t.Errorf("FetchHeightRange: unexpected failure looking up shas %v", err)
}
if endheight == database.AllShas {
if int64(len(shalist)) != nBlocks-startheight {
t.Errorf("FetchHeightRange: expected A %v shas, got %v", nBlocks-startheight, len(shalist))
}
} else {
if int64(len(shalist)) != testcnt {
t.Errorf("FetchHeightRange: expected %v shas, got %v", testcnt, len(shalist))
}
}
for i := range shalist {
sha0 := *shanames[int64(i)+startheight]
sha1 := shalist[i]
if sha0 != sha1 {
t.Errorf("FetchHeightRange: mismatch sha at %v requested range %v %v: %v %v ", int64(i)+startheight, startheight, endheight, sha0, sha1)
}
}
}
}
func TestLimitAndSkipFetchTxsForAddr(t *testing.T) {
testDb, err := setUpTestDb(t)
defer testDb.cleanUpFunc()
// Insert a block with some fake test transactions. The block will have
// 10 copies of a fake transaction involving same address.
addrString := "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
targetAddr, err := btcutil.DecodeAddress(addrString, &btcnet.MainNetParams)
if err != nil {
t.Fatalf("Unable to decode test address: %v", err)
}
outputScript, err := btcscript.PayToAddrScript(targetAddr)
if err != nil {
t.Fatalf("Unable make test pkScript %v", err)
}
fakeTxOut := btcwire.NewTxOut(10, outputScript)
var emptyHash btcwire.ShaHash
fakeHeader := btcwire.NewBlockHeader(&emptyHash, &emptyHash, 1, 1)
msgBlock := btcwire.NewMsgBlock(fakeHeader)
for i := 0; i < 10; i++ {
mtx := btcwire.NewMsgTx()
mtx.AddTxOut(fakeTxOut)
msgBlock.AddTransaction(mtx)
}
// Insert the test block into the DB.
testBlock := btcutil.NewBlock(msgBlock)
newheight, err := testDb.db.InsertBlock(testBlock)
if err != nil {
t.Fatalf("Unable to insert block into db: %v", err)
}
// Create and insert an address index for out test addr.
txLoc, _ := testBlock.TxLoc()
index := make(database.BlockAddrIndex)
for i := range testBlock.Transactions() {
var hash160 [ripemd160.Size]byte
scriptAddr := targetAddr.ScriptAddress()
copy(hash160[:], scriptAddr[:])
index[hash160] = append(index[hash160], &txLoc[i])
}
blkSha, _ := testBlock.Sha()
err = testDb.db.UpdateAddrIndexForBlock(blkSha, newheight, index)
if err != nil {
t.Fatalf("UpdateAddrIndexForBlock: failed to index"+
" addrs for block #%d (%s) "+
"err %v", newheight, blkSha, err)
return
}
// Try skipping the first 4 results, should get 6 in return.
txReply, err := testDb.db.FetchTxsForAddr(targetAddr, 4, 100000)
if err != nil {
t.Fatalf("Unable to fetch transactions for address: %v", err)
}
if len(txReply) != 6 {
t.Fatalf("Did not correctly skip forward in txs for address reply"+
" got %v txs, expected %v", len(txReply), 6)
}
// Limit the number of results to 3.
txReply, err = testDb.db.FetchTxsForAddr(targetAddr, 0, 3)
if err != nil {
t.Fatalf("Unable to fetch transactions for address: %v", err)
}
if len(txReply) != 3 {
t.Fatalf("Did not correctly limit in txs for address reply"+
" got %v txs, expected %v", len(txReply), 3)
}
// Skip 1, limit 5.
txReply, err = testDb.db.FetchTxsForAddr(targetAddr, 1, 5)
if err != nil {
t.Fatalf("Unable to fetch transactions for address: %v", err)
}
if len(txReply) != 5 {
t.Fatalf("Did not correctly limit in txs for address reply"+
" got %v txs, expected %v", len(txReply), 5)
}
}