0280fa0264
This commit converts all block height references to int32 instead of int64. The current target block production rate is 10 mins per block which means it will take roughly 40,800 years to reach the maximum height an int32 affords. Even if the target rate were lowered to one block per minute, it would still take roughly another 4,080 years to reach the maximum. In the mean time, there is no reason to use a larger type which results in higher memory and disk space usage. However, for now, in order to avoid having to reserialize a bunch of database information, the heights are still serialized to the database as 8-byte uint64s. This is being mainly being done in preparation for further upcoming infrastructure changes which will use the smaller and more efficient 4-byte serialization in the database as well.
586 lines
16 KiB
Go
586 lines
16 KiB
Go
// Copyright (c) 2013-2014 The btcsuite developers
|
|
// 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"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/database"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/golangcrypto/ripemd160"
|
|
)
|
|
|
|
var network = wire.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, dbname string) (*testDb, error) {
|
|
// Ignore db remove errors since it means we didn't have an old one.
|
|
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 *wire.ShaHash, newestBlockIdx int32) {
|
|
// 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 wire.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 := txscript.ExtractPkScriptAddrs(testTx.MsgTx().TxOut[0].PkScript, &chaincfg.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] = []*wire.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", "tstdbopmode")
|
|
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 *wire.ShaHash, newestBlockIdx int32) {
|
|
// 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, "tstdbopmode")
|
|
if err != nil {
|
|
t.Errorf("Failed to open test database %v", err)
|
|
return
|
|
}
|
|
defer testDb.cleanUpFunc()
|
|
err = nil
|
|
out:
|
|
for height := int32(0); height < int32(len(testDb.blocks)); height++ {
|
|
block := testDb.blocks[height]
|
|
mblock := block.MsgBlock()
|
|
var txneededList []*wire.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, "tstdbbackout")
|
|
if err != nil {
|
|
t.Errorf("Failed to open test database %v", err)
|
|
return
|
|
}
|
|
defer testDb.cleanUpFunc()
|
|
|
|
if len(testDb.blocks) < 120 {
|
|
t.Errorf("test data too small")
|
|
return
|
|
}
|
|
|
|
err = nil
|
|
for height := int32(0); height < int32(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 := testDb.blocks[99].Sha()
|
|
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 = testDb.blocks[119].Sha()
|
|
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 := 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(chaincfg.MainNetParams.GenesisBlock)
|
|
blocks = append(blocks, genesis)
|
|
|
|
var block *btcutil.Block
|
|
err = nil
|
|
for height := int32(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 int32 = 50
|
|
var testcnt int32 = 100
|
|
|
|
shanames := make([]*wire.ShaHash, len(blocks))
|
|
|
|
nBlocks := int32(len(blocks))
|
|
|
|
for i := range blocks {
|
|
shanames[i] = blocks[i].Sha()
|
|
}
|
|
|
|
for startheight := int32(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 int32(len(shalist)) != nBlocks-startheight {
|
|
t.Errorf("FetchHeightRange: expected A %v shas, got %v", nBlocks-startheight, len(shalist))
|
|
}
|
|
} else {
|
|
if int32(len(shalist)) != testcnt {
|
|
t.Errorf("FetchHeightRange: expected %v shas, got %v", testcnt, len(shalist))
|
|
}
|
|
}
|
|
|
|
for i := range shalist {
|
|
sha0 := *shanames[int32(i)+startheight]
|
|
sha1 := shalist[i]
|
|
if sha0 != sha1 {
|
|
t.Errorf("FetchHeightRange: mismatch sha at %v requested range %v %v: %v %v ", int32(i)+startheight, startheight, endheight, sha0, sha1)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestLimitAndSkipFetchTxsForAddr(t *testing.T) {
|
|
testDb, err := setUpTestDb(t, "tstdbtxaddr")
|
|
if err != nil {
|
|
t.Errorf("Failed to open test database %v", err)
|
|
return
|
|
}
|
|
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, &chaincfg.MainNetParams)
|
|
if err != nil {
|
|
t.Fatalf("Unable to decode test address: %v", err)
|
|
}
|
|
outputScript, err := txscript.PayToAddrScript(targetAddr)
|
|
if err != nil {
|
|
t.Fatalf("Unable make test pkScript %v", err)
|
|
}
|
|
fakeTxOut := wire.NewTxOut(10, outputScript)
|
|
var emptyHash wire.ShaHash
|
|
fakeHeader := wire.NewBlockHeader(&emptyHash, &emptyHash, 1, 1)
|
|
msgBlock := wire.NewMsgBlock(fakeHeader)
|
|
for i := 0; i < 10; i++ {
|
|
mtx := wire.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)
|
|
}
|
|
}
|