d574a3af6d
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.
597 lines
16 KiB
Go
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)
|
|
}
|
|
}
|