511 lines
15 KiB
Go
511 lines
15 KiB
Go
// Copyright (c) 2013-2014 The btcsuite developers
|
|
// Copyright (c) 2015 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ldb_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/database"
|
|
_ "github.com/decred/dcrd/database/ldb"
|
|
"github.com/decred/dcrd/wire"
|
|
"github.com/decred/dcrutil"
|
|
)
|
|
|
|
var tstBlocks []*dcrutil.Block
|
|
|
|
func loadblocks(t *testing.T) []*dcrutil.Block {
|
|
if len(tstBlocks) != 0 {
|
|
return tstBlocks
|
|
}
|
|
|
|
testdatafile := filepath.Join("..", "/../blockchain/testdata", "blocks0to168.bz2")
|
|
blocks, err := loadBlocks(t, testdatafile)
|
|
if err != nil {
|
|
t.Errorf("Unable to load blocks from test data: %v", err)
|
|
return nil
|
|
}
|
|
tstBlocks = blocks
|
|
return blocks
|
|
}
|
|
|
|
func TestUnspentInsert(t *testing.T) {
|
|
testUnspentInsertStakeTree(t)
|
|
testUnspentInsertRegTree(t)
|
|
}
|
|
|
|
// insert every block in the test chain
|
|
// after each insert, fetch all the tx affected by the latest
|
|
// block and verify that the the tx is spent/unspent
|
|
// new tx should be fully unspent, referenced tx should have
|
|
// the associated txout set to spent.
|
|
// checks tx tree stake only
|
|
func testUnspentInsertStakeTree(t *testing.T) {
|
|
// Ignore db remove errors since it means we didn't have an old one.
|
|
dbname := fmt.Sprintf("tstdbuspnt1")
|
|
dbnamever := dbname + ".ver"
|
|
_ = os.RemoveAll(dbname)
|
|
_ = os.RemoveAll(dbnamever)
|
|
db, err := database.CreateDB("leveldb", dbname)
|
|
if err != nil {
|
|
t.Errorf("Failed to open test database %v", err)
|
|
return
|
|
}
|
|
defer os.RemoveAll(dbname)
|
|
defer os.RemoveAll(dbnamever)
|
|
defer func() {
|
|
if err := db.Close(); err != nil {
|
|
t.Errorf("Close: unexpected error: %v", err)
|
|
}
|
|
}()
|
|
blocks := loadblocks(t)
|
|
endtest:
|
|
for height := int64(0); height < int64(len(blocks))-1; height++ {
|
|
block := blocks[height]
|
|
|
|
var txneededList []*chainhash.Hash
|
|
var txlookupList []*chainhash.Hash
|
|
var txOutList []*chainhash.Hash
|
|
var txInList []*wire.OutPoint
|
|
spentFromParent := make(map[wire.OutPoint]struct{})
|
|
for _, tx := range block.MsgBlock().STransactions {
|
|
for _, txin := range tx.TxIn {
|
|
if txin.PreviousOutPoint.Index == uint32(4294967295) {
|
|
continue
|
|
}
|
|
origintxsha := &txin.PreviousOutPoint.Hash
|
|
|
|
exists, err := db.ExistsTxSha(origintxsha)
|
|
if err != nil {
|
|
t.Errorf("ExistsTxSha: unexpected error %v ", err)
|
|
}
|
|
if !exists {
|
|
// Check and see if the outpoint references txtreeregular of
|
|
// the previous block. If it does, make sure nothing in tx
|
|
// treeregular spends it in flight. Then check make sure it's
|
|
// not currently spent for this block. If it isn't, mark it
|
|
// spent and skip lookup in the db below, since the db won't
|
|
// yet be able to add it as it's still to be inserted.
|
|
spentFromParentReg := false
|
|
parent := blocks[height-1]
|
|
parentValid := dcrutil.IsFlagSet16(dcrutil.BlockValid, block.MsgBlock().Header.VoteBits)
|
|
if parentValid {
|
|
for _, prtx := range parent.Transactions() {
|
|
// Check and make sure it's not being spent in this tx
|
|
// tree first by an in flight tx. Mark it spent if it
|
|
// is so it fails the check below.
|
|
for _, prtxCheck := range parent.Transactions() {
|
|
for _, prTxIn := range prtxCheck.MsgTx().TxIn {
|
|
if prTxIn.PreviousOutPoint == txin.PreviousOutPoint {
|
|
spentFromParent[txin.PreviousOutPoint] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If it is in the tree, make sure it's not already spent
|
|
// somewhere else and mark it spent. Set the flag below
|
|
// so we skip lookup.
|
|
if prtx.Sha().IsEqual(origintxsha) {
|
|
if _, spent := spentFromParent[txin.PreviousOutPoint]; !spent {
|
|
spentFromParent[txin.PreviousOutPoint] = struct{}{}
|
|
spentFromParentReg = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !spentFromParentReg {
|
|
t.Errorf("referenced tx not found %v %v", origintxsha, height)
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
txInList = append(txInList, &txin.PreviousOutPoint)
|
|
txneededList = append(txneededList, origintxsha)
|
|
txlookupList = append(txlookupList, origintxsha)
|
|
}
|
|
txshaname := tx.TxSha()
|
|
txlookupList = append(txlookupList, &txshaname)
|
|
txOutList = append(txOutList, &txshaname)
|
|
}
|
|
|
|
txneededmap := map[chainhash.Hash]*database.TxListReply{}
|
|
txlist := 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 endtest
|
|
}
|
|
txneededmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txneededmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == true {
|
|
t.Errorf("txin %v:%v is already spent", spend.Hash, spend.Index)
|
|
}
|
|
}
|
|
|
|
newheight, err := db.InsertBlock(block)
|
|
if err != nil {
|
|
t.Errorf("failed to insert block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
if newheight != height {
|
|
t.Errorf("height mismatch expect %v returned %v", height, newheight)
|
|
break endtest
|
|
}
|
|
// only check transactions if current block is valid
|
|
txlookupmap := map[chainhash.Hash]*database.TxListReply{}
|
|
txlist = db.FetchTxByShaList(txlookupList)
|
|
for _, txe := range txlist {
|
|
if txe.Err != nil {
|
|
t.Errorf("tx list fetch failed %v err %v ", txe.Sha, txe.Err)
|
|
break endtest
|
|
}
|
|
txlookupmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txlookupmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == false {
|
|
t.Errorf("txin %v:%v is unspent %v", spend.Hash, spend.Index, itxe.TxSpent)
|
|
}
|
|
}
|
|
for _, txo := range txOutList {
|
|
itxe := txlookupmap[*txo]
|
|
for i, spent := range itxe.TxSpent {
|
|
if spent == true {
|
|
t.Errorf("height: %v freshly inserted tx %v already spent %v", height, txo, i)
|
|
}
|
|
}
|
|
|
|
}
|
|
if len(txInList) == 0 {
|
|
continue
|
|
}
|
|
dropblock := blocks[height-1]
|
|
|
|
err = db.DropAfterBlockBySha(dropblock.Sha())
|
|
if err != nil {
|
|
t.Errorf("failed to drop block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
|
|
txlookupmap = map[chainhash.Hash]*database.TxListReply{}
|
|
txlist = db.FetchUnSpentTxByShaList(txlookupList)
|
|
for _, txe := range txlist {
|
|
if txe.Err != nil {
|
|
if _, ok := txneededmap[*txe.Sha]; ok {
|
|
t.Errorf("tx list fetch failed %v err %v ", txe.Sha, txe.Err)
|
|
break endtest
|
|
}
|
|
}
|
|
txlookupmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txlookupmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == true {
|
|
t.Errorf("txin %v:%v is unspent %v", spend.Hash, spend.Index, itxe.TxSpent)
|
|
}
|
|
}
|
|
newheight, err = db.InsertBlock(block)
|
|
if err != nil {
|
|
t.Errorf("failed to insert block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
txlookupmap = map[chainhash.Hash]*database.TxListReply{}
|
|
txlist = db.FetchTxByShaList(txlookupList)
|
|
for _, txe := range txlist {
|
|
if txe.Err != nil {
|
|
t.Errorf("tx list fetch failed %v err %v ", txe.Sha, txe.Err)
|
|
break endtest
|
|
}
|
|
txlookupmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txlookupmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == false {
|
|
t.Errorf("txin %v:%v is unspent %v", spend.Hash, spend.Index, itxe.TxSpent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// getRegTreeOpsSpentBeforeThisOp returns all outpoints spent before this one
|
|
// in the block's tx tree regular. used for checking vs in flight tx.
|
|
func getRegTreeOpsSpentBeforeThisOp(block *dcrutil.Block, idx int, txinIdx int) map[wire.OutPoint]struct{} {
|
|
spentOps := make(map[wire.OutPoint]struct{})
|
|
|
|
thisTx := block.Transactions()[idx]
|
|
for i, txIn := range thisTx.MsgTx().TxIn {
|
|
if i < txinIdx {
|
|
spentOps[txIn.PreviousOutPoint] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for i, tx := range block.Transactions() {
|
|
if i < idx {
|
|
for _, txIn := range tx.MsgTx().TxIn {
|
|
spentOps[txIn.PreviousOutPoint] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
return spentOps
|
|
}
|
|
|
|
// unspendInflightTxTree returns all outpoints spent that reference internal
|
|
// transactions in a TxTreeRegular.
|
|
func unspendInflightTxTree(block *dcrutil.Block) map[wire.OutPoint]struct{} {
|
|
unspentOps := make(map[wire.OutPoint]struct{})
|
|
allTxHashes := make(map[chainhash.Hash]struct{})
|
|
for _, tx := range block.Transactions() {
|
|
h := tx.Sha()
|
|
allTxHashes[*h] = struct{}{}
|
|
}
|
|
|
|
for _, tx := range block.Transactions() {
|
|
for _, txIn := range tx.MsgTx().TxIn {
|
|
if _, isLocal := allTxHashes[txIn.PreviousOutPoint.Hash]; isLocal {
|
|
unspentOps[txIn.PreviousOutPoint] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
return unspentOps
|
|
}
|
|
|
|
// unspendStakeTxTree returns all outpoints spent before this one
|
|
// in the block's tx tree stake. used for unspending the stake tx
|
|
// tree to evaluate tx tree regular of prev block.
|
|
func unspendStakeTxTree(block *dcrutil.Block) map[wire.OutPoint]struct{} {
|
|
unspentOps := make(map[wire.OutPoint]struct{})
|
|
|
|
for _, tx := range block.STransactions() {
|
|
for _, txIn := range tx.MsgTx().TxIn {
|
|
unspentOps[txIn.PreviousOutPoint] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return unspentOps
|
|
}
|
|
|
|
// insert every block in the test chain
|
|
// after each insert, fetch all the tx affected by the latest
|
|
// block and verify that the the tx is spent/unspent
|
|
// new tx should be fully unspent, referenced tx should have
|
|
// the associated txout set to spent.
|
|
// checks tx tree regular only
|
|
func testUnspentInsertRegTree(t *testing.T) {
|
|
// Ignore db remove errors since it means we didn't have an old one.
|
|
dbname := fmt.Sprintf("tstdbuspnt1")
|
|
dbnamever := dbname + ".ver"
|
|
_ = os.RemoveAll(dbname)
|
|
_ = os.RemoveAll(dbnamever)
|
|
db, err := database.CreateDB("leveldb", dbname)
|
|
if err != nil {
|
|
t.Errorf("Failed to open test database %v", err)
|
|
return
|
|
}
|
|
defer os.RemoveAll(dbname)
|
|
defer os.RemoveAll(dbnamever)
|
|
defer func() {
|
|
if err := db.Close(); err != nil {
|
|
t.Errorf("Close: unexpected error: %v", err)
|
|
}
|
|
}()
|
|
blocks := loadblocks(t)
|
|
endtest:
|
|
for height := int64(0); height < int64(len(blocks))-1; height++ {
|
|
block := blocks[height]
|
|
|
|
// jam in genesis block
|
|
if height == 0 {
|
|
_, err := db.InsertBlock(block)
|
|
if err != nil {
|
|
t.Errorf("failed to insert block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
continue
|
|
}
|
|
|
|
var txneededList []*chainhash.Hash
|
|
var txlookupList []*chainhash.Hash
|
|
var txOutList []*chainhash.Hash
|
|
var txInList []*wire.OutPoint
|
|
parent := blocks[height-1]
|
|
unspentStakeOps := unspendStakeTxTree(block)
|
|
|
|
// Check regular tree of parent and make sure it's ok
|
|
for txIdx, tx := range parent.MsgBlock().Transactions {
|
|
for txinIdx, txin := range tx.TxIn {
|
|
if txin.PreviousOutPoint.Index == uint32(4294967295) {
|
|
continue
|
|
}
|
|
|
|
origintxsha := &txin.PreviousOutPoint.Hash
|
|
|
|
exists, err := db.ExistsTxSha(origintxsha)
|
|
if err != nil {
|
|
t.Errorf("ExistsTxSha: unexpected error %v ", err)
|
|
}
|
|
if !exists {
|
|
// Check and see if something in flight spends it from this
|
|
// tx tree. We can skip looking for this transaction OP
|
|
// if that's the case.
|
|
spentFromParentReg := false
|
|
alreadySpentOps := getRegTreeOpsSpentBeforeThisOp(parent,
|
|
txIdx, txinIdx)
|
|
_, alreadySpent := alreadySpentOps[txin.PreviousOutPoint]
|
|
if !alreadySpent {
|
|
spentFromParentReg = true
|
|
}
|
|
|
|
if !spentFromParentReg {
|
|
t.Errorf("referenced tx not found %v %v", origintxsha,
|
|
height)
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
txInList = append(txInList, &txin.PreviousOutPoint)
|
|
txneededList = append(txneededList, origintxsha)
|
|
txlookupList = append(txlookupList, origintxsha)
|
|
}
|
|
txshaname := tx.TxSha()
|
|
txlookupList = append(txlookupList, &txshaname)
|
|
txOutList = append(txOutList, &txshaname)
|
|
}
|
|
|
|
txneededmap := map[chainhash.Hash]*database.TxListReply{}
|
|
txlist := 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 endtest
|
|
}
|
|
txneededmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txneededmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == true {
|
|
t.Errorf("txin %v:%v is already spent", spend.Hash, spend.Index)
|
|
}
|
|
}
|
|
|
|
newheight, err := db.InsertBlock(block)
|
|
if err != nil {
|
|
t.Errorf("failed to insert block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
if newheight != height {
|
|
t.Errorf("height mismatch expect %v returned %v", height, newheight)
|
|
break endtest
|
|
}
|
|
// only check transactions if current block validates parent block
|
|
if !dcrutil.IsFlagSet16(block.MsgBlock().Header.VoteBits, dcrutil.BlockValid) {
|
|
continue
|
|
}
|
|
|
|
txlookupmap := map[chainhash.Hash]*database.TxListReply{}
|
|
txlist = db.FetchTxByShaList(txlookupList)
|
|
for _, txe := range txlist {
|
|
if txe.Err != nil {
|
|
t.Errorf("tx list fetch failed %v err %v (height %v)", txe.Sha,
|
|
txe.Err, height)
|
|
break endtest
|
|
}
|
|
txlookupmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txlookupmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == false {
|
|
t.Errorf("txin %v:%v is unspent %v", spend.Hash, spend.Index, itxe.TxSpent)
|
|
}
|
|
}
|
|
|
|
alreadySpentOps := unspendInflightTxTree(parent)
|
|
for _, txo := range txOutList {
|
|
itxe := txlookupmap[*txo]
|
|
for i, spent := range itxe.TxSpent {
|
|
if spent == true {
|
|
// If this was spent in flight, skip
|
|
thisOP := wire.OutPoint{
|
|
Hash: *txo,
|
|
Index: uint32(i),
|
|
Tree: dcrutil.TxTreeRegular,
|
|
}
|
|
_, alreadySpent := alreadySpentOps[thisOP]
|
|
if alreadySpent {
|
|
continue
|
|
}
|
|
|
|
// If it was spent in the stake tree it's actually unspent too
|
|
_, wasSpentInStakeTree := unspentStakeOps[thisOP]
|
|
if wasSpentInStakeTree {
|
|
continue
|
|
}
|
|
|
|
t.Errorf("height: %v freshly inserted tx %v already spent %v", height, txo, i)
|
|
}
|
|
}
|
|
|
|
}
|
|
if len(txInList) == 0 {
|
|
continue
|
|
}
|
|
dropblock := blocks[height-1]
|
|
|
|
err = db.DropAfterBlockBySha(dropblock.Sha())
|
|
if err != nil {
|
|
t.Errorf("failed to drop block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
|
|
txlookupmap = map[chainhash.Hash]*database.TxListReply{}
|
|
txlist = db.FetchUnSpentTxByShaList(txlookupList)
|
|
for _, txe := range txlist {
|
|
if txe.Err != nil {
|
|
if _, ok := txneededmap[*txe.Sha]; ok {
|
|
t.Errorf("tx list fetch failed %v err %v ", txe.Sha, txe.Err)
|
|
break endtest
|
|
}
|
|
}
|
|
txlookupmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txlookupmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == true {
|
|
t.Errorf("txin %v:%v is unspent %v", spend.Hash, spend.Index, itxe.TxSpent)
|
|
}
|
|
}
|
|
newheight, err = db.InsertBlock(block)
|
|
if err != nil {
|
|
t.Errorf("failed to insert block %v err %v", height, err)
|
|
break endtest
|
|
}
|
|
txlookupmap = map[chainhash.Hash]*database.TxListReply{}
|
|
txlist = db.FetchTxByShaList(txlookupList)
|
|
for _, txe := range txlist {
|
|
if txe.Err != nil {
|
|
t.Errorf("tx list fetch failed %v err %v ", txe.Sha, txe.Err)
|
|
break endtest
|
|
}
|
|
txlookupmap[*txe.Sha] = txe
|
|
}
|
|
for _, spend := range txInList {
|
|
itxe := txlookupmap[spend.Hash]
|
|
if itxe.TxSpent[spend.Index] == false {
|
|
t.Errorf("txin %v:%v is unspent %v", spend.Hash, spend.Index, itxe.TxSpent)
|
|
}
|
|
}
|
|
}
|
|
}
|