Cleanup after insert failure, do not leave inconsistant db.
Fix error returns in InsertBlock and FetchBlockBySha Give up on return by name in InsertBlock() and return explicit err one location in FetchBlockBySha to return proper error value
This commit is contained in:
parent
d8c3213d99
commit
3b743e4cfc
4 changed files with 222 additions and 11 deletions
141
sqlite3/insertfail_test.go
Normal file
141
sqlite3/insertfail_test.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) 2013 Conformal Systems LLC.
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"github.com/conformal/btcdb"
|
||||
"github.com/conformal/btcdb/sqlite3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFailOperational(t *testing.T) {
|
||||
sqlite3.SetTestingT(t)
|
||||
failtestOperationalMode(t, dbTmDefault)
|
||||
failtestOperationalMode(t, dbTmNormal)
|
||||
failtestOperationalMode(t, dbTmFast)
|
||||
failtestOperationalMode(t, dbTmNoVerify)
|
||||
}
|
||||
|
||||
func failtestOperationalMode(t *testing.T, mode int) {
|
||||
// simplified basic operation is:
|
||||
// 1) fetch block from remote server
|
||||
// 2) look up all txin (except coinbase in db)
|
||||
// 3) insert block
|
||||
|
||||
// Ignore db remove errors since it means we didn't have an old one.
|
||||
dbname := "tstdbop1"
|
||||
_ = os.Remove(dbname)
|
||||
db, err := btcdb.CreateDB("sqlite", dbname)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to open test database %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(dbname)
|
||||
defer db.Close()
|
||||
|
||||
switch mode {
|
||||
case dbTmDefault: // default
|
||||
// no setup
|
||||
case dbTmNormal: // explicit normal
|
||||
db.SetDBInsertMode(btcdb.InsertNormal)
|
||||
case dbTmFast: // fast mode
|
||||
db.SetDBInsertMode(btcdb.InsertFast)
|
||||
if sqldb, ok := db.(*sqlite3.SqliteDb); ok {
|
||||
sqldb.TempTblMax = 100
|
||||
} else {
|
||||
t.Errorf("not right type")
|
||||
}
|
||||
case dbTmNoVerify: // validated block
|
||||
// no point in testing this
|
||||
return
|
||||
}
|
||||
|
||||
// Since we are dealing with small dataset, reduce cache size
|
||||
sqlite3.SetBlockCacheSize(db, 2)
|
||||
sqlite3.SetTxCacheSize(db, 3)
|
||||
|
||||
testdatafile := filepath.Join("testdata", "blocks1-256.bz2")
|
||||
blocks, err := loadBlocks(t, testdatafile)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to load blocks from test data for mode %v: %v",
|
||||
mode, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = nil
|
||||
out:
|
||||
for height := int64(0); height < int64(len(blocks)); height++ {
|
||||
block := blocks[height]
|
||||
|
||||
mblock := block.MsgBlock()
|
||||
blockname, _ := block.Sha()
|
||||
|
||||
if height == 248 {
|
||||
// time to corrupt the datbase, to see if it leaves the block or tx in the db
|
||||
if len(mblock.Transactions) != 2 {
|
||||
t.Errorf("transaction #248 should have two transactions txid %v ?= 828ef3b079f9c23829c56fe86e85b4a69d9e06e5b54ea597eef5fb3ffef509fe", blockname)
|
||||
return
|
||||
}
|
||||
tx := mblock.Transactions[1]
|
||||
txin := tx.TxIn[0]
|
||||
origintxsha := &txin.PreviousOutpoint.Hash
|
||||
sqlite3.KillTx(db, origintxsha)
|
||||
_, _, _, _, err = db.FetchTxAllBySha(origintxsha)
|
||||
if err == nil {
|
||||
t.Errorf("deleted tx found %v", origintxsha)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if height == 248 {
|
||||
}
|
||||
newheight, err := db.InsertBlock(block)
|
||||
if err != nil {
|
||||
if height != 248 {
|
||||
t.Errorf("failed to insert block %v err %v", height, err)
|
||||
break out
|
||||
}
|
||||
} else {
|
||||
if height == 248 {
|
||||
t.Errorf("block insert with missing input tx succeeded block %v err %v", height, err)
|
||||
break out
|
||||
}
|
||||
}
|
||||
if height == 248 {
|
||||
for _, tx := range mblock.Transactions {
|
||||
txsha, err := tx.TxSha(block.ProtocolVersion())
|
||||
_, _, _, _, err = db.FetchTxAllBySha(&txsha)
|
||||
if err == nil {
|
||||
t.Errorf("referenced tx found, should not have been %v, ", txsha)
|
||||
}
|
||||
}
|
||||
}
|
||||
if height == 248 {
|
||||
exists := db.ExistsSha(blockname)
|
||||
if exists == true {
|
||||
t.Errorf("block still present after failed insert")
|
||||
}
|
||||
// if we got here with no error, testing was successful
|
||||
break out
|
||||
}
|
||||
if newheight != height {
|
||||
t.Errorf("height mismatch expect %v returned %v", height, newheight)
|
||||
break out
|
||||
}
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case dbTmDefault: // default
|
||||
// no cleanup
|
||||
case dbTmNormal: // explicit normal
|
||||
// no cleanup
|
||||
case dbTmFast: // fast mode
|
||||
db.SetDBInsertMode(btcdb.InsertNormal)
|
||||
case dbTmNoVerify: // validated block
|
||||
db.SetDBInsertMode(btcdb.InsertNormal)
|
||||
}
|
||||
}
|
|
@ -8,8 +8,15 @@ import (
|
|||
"fmt"
|
||||
"github.com/conformal/btcdb"
|
||||
"github.com/conformal/btcwire"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var t *testing.T
|
||||
|
||||
func SetTestingT(t_arg *testing.T) {
|
||||
t = t_arg
|
||||
}
|
||||
|
||||
// FetchSha returns the datablock and pver for the given ShaHash.
|
||||
// This is a testing only interface.
|
||||
func FetchSha(db btcdb.Db, sha *btcwire.ShaHash) (buf []byte, pver uint32,
|
||||
|
@ -46,3 +53,32 @@ func SetTxCacheSize(db btcdb.Db, newsize int) {
|
|||
tc := &sqldb.txCache
|
||||
tc.maxcount = newsize
|
||||
}
|
||||
|
||||
// KillTx is a function that deletes a transaction from the database
|
||||
// this should only be used for testing purposes to valiate error paths
|
||||
// in the database. This is _expected_ to leave the database in an
|
||||
// inconsistant state.
|
||||
func KillTx(dbarg btcdb.Db, txsha *btcwire.ShaHash) {
|
||||
db, ok := dbarg.(*SqliteDb)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
db.endTx(false)
|
||||
db.startTx()
|
||||
tx := &db.txState
|
||||
key := txsha.String()
|
||||
_, err := tx.tx.Exec("DELETE FROM txtmp WHERE key == ?", key)
|
||||
if err != nil {
|
||||
log.Warnf("error deleting tx %v from txtmp", txsha)
|
||||
}
|
||||
_, err = tx.tx.Exec("DELETE FROM tx WHERE key == ?", key)
|
||||
if err != nil {
|
||||
log.Warnf("error deleting tx %v from tx (%v)", txsha, key)
|
||||
}
|
||||
err = db.endTx(true)
|
||||
if err != nil {
|
||||
// XXX
|
||||
db.endTx(false)
|
||||
}
|
||||
db.InvalidateCache()
|
||||
}
|
||||
|
|
|
@ -575,7 +575,12 @@ func (db *SqliteDb) DropAfterBlockBySha(sha *btcwire.ShaHash) (err error) {
|
|||
// lookup to unspend coins in them
|
||||
db.InvalidateCache()
|
||||
|
||||
_, err = tx.tx.Exec("DELETE FROM txtmp WHERE blockid > ?", keepidx)
|
||||
return db.delFromDB(keepidx)
|
||||
}
|
||||
|
||||
func (db *SqliteDb) delFromDB(keepidx int64) (error) {
|
||||
tx := &db.txState
|
||||
_, err := tx.tx.Exec("DELETE FROM txtmp WHERE blockid > ?", keepidx)
|
||||
if err != nil {
|
||||
// XXX
|
||||
db.endTx(false)
|
||||
|
@ -601,32 +606,33 @@ func (db *SqliteDb) DropAfterBlockBySha(sha *btcwire.ShaHash) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// InsertBlock inserts raw block and transaction data from a block into the
|
||||
// database. The first block inserted into the database will be treated as the
|
||||
// genesis block. Every subsequent block insert requires the referenced parent
|
||||
// block to already exist.
|
||||
func (db *SqliteDb) InsertBlock(block *btcutil.Block) (height int64, err error) {
|
||||
func (db *SqliteDb) InsertBlock(block *btcutil.Block) (int64, error) {
|
||||
db.dbLock.Lock()
|
||||
defer db.dbLock.Unlock()
|
||||
|
||||
blocksha, err := block.Sha()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to compute block sha %v", blocksha)
|
||||
return
|
||||
return -1, err
|
||||
}
|
||||
|
||||
mblock := block.MsgBlock()
|
||||
rawMsg, pver, err := block.Bytes()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to obtain raw block sha %v", blocksha)
|
||||
return
|
||||
return -1, err
|
||||
}
|
||||
txloc, err := block.TxLoc()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to obtain raw block sha %v", blocksha)
|
||||
return
|
||||
return -1, err
|
||||
}
|
||||
|
||||
// Insert block into database
|
||||
|
@ -635,9 +641,32 @@ func (db *SqliteDb) InsertBlock(block *btcutil.Block) (height int64, err error)
|
|||
if err != nil {
|
||||
log.Warnf("Failed to insert block %v %v %v", blocksha,
|
||||
&mblock.Header.PrevBlock, err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
txinsertidx := -1
|
||||
success := false
|
||||
|
||||
defer func() {
|
||||
if success {
|
||||
return
|
||||
}
|
||||
|
||||
for txidx := 0; txidx <= txinsertidx; txidx++ {
|
||||
tx := mblock.Transactions[txidx]
|
||||
|
||||
err = db.unSpend(tx)
|
||||
if err != nil {
|
||||
log.Warnf("unSpend error during block insert unwind %v %v %v", blocksha, txidx, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = db.delFromDB(newheight -1)
|
||||
if err != nil {
|
||||
log.Warnf("Error during block insert unwind %v %v", blocksha, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// At least two blocks in the long past were generated by faulty
|
||||
// miners, the sha of the transaction exists in a previous block,
|
||||
// detect this condition and 'accept' the block.
|
||||
|
@ -646,8 +675,12 @@ func (db *SqliteDb) InsertBlock(block *btcutil.Block) (height int64, err error)
|
|||
txsha, err = tx.TxSha(pver)
|
||||
if err != nil {
|
||||
log.Warnf("failed to compute tx name block %v idx %v err %v", blocksha, txidx, err)
|
||||
return
|
||||
return -1, err
|
||||
}
|
||||
|
||||
// num tx inserted, thus would need unwind if failure occurs
|
||||
txinsertidx = txidx
|
||||
|
||||
// Some old blocks contain duplicate transactions
|
||||
// Attempt to cleanly bypass this problem
|
||||
// http://blockexplorer.com/b/91842
|
||||
|
@ -687,15 +720,16 @@ func (db *SqliteDb) InsertBlock(block *btcutil.Block) (height int64, err error)
|
|||
oBlkIdx, _, _, err = db.fetchLocationBySha(&txsha)
|
||||
log.Warnf("oblkidx %v err %v", oBlkIdx, err)
|
||||
|
||||
return
|
||||
return -1, err
|
||||
}
|
||||
err = db.doSpend(tx)
|
||||
if err != nil {
|
||||
log.Warnf("block %v idx %v failed to spend tx %v %v err %v", blocksha, newheight, &txsha, txidx, err)
|
||||
|
||||
return
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
success = true
|
||||
db.syncPoint()
|
||||
return newheight, nil
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ func (db *SqliteDb) fetchBlockBySha(sha *btcwire.ShaHash) (blk *btcutil.Block, e
|
|||
|
||||
buf, pver, height, err := db.fetchSha(*sha)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blk, err = btcutil.NewBlockFromBytes(buf, pver)
|
||||
|
|
Loading…
Reference in a new issue