From 85d97d74369b8113f58045844cbd150008923dd8 Mon Sep 17 00:00:00 2001 From: Dale Rahn Date: Fri, 2 Aug 2013 18:31:21 -0400 Subject: [PATCH] block converted. --- leveldb/block.go | 293 ++++++++++++ leveldb/dbcache.go | 375 +++++++++++++++ leveldb/doc.go | 15 + leveldb/insertremove_test.go | 209 ++++++++ leveldb/internal_test.go | 48 ++ leveldb/leveldb.go | 798 +++++++++++++++++++++++++++++++ leveldb/operational_test.go | 420 ++++++++++++++++ leveldb/testdata/blocks1-256.bz2 | Bin 0 -> 37555 bytes leveldb/tx.go | 335 +++++++++++++ 9 files changed, 2493 insertions(+) create mode 100644 leveldb/block.go create mode 100644 leveldb/dbcache.go create mode 100644 leveldb/doc.go create mode 100644 leveldb/insertremove_test.go create mode 100644 leveldb/internal_test.go create mode 100644 leveldb/leveldb.go create mode 100644 leveldb/operational_test.go create mode 100644 leveldb/testdata/blocks1-256.bz2 create mode 100644 leveldb/tx.go diff --git a/leveldb/block.go b/leveldb/block.go new file mode 100644 index 00000000..145a89a7 --- /dev/null +++ b/leveldb/block.go @@ -0,0 +1,293 @@ +// 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 ldb + +import ( + "bytes" + "fmt" + "encoding/binary" + "errors" + "github.com/conformal/btcdb" + "github.com/conformal/btcwire" +) + +// InsertBlockData stores a block hash and its associated data block with a +// previous sha of `prevSha' and a version of `pver'. +func (db *LevelDb) InsertBlockData(sha *btcwire.ShaHash, prevSha *btcwire.ShaHash, pver uint32, buf []byte) (blockid int64, err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + return db.insertBlockData(sha, prevSha, pver, buf) +} + +func (db *LevelDb) getBlkLoc(sha *btcwire.ShaHash) (int64, int, error) { + var blkHeight int64 + var blkFile int + + key := sha.Bytes() + + data, err := db.bShaDb.Get(key, db.ro) + + if err != nil { + return 0, 0, err + } + + // deserialize + dr := bytes.NewBuffer(data) + err = binary.Read(dr, binary.LittleEndian, &blkHeight) + if err != nil { + err = errors.New("Db Corrupt") + return 0, 0, err + } + err = binary.Read(dr, binary.LittleEndian, &blkFile) + if err != nil { + err = errors.New("Db Corrupt") + return 0, 0, err + } + return blkHeight, blkFile, nil +} + +func (db *LevelDb) getBlkByHeight(blkHeight int64, blkFile int) (rsha *btcwire.ShaHash, rbuf []byte, err error) { + var blkVal []byte + + key := fmt.Sprintf("%d",blkHeight) + + blkVal, err = db.bBlkDb[blkFile].Get([]byte(key), db.ro) + if err != nil { + return // exists ??? + } + + var sha btcwire.ShaHash + + sha.SetBytes(blkVal[0:31]) + + return &sha, blkVal[32:], nil +} + +func (db *LevelDb) getBlk(sha *btcwire.ShaHash) (rblkHeight int64, rblkFile int, rbuf []byte, err error) { + var blkHeight int64 + var blkFile int + + blkHeight, blkFile, err = db.getBlkLoc(sha) + if err != nil { + return + } + + var buf []byte + + _, buf, err = db.getBlkByHeight(blkHeight, blkFile) + if err != nil { + return + } + return blkHeight, blkFile, buf, nil +} + +func (db *LevelDb) setBlk(sha *btcwire.ShaHash, blkHeight int64, blkFile int, buf []byte) (error) { + + // serialize + var lw bytes.Buffer + err := binary.Write(&lw, binary.LittleEndian, &blkHeight) + if err != nil { + err = errors.New("Write fail") + return err + } + err = binary.Write(&lw, binary.LittleEndian, &blkFile) + if err != nil { + err = errors.New("Write fail") + return err + } + key := sha.Bytes() + + err = db.bShaDb.Put(key, lw.Bytes(), db.wo) + + if err != nil { + return err + } + + key = []byte(fmt.Sprintf("%d",blkHeight)) + + shaB := sha.Bytes() + blkVal := make([]byte, len(shaB) + len(buf)) + copy (blkVal[0:], shaB) + copy (blkVal[len(shaB):], buf) + err = db.bBlkDb[blkFile].Put(key, blkVal, db.wo) + + return nil +} + +// insertSha stores a block hash and its associated data block with a +// previous sha of `prevSha' and a version of `pver'. +// insertSha shall be called with db lock held +func (db *LevelDb) insertBlockData(sha *btcwire.ShaHash, prevSha *btcwire.ShaHash, pver uint32, buf []byte) (blockid int64, err error) { + tx := &db.txState + if tx.tx == nil { + err = db.startTx() + if err != nil { + return + } + } + + oBlkHeight, _, err:= db.getBlkLoc(prevSha) + + if err != nil { + // check current block count + // if count != 0 { + // err = btcdb.PrevShaMissing + // return + // } + oBlkHeight = -1 + } + + // TODO(drahn) check curfile filesize, increment curfile if this puts it over + curFile := 0 + blkHeight := oBlkHeight - 1 + + err = db.setBlk(sha, blkHeight, curFile, buf) + + if err != nil { + return + } + + // update the last block cache + db.lastBlkShaCached = true + db.lastBlkSha = *sha + db.lastBlkIdx = blkHeight + + return blkHeight, nil +} + +// fetchSha returns the datablock and pver for the given ShaHash. +func (db *LevelDb) fetchSha(sha *btcwire.ShaHash) (rbuf []byte, rpver uint32, + rblkHeight int64, err error) { + var blkHeight int64 + var buf []byte + + blkHeight, _, buf, err = db.getBlk(sha) + if err != nil { + return + } + + fakepver := uint32(1) + + return buf, fakepver, blkHeight, nil +} + +// ExistsSha looks up the given block hash +// returns true if it is present in the database. +func (db *LevelDb) ExistsSha(sha *btcwire.ShaHash) (exists bool) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + _, exists = db.fetchBlockCache(sha) + if exists { + return + } + + // not in cache, try database + exists = db.blkExistsSha(sha) + return +} + +// blkExistsSha looks up the given block hash +// returns true if it is present in the database. +// CALLED WITH LOCK HELD +func (db *LevelDb) blkExistsSha(sha *btcwire.ShaHash) bool { + var pver uint32 + + oBlkHeight, _, err:= db.getBlkLoc(sha) + + if err != nil { + /* + should this warn if the failure is something besides does not exist ? + log.Warnf("blkExistsSha: fail %v", err) + */ + return false + } + return true +} + +// FetchBlockShaByHeight returns a block hash based on its height in the +// block chain. +func (db *LevelDb) FetchBlockShaByHeight(height int64) (sha *btcwire.ShaHash, err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + return db.fetchBlockShaByHeight(height) +} + +// fetchBlockShaByHeight returns a block hash based on its height in the +// block chain. +func (db *LevelDb) fetchBlockShaByHeight(height int64) (sha *btcwire.ShaHash, err error) { + + // TODO(drahn) figure out which file blkHeight is located + blkFile := 0 + var buf []byte + + _, buf, err = db.getBlkByHeight(height, blkFile) + + var shaval btcwire.ShaHash + shaval.SetBytes(buf[0:31]) + return &shaval, nil +} + +// FetchHeightRange looks up a range of blocks by the start and ending +// heights. Fetch is inclusive of the start height and exclusive of the +// ending height. To fetch all hashes from the start height until no +// more are present, use the special id `AllShas'. +func (db *LevelDb) FetchHeightRange(startHeight, endHeight int64) (rshalist []btcwire.ShaHash, err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + var endidx int64 + if endHeight == btcdb.AllShas { + endidx = startHeight + 500 + } else { + endidx = endHeight + } + + var shalist []btcwire.ShaHash + for height := startHeight; height < endidx; height++ { + // TODO(drahn) fix blkFile from height + blkFile := 0 + + key := fmt.Sprintf("%d", height) + blkVal, lerr := db.bBlkDb[blkFile].Get([]byte(key), db.ro) + if lerr != nil { + break + } + + var sha btcwire.ShaHash + sha.SetBytes(blkVal[0:31]) + shalist = append(shalist, sha) + } + + if err == nil { + return + } + log.Tracef("FetchIdxRange idx %v %v returned %v shas err %v", startHeight, endHeight, len(shalist), err) + + return shalist, nil +} + +// NewestSha returns the hash and block height of the most recent (end) block of +// the block chain. It will return the zero hash, -1 for the block height, and +// no error (nil) if there are not any blocks in the database yet. +func (db *LevelDb) NewestSha() (rsha *btcwire.ShaHash, rblkid int64, err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + if db.lastBlkIdx == -1 { + err = errors.New("Empty Database") + return + } + sha := db.lastBlkSha + + return &sha, db.lastBlkIdx, nil +} + +func (db *LevelDb) NewIterateBlocks() (rbogus btcdb.BlockIterator, err error) { + err = errors.New("Not implemented") + return +} diff --git a/leveldb/dbcache.go b/leveldb/dbcache.go new file mode 100644 index 00000000..6c3bfafe --- /dev/null +++ b/leveldb/dbcache.go @@ -0,0 +1,375 @@ +// 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 ldb + +import ( + "bytes" + "container/list" + "github.com/conformal/btcdb" + "github.com/conformal/btcutil" + "github.com/conformal/btcwire" + "sync" +) + +type txCache struct { + maxcount int + fifo list.List + // NOTE: the key is specifically ShaHash, not *ShaHash + txMap map[btcwire.ShaHash]*txCacheObj + cacheLock sync.RWMutex +} + +type txCacheObj struct { + next *txCacheObj + sha btcwire.ShaHash + blksha btcwire.ShaHash + pver uint32 + tx *btcwire.MsgTx + height int64 + spent []byte + txbuf []byte +} + +type blockCache struct { + maxcount int + fifo list.List + blockMap map[btcwire.ShaHash]*blockCacheObj + blockHeightMap map[int64]*blockCacheObj + cacheLock sync.RWMutex +} + +type blockCacheObj struct { + next *blockCacheObj + sha btcwire.ShaHash + blk *btcutil.Block +} + +// FetchBlockBySha - return a btcutil Block, object may be a cached. +func (db *LevelDb) FetchBlockBySha(sha *btcwire.ShaHash) (blk *btcutil.Block, err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + return db.fetchBlockBySha(sha) +} + +// fetchBlockBySha - return a btcutil Block, object may be a cached. +// Must be called with db lock held. +func (db *LevelDb) fetchBlockBySha(sha *btcwire.ShaHash) (blk *btcutil.Block, err error) { + + blkcache, ok := db.fetchBlockCache(sha) + if ok { + return blkcache.blk, nil + } + + buf, pver, height, err := db.fetchSha(sha) + if err != nil { + return + } + + blk, err = btcutil.NewBlockFromBytes(buf, pver) + if err != nil { + return + } + blk.SetHeight(height) + db.insertBlockCache(sha, blk) + + return +} + +// fetchBlockCache check if a block is in the block cache, if so return it. +func (db *LevelDb) fetchBlockCache(sha *btcwire.ShaHash) (*blockCacheObj, bool) { + + db.blockCache.cacheLock.RLock() + defer db.blockCache.cacheLock.RUnlock() + + blkobj, ok := db.blockCache.blockMap[*sha] + if !ok { // could this just return the map deref? + return nil, false + } + return blkobj, true +} + +// fetchBlockHeightCache check if a block is in the block cache, if so return it. +func (db *LevelDb) fetchBlockHeightCache(height int64) (*blockCacheObj, bool) { + + db.blockCache.cacheLock.RLock() + defer db.blockCache.cacheLock.RUnlock() + + blkobj, ok := db.blockCache.blockHeightMap[height] + if !ok { // could this just return the map deref? + return nil, false + } + return blkobj, true +} + +// insertBlockCache insert the given sha/block into the cache map. +// If the block cache is determined to be full, it will release +// an old entry in FIFO order. +func (db *LevelDb) insertBlockCache(sha *btcwire.ShaHash, blk *btcutil.Block) { + bc := &db.blockCache + + bc.cacheLock.Lock() + defer bc.cacheLock.Unlock() + + blkObj := blockCacheObj{sha: *sha, blk: blk} + bc.fifo.PushBack(&blkObj) + + if bc.fifo.Len() > bc.maxcount { + listobj := bc.fifo.Front() + bc.fifo.Remove(listobj) + tailObj, ok := listobj.Value.(*blockCacheObj) + if ok { + delete(bc.blockMap, tailObj.sha) + delete(bc.blockHeightMap, tailObj.blk.Height()) + } else { + panic("invalid type pushed on blockCache list") + } + } + + bc.blockHeightMap[blk.Height()] = &blkObj + bc.blockMap[blkObj.sha] = &blkObj +} + +// FetchTxByShaList given a array of ShaHash, look up the transactions +// and return them in a TxListReply array. +func (db *LevelDb) FetchTxByShaList(txShaList []*btcwire.ShaHash) []*btcdb.TxListReply { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + var replies []*btcdb.TxListReply + for _, txsha := range txShaList { + tx, _, _, _, height, txspent, err := db.fetchTxDataBySha(txsha) + btxspent := []bool{} + if err == nil { + btxspent = make([]bool, len(tx.TxOut), len(tx.TxOut)) + for idx := range tx.TxOut { + byteidx := idx / 8 + byteoff := uint(idx % 8) + btxspent[idx] = (txspent[byteidx] & (byte(1) << byteoff)) != 0 + } + } + txlre := btcdb.TxListReply{Sha: txsha, Tx: tx, Height: height, TxSpent: btxspent, Err: err} + replies = append(replies, &txlre) + } + return replies +} + +// fetchTxDataBySha returns several pieces of data regarding the given sha. +func (db *LevelDb) fetchTxDataBySha(txsha *btcwire.ShaHash) (rtx *btcwire.MsgTx, rtxbuf []byte, rpver uint32, rblksha *btcwire.ShaHash, rheight int64, rtxspent []byte, err error) { + + var pver uint32 + var blksha *btcwire.ShaHash + var height int64 + var txspent []byte + var toff int + var tlen int + var blk *btcutil.Block + var blkbuf []byte + + // Check Tx cache + if txc, ok := db.fetchTxCache(txsha); ok { + if txc.spent != nil { + return txc.tx, txc.txbuf, txc.pver, &txc.blksha, txc.height, txc.spent, nil + } + } + + // If not cached load it + height, toff, tlen, txspent, err = db.fetchLocationUsedBySha(txsha) + if err != nil { + return + } + + blksha, err = db.fetchBlockShaByHeight(height) + if err != nil { + log.Warnf("block idx lookup %v to %v", height, err) + return + } + log.Tracef("transaction %v is at block %v %v tx %v", + txsha, blksha, height, toff) + + blk, err = db.fetchBlockBySha(blksha) + if err != nil { + log.Warnf("unable to fetch block %v %v ", + height, &blksha) + return + } + + blkbuf, pver, err = blk.Bytes() + if err != nil { + log.Warnf("unable to decode block %v %v", height, &blksha) + return + } + + txbuf := make([]byte, tlen) + copy(txbuf[:], blkbuf[toff:toff+tlen]) + rbuf := bytes.NewBuffer(txbuf) + + var tx btcwire.MsgTx + err = tx.BtcDecode(rbuf, pver) + if err != nil { + log.Warnf("unable to decode tx block %v %v txoff %v txlen %v", + height, &blksha, toff, tlen) + return + } + + // Shove data into TxCache + // XXX - + var txc txCacheObj + txc.sha = *txsha + txc.tx = &tx + txc.txbuf = txbuf + txc.pver = pver + txc.height = height + txc.spent = txspent + txc.blksha = *blksha + db.insertTxCache(&txc) + + return &tx, txbuf, pver, blksha, height, txspent, nil +} + +// FetchTxAllBySha returns several pieces of data regarding the given sha. +func (db *LevelDb) FetchTxAllBySha(txsha *btcwire.ShaHash) (rtx *btcwire.MsgTx, rtxbuf []byte, rpver uint32, rblksha *btcwire.ShaHash, err error) { + var pver uint32 + var blksha *btcwire.ShaHash + var height int64 + var toff int + var tlen int + var blk *btcutil.Block + var blkbuf []byte + + // Check Tx cache + if txc, ok := db.fetchTxCache(txsha); ok { + return txc.tx, txc.txbuf, txc.pver, &txc.blksha, nil + } + + // If not cached load it + height, toff, tlen, err = db.FetchLocationBySha(txsha) + if err != nil { + return + } + + blksha, err = db.FetchBlockShaByHeight(height) + if err != nil { + log.Warnf("block idx lookup %v to %v", height, err) + return + } + log.Tracef("transaction %v is at block %v %v tx %v", + txsha, blksha, height, toff) + + blk, err = db.FetchBlockBySha(blksha) + if err != nil { + log.Warnf("unable to fetch block %v %v ", + height, &blksha) + return + } + + blkbuf, pver, err = blk.Bytes() + if err != nil { + log.Warnf("unable to decode block %v %v", height, &blksha) + return + } + + txbuf := make([]byte, tlen) + copy(txbuf[:], blkbuf[toff:toff+tlen]) + rbuf := bytes.NewBuffer(txbuf) + + var tx btcwire.MsgTx + err = tx.BtcDecode(rbuf, pver) + if err != nil { + log.Warnf("unable to decode tx block %v %v txoff %v txlen %v", + height, &blksha, toff, tlen) + return + } + + // Shove data into TxCache + // XXX - + var txc txCacheObj + txc.sha = *txsha + txc.tx = &tx + txc.txbuf = txbuf + txc.pver = pver + txc.height = height + txc.blksha = *blksha + db.insertTxCache(&txc) + + return &tx, txbuf, pver, blksha, nil +} + +// FetchTxBySha returns some data for the given Tx Sha. +func (db *LevelDb) FetchTxBySha(txsha *btcwire.ShaHash) (rtx *btcwire.MsgTx, rpver uint32, blksha *btcwire.ShaHash, err error) { + rtx, _, rpver, blksha, err = db.FetchTxAllBySha(txsha) + return +} + +// FetchTxBufBySha return the bytestream data and associated protocol version. +// for the given Tx Sha +func (db *LevelDb) FetchTxBufBySha(txsha *btcwire.ShaHash) (txbuf []byte, rpver uint32, err error) { + _, txbuf, rpver, _, err = db.FetchTxAllBySha(txsha) + return +} + +// fetchTxCache look up the given transaction in the Tx cache. +func (db *LevelDb) fetchTxCache(sha *btcwire.ShaHash) (*txCacheObj, bool) { + tc := &db.txCache + + tc.cacheLock.RLock() + defer tc.cacheLock.RUnlock() + + txObj, ok := tc.txMap[*sha] + if !ok { // could this just return the map deref? + return nil, false + } + return txObj, true +} + +// insertTxCache, insert the given txobj into the cache. +// if the tx cache is determined to be full, it will release +// an old entry in FIFO order. +func (db *LevelDb) insertTxCache(txObj *txCacheObj) { + tc := &db.txCache + + tc.cacheLock.Lock() + defer tc.cacheLock.Unlock() + + tc.fifo.PushBack(txObj) + + if tc.fifo.Len() >= tc.maxcount { + listobj := tc.fifo.Front() + tc.fifo.Remove(listobj) + tailObj, ok := listobj.Value.(*txCacheObj) + if ok { + delete(tc.txMap, tailObj.sha) + } else { + panic("invalid type pushed on tx list") + } + + } + + tc.txMap[txObj.sha] = txObj +} + +// InvalidateTxCache clear/release all cached transactions. +func (db *LevelDb) InvalidateTxCache() { + tc := &db.txCache + tc.cacheLock.Lock() + defer tc.cacheLock.Unlock() + tc.txMap = map[btcwire.ShaHash]*txCacheObj{} + tc.fifo = list.List{} +} + +// InvalidateTxCache clear/release all cached blocks. +func (db *LevelDb) InvalidateBlockCache() { + bc := &db.blockCache + bc.cacheLock.Lock() + defer bc.cacheLock.Unlock() + bc.blockMap = map[btcwire.ShaHash]*blockCacheObj{} + bc.blockHeightMap = map[int64]*blockCacheObj{} + bc.fifo = list.List{} +} + +// InvalidateCache clear/release all cached blocks and transactions. +func (db *LevelDb) InvalidateCache() { + db.InvalidateTxCache() + db.InvalidateBlockCache() +} diff --git a/leveldb/doc.go b/leveldb/doc.go new file mode 100644 index 00000000..65906a93 --- /dev/null +++ b/leveldb/doc.go @@ -0,0 +1,15 @@ +// 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 implements a sqlite3 instance of btcdb. + +sqlite provides a zero setup, single file database. It requires cgo +and the presence of the sqlite library and headers, but nothing else. +The performance is generally high although it goes down with database +size. + +Many of the block or tx specific functions for btcdb are in this subpackage. +*/ +package ldb diff --git a/leveldb/insertremove_test.go b/leveldb/insertremove_test.go new file mode 100644 index 00000000..27fa248d --- /dev/null +++ b/leveldb/insertremove_test.go @@ -0,0 +1,209 @@ +// 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 ldb_test + +import ( + "github.com/conformal/btcdb" + "github.com/conformal/btcdb/sqlite3" + "github.com/conformal/btcutil" + "github.com/conformal/btcwire" + "os" + "path/filepath" + "testing" +) + +var tstBlocks []*btcutil.Block + +func loadblocks(t *testing.T) []*btcutil.Block { + if len(tstBlocks) != 0 { + return tstBlocks + } + + testdatafile := filepath.Join("testdata", "blocks1-256.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) { + testUnspentInsert(t, dbTmDefault) + testUnspentInsert(t, dbTmNormal) + testUnspentInsert(t, dbTmFast) +} + +// 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. +func testUnspentInsert(t *testing.T, mode int) { + // Ignore db remove errors since it means we didn't have an old one. + dbname := "tstdbuspnt1" + _ = 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.LevelDb); ok { + sqldb.TempTblMax = 100 + } else { + t.Errorf("not right type") + } + case dbTmNoVerify: // validated block + t.Errorf("UnspentInsert test is not valid in NoVerify mode") + } + + // Since we are dealing with small dataset, reduce cache size + sqlite3.SetBlockCacheSize(db, 2) + sqlite3.SetTxCacheSize(db, 3) + + blocks := loadblocks(t) +endtest: + for height := int64(0); height < int64(len(blocks)); height++ { + + block := blocks[height] + // look up inputs to this x + mblock := block.MsgBlock() + var txneededList []*btcwire.ShaHash + var txlookupList []*btcwire.ShaHash + var txOutList []*btcwire.ShaHash + var txInList []*btcwire.OutPoint + for _, tx := range mblock.Transactions { + for _, txin := range tx.TxIn { + if txin.PreviousOutpoint.Index == uint32(4294967295) { + continue + } + origintxsha := &txin.PreviousOutpoint.Hash + + txInList = append(txInList, &txin.PreviousOutpoint) + txneededList = append(txneededList, origintxsha) + txlookupList = append(txlookupList, origintxsha) + + if !db.ExistsTxSha(origintxsha) { + t.Errorf("referenced tx not found %v ", origintxsha) + } + + } + txshaname, _ := tx.TxSha(block.ProtocolVersion()) + txlookupList = append(txlookupList, &txshaname) + txOutList = append(txOutList, &txshaname) + } + + txneededmap := map[btcwire.ShaHash]*btcdb.TxListReply{} + txlist := db.FetchTxByShaList(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 + } + + txlookupmap := map[btcwire.ShaHash]*btcdb.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("freshly inserted tx %v already spent %v", txo, i) + } + } + + } + if len(txInList) == 0 { + continue + } + dropblock := blocks[height-1] + dropsha, _ := dropblock.Sha() + + err = db.DropAfterBlockBySha(dropsha) + if err != nil { + t.Errorf("failed to drop block %v err %v", height, err) + break endtest + } + + txlookupmap = map[btcwire.ShaHash]*btcdb.TxListReply{} + txlist = db.FetchTxByShaList(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[btcwire.ShaHash]*btcdb.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) + } + } + } +} diff --git a/leveldb/internal_test.go b/leveldb/internal_test.go new file mode 100644 index 00000000..08706154 --- /dev/null +++ b/leveldb/internal_test.go @@ -0,0 +1,48 @@ +// 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 ldb + +import ( + "fmt" + "github.com/conformal/btcdb" + "github.com/conformal/btcwire" +) + +// 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, + blkid int64, err error) { + sqldb, ok := db.(*LevelDb) + if !ok { + err = fmt.Errorf("Invalid data type") + return + } + buf, pver, blkid, err = sqldb.fetchSha(*sha) + return +} + +// SetBlockCacheSize configures the maximum number of blocks in the cache to +// be the given size should be made before any fetching. +// This is a testing only interface. +func SetBlockCacheSize(db btcdb.Db, newsize int) { + sqldb, ok := db.(*LevelDb) + if !ok { + return + } + bc := &sqldb.blockCache + bc.maxcount = newsize +} + +// SetTxCacheSize configures the maximum number of tx in the cache to +// be the given size should be made before any fetching. +// This is a testing only interface. +func SetTxCacheSize(db btcdb.Db, newsize int) { + sqldb, ok := db.(*LevelDb) + if !ok { + return + } + tc := &sqldb.txCache + tc.maxcount = newsize +} diff --git a/leveldb/leveldb.go b/leveldb/leveldb.go new file mode 100644 index 00000000..d44d0e6e --- /dev/null +++ b/leveldb/leveldb.go @@ -0,0 +1,798 @@ +// 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 ldb + +import ( + "database/sql" + "fmt" + "github.com/conformal/btcdb" + "github.com/conformal/btcutil" + "github.com/conformal/btcwire" + "github.com/conformal/seelog" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" + _ "github.com/mattn/go-sqlite3" + "os" + "sync" +) + +const ( + dbVersion int = 2 + dbMaxTransCnt = 20000 + dbMaxTransMem = 64 * 1024 * 1024 // 64 MB +) + +var log seelog.LoggerInterface = seelog.Disabled + +type tBlockInsertData struct { + sha btcwire.ShaHash + pver uint32 + buf []byte +} +type tTxInsertData struct { + txsha *btcwire.ShaHash + blockid int64 + txoff int + txlen int + usedbuf []byte +} + +type txState struct { + tx *sql.Tx + writeCount int + txDataSz int + txInsertList []interface{} +} +type LevelDb struct { + // to be removed + sqldb *sql.DB + blkStmts []*sql.Stmt + blkBaseStmts []*sql.Stmt + txStmts []*sql.Stmt + txBaseStmts []*sql.Stmt + txState txState + + // lock preventing multiple entry + dbLock sync.Mutex + + // leveldb pieces + bShaDb *leveldb.DB + bBlkDb []*leveldb.DB + tShaDb *leveldb.DB + tLocDb []*leveldb.DB + tSpentDb []*leveldb.DB + blkOpen int + txOpen int + txSpentOpen int + ro *opt.ReadOptions + wo *opt.WriteOptions + + lastBlkShaCached bool + lastBlkSha btcwire.ShaHash + lastBlkIdx int64 + txCache txCache + blockCache blockCache + + UseTempTX bool + TempTblSz int + TempTblMax int + + dbInsertMode btcdb.InsertMode +} + +var self = btcdb.DriverDB{DbType: "sqlite", Create: CreateSqliteDB, Open: OpenSqliteDB} + +func init() { + btcdb.AddDBDriver(self) +} + +// createDB configure the database, setting up all tables to initial state. +func createDB(db *sql.DB) error { + log.Infof("Initializing new block database") + + // XXX check for old tables + buildTables := []string{ + "CREATE TABLE dbversion (version integer);", + "CREATE TABLE block ( blockid INTEGER PRIMARY KEY, key BLOB UNIQUE, " + + "pver INTEGER NOT NULL, data BLOB NOT NULL);", + "INSERT INTO dbversion (version) VALUES (" + fmt.Sprintf("%d", dbVersion) + + ");", + } + buildtxTables := []string{ + "CREATE TABLE tx (txidx INTEGER PRIMARY KEY, " + + "key TEXT, " + + "blockid INTEGER NOT NULL, " + + "txoff INTEGER NOT NULL, txlen INTEGER NOT NULL, " + + "data BLOB NOT NULL, " + + "FOREIGN KEY(blockid) REFERENCES block(blockid));", + "CREATE TABLE txtmp (key TEXT PRIMARY KEY, " + + "blockid INTEGER NOT NULL, " + + "txoff INTEGER NOT NULL, txlen INTEGER NOT NULL, " + + "data BLOB NOT NULL, " + + "FOREIGN KEY(blockid) REFERENCES block(blockid));", + "CREATE UNIQUE INDEX uniquetx ON tx (key);", + } + for _, sql := range buildTables { + _, err := db.Exec(sql) + if err != nil { + log.Warnf("sql table op failed %v [%v]", err, sql) + return err + } + } + for _, sql := range buildtxTables { + _, err := db.Exec(sql) + if err != nil { + log.Warnf("sql table op failed %v [%v]", err, sql) + return err + } + } + + return nil +} + +// OpenSqliteDB opens an existing database for use. +func OpenSqliteDB(filepath string) (pbdb btcdb.Db, err error) { + log = btcdb.GetLog() + return newOrCreateSqliteDB(filepath, false) +} + +// CreateSqliteDB creates, initializes and opens a database for use. +func CreateSqliteDB(filepath string) (pbdb btcdb.Db, err error) { + log = btcdb.GetLog() + return newOrCreateSqliteDB(filepath, true) +} + +// newOrCreateSqliteDB opens a database, either creating it or opens +// existing database based on flag. +func newOrCreateSqliteDB(filepath string, create bool) (pbdb btcdb.Db, err error) { + var bdb LevelDb + if create == false { + _, err = os.Stat(filepath) + if err != nil { + return nil, btcdb.DbDoesNotExist + } + } + + db, err := sql.Open("sqlite3", filepath) + if err != nil { + log.Warnf("db open failed %v\n", err) + return nil, err + } + + dbverstmt, err := db.Prepare("SELECT version FROM dbversion;") + if err != nil { + // about the only reason this would fail is that the database + // is not initialized + if create == false { + return nil, btcdb.DbDoesNotExist + } + err = createDB(db) + if err != nil { + // already warned in the called function + return nil, err + } + dbverstmt, err = db.Prepare("SELECT version FROM dbversion;") + if err != nil { + // if it failed this a second time, fail. + return nil, err + } + + } + row := dbverstmt.QueryRow() + var version int + err = row.Scan(&version) + if err != nil { + log.Warnf("unable to find db version: no row\n", err) + } + switch version { + case dbVersion: + // all good + default: + log.Warnf("mismatch db version: %v expected %v\n", version, dbVersion) + return nil, fmt.Errorf("Invalid version in database") + } + db.Exec("PRAGMA foreign_keys = ON;") + db.Exec("PRAGMA journal_mode=WAL;") + bdb.sqldb = db + + bdb.blkStmts = make([]*sql.Stmt, len(blkqueries)) + bdb.blkBaseStmts = make([]*sql.Stmt, len(blkqueries)) + for i := range blkqueries { + stmt, err := db.Prepare(blkqueries[i]) + if err != nil { + // XXX log/ + return nil, err + } + bdb.blkBaseStmts[i] = stmt + } + for i := range bdb.blkBaseStmts { + bdb.blkStmts[i] = bdb.blkBaseStmts[i] + } + + bdb.txBaseStmts = make([]*sql.Stmt, len(txqueries)) + for i := range txqueries { + stmt, err := db.Prepare(txqueries[i]) + if err != nil { + // XXX log/ + return nil, err + } + bdb.txBaseStmts[i] = stmt + } + // NOTE: all array entries in txStmts remain nil'ed + // tx statements are lazy bound + bdb.txStmts = make([]*sql.Stmt, len(txqueries)) + + bdb.blockCache.maxcount = 150 + bdb.blockCache.blockMap = map[btcwire.ShaHash]*blockCacheObj{} + bdb.blockCache.blockMap = map[btcwire.ShaHash]*blockCacheObj{} + bdb.blockCache.blockHeightMap = map[int64]*blockCacheObj{} + bdb.txCache.maxcount = 2000 + bdb.txCache.txMap = map[btcwire.ShaHash]*txCacheObj{} + + bdb.UseTempTX = true + bdb.TempTblMax = 1000000 + + return &bdb, nil +} + +// Sync verifies that the database is coherent on disk, +// and no outstanding transactions are in flight. +func (db *LevelDb) Sync() { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + db.endTx(true) +} + +// syncPoint notifies the db that this is a safe time to sync the database, +// if there are many outstanding transactions. +// Must be called with db lock held. +func (db *LevelDb) syncPoint() { + + tx := &db.txState + + if db.TempTblSz > db.TempTblMax { + err := db.migrateTmpTable() + if err != nil { + return + } + } else { + if len(tx.txInsertList) > dbMaxTransCnt || tx.txDataSz > dbMaxTransMem { + db.endTx(true) + } + } +} + +// Close cleanly shuts down database, syncing all data. +func (db *LevelDb) Close() { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + db.close() +} + +// RollbackClose discards the recent database changes to the previously +// saved data at last Sync. +func (db *LevelDb) RollbackClose() { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + tx := &db.txState + if tx.tx != nil { + err := tx.tx.Rollback() + if err != nil { + log.Debugf("Rollback failed: %v", err) + } else { + tx.tx = nil + } + } + db.close() +} + +// close performs the internal shutdown/close operation. +func (db *LevelDb) close() { + db.endTx(true) + + db.InvalidateCache() + + for i := range db.blkBaseStmts { + db.blkBaseStmts[i].Close() + } + for i := range db.txBaseStmts { + if db.txBaseStmts[i] != nil { + db.txBaseStmts[i].Close() + db.txBaseStmts[i] = nil + } + } + db.sqldb.Close() +} + +// txop returns the appropriately prepared statement, based on +// transaction state of the database. +func (db *LevelDb) txop(op int) *sql.Stmt { + if db.txStmts[op] != nil { + return db.txStmts[op] + } + if db.txState.tx == nil { + // we are not in a transaction, return the base statement + return db.txBaseStmts[op] + } + + if db.txStmts[op] == nil { + db.txStmts[op] = db.txState.tx.Stmt(db.txBaseStmts[op]) + } + + return db.txStmts[op] +} + +// startTx starts a transaction, preparing or scrubbing statements +// for proper operation inside a transaction. +func (db *LevelDb) startTx() (err error) { + tx := &db.txState + if tx.tx != nil { + // this shouldn't happen... + log.Warnf("Db startTx called while in a transaction") + return + } + tx.tx, err = db.sqldb.Begin() + if err != nil { + log.Warnf("Db startTx: begin failed %v", err) + tx.tx = nil + return + } + for i := range db.blkBaseStmts { + db.blkStmts[i] = tx.tx.Stmt(db.blkBaseStmts[i]) + } + for i := range db.txBaseStmts { + db.txStmts[i] = nil // these are lazily prepared + } + return +} + +// endTx commits the current active transaction, it zaps all of the prepared +// statements associated with the transaction. +func (db *LevelDb) endTx(recover bool) (err error) { + tx := &db.txState + + if tx.tx == nil { + return + } + + err = tx.tx.Commit() + if err != nil && recover { + // XXX - double check that the tx is dead after + // commit failure (rollback?) + + log.Warnf("Db endTx: commit failed %v", err) + err = db.rePlayTransaction() + if err != nil { + // We tried, return failure (after zeroing state) + // so the upper level can notice and restart + } + } + for i := range db.blkBaseStmts { + db.blkStmts[i].Close() + db.blkStmts[i] = db.blkBaseStmts[i] + } + for i := range db.txStmts { + if db.txStmts[i] != nil { + db.txStmts[i].Close() + db.txStmts[i] = nil + } + } + tx.tx = nil + var emptyTxList []interface{} + tx.txInsertList = emptyTxList + tx.txDataSz = 0 + return +} + +// rePlayTransaction will attempt to re-execute inserts performed +// sync the beginning of a transaction. This is to be used after +// a sql Commit operation fails to keep the database from losing data. +func (db *LevelDb) rePlayTransaction() (err error) { + err = db.startTx() + if err != nil { + return + } + tx := &db.txState + for _, ins := range tx.txInsertList { + switch v := ins.(type) { + case tBlockInsertData: + block := v + _, err = db.blkStmts[blkInsertSha].Exec(block.sha.Bytes(), + block.pver, block.buf) + if err != nil { + break + } + case tTxInsertData: + txd := v + txnamebytes := txd.txsha.Bytes() + txop := db.txop(txInsertStmt) + _, err = txop.Exec(txd.blockid, txnamebytes, txd.txoff, + txd.txlen, txd.usedbuf) + if err != nil { + break + } + } + } + // This function is called even if we have failed. + // We need to clean up so the database can be used again. + // However we want the original error not any new error, + // unless there was no original error but the commit fails. + err2 := db.endTx(false) + if err == nil && err2 != nil { + err = err2 + } + + return +} + +// DropAfterBlockBySha will remove any blocks from the database after the given block. +// It terminates any existing transaction and performs its operations in an +// atomic transaction, it is terminated (committed) before exit. +func (db *LevelDb) DropAfterBlockBySha(sha *btcwire.ShaHash) (err error) { + var row *sql.Row + db.dbLock.Lock() + defer db.dbLock.Unlock() + + // This is a destructive operation and involves multiple requests + // so requires a transaction, terminate any transaction to date + // and start a new transaction + err = db.endTx(true) + if err != nil { + return err + } + err = db.startTx() + if err != nil { + return err + } + + var startheight int64 + + if db.lastBlkShaCached { + startheight = db.lastBlkIdx + } else { + querystr := "SELECT blockid FROM block ORDER BY blockid DESC;" + + tx := &db.txState + if tx.tx != nil { + row = tx.tx.QueryRow(querystr) + } else { + row = db.sqldb.QueryRow(querystr) + } + var startblkidx int64 + err = row.Scan(&startblkidx) + if err != nil { + log.Warnf("DropAfterBlockBySha:unable to fetch blockheight %v", err) + return err + } + startheight = startblkidx + } + // also drop any cached sha data + db.lastBlkShaCached = false + + querystr := "SELECT blockid FROM block WHERE key = ?;" + + tx := &db.txState + row = tx.tx.QueryRow(querystr, sha.Bytes()) + + var keepidx int64 + err = row.Scan(&keepidx) + if err != nil { + // XXX + db.endTx(false) + return err + } + + for height := startheight; height > keepidx; height = height - 1 { + var blk *btcutil.Block + blkc, ok := db.fetchBlockHeightCache(height) + + if ok { + blk = blkc.blk + } else { + // must load the block from the db + sha, err = db.fetchBlockShaByHeight(height - 1) + if err != nil { + return + } + + var buf []byte + var pver uint32 + + buf, pver, _, err = db.fetchSha(*sha) + if err != nil { + return + } + + blk, err = btcutil.NewBlockFromBytes(buf, pver) + if err != nil { + return + } + } + + for _, tx := range blk.MsgBlock().Transactions { + err = db.unSpend(tx) + if err != nil { + return + } + } + } + + // invalidate the cache after possibly using cached entries for block + // lookup to unspend coins in them + db.InvalidateCache() + + _, err = tx.tx.Exec("DELETE FROM txtmp WHERE blockid > ?", keepidx) + if err != nil { + // XXX + db.endTx(false) + return err + } + + _, err = tx.tx.Exec("DELETE FROM tx WHERE blockid > ?", keepidx) + if err != nil { + // XXX + db.endTx(false) + return err + } + + // delete from block last in case of foreign keys + _, err = tx.tx.Exec("DELETE FROM block WHERE blockid > ?", keepidx) + if err != nil { + // XXX + db.endTx(false) + return err + } + + err = db.endTx(true) + if err != nil { + return err + } + return +} + +// 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 *LevelDb) InsertBlock(block *btcutil.Block) (height int64, err 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 + } + mblock := block.MsgBlock() + rawMsg, pver, err := block.Bytes() + if err != nil { + log.Warnf("Failed to obtain raw block sha %v", blocksha) + return + } + txloc, err := block.TxLoc() + if err != nil { + log.Warnf("Failed to obtain raw block sha %v", blocksha) + return + } + + // Insert block into database + newheight, err := db.insertBlockData(blocksha, &mblock.Header.PrevBlock, + pver, rawMsg) + if err != nil { + log.Warnf("Failed to insert block %v %v %v", blocksha, + &mblock.Header.PrevBlock, err) + return + } + + // 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. + for txidx, tx := range mblock.Transactions { + var txsha btcwire.ShaHash + 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 + } + // Some old blocks contain duplicate transactions + // Attempt to cleanly bypass this problem + // http://blockexplorer.com/b/91842 + // http://blockexplorer.com/b/91880 + if newheight == 91842 { + dupsha, err := btcwire.NewShaHashFromStr("d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599") + if err != nil { + panic("invalid sha string in source") + } + if txsha == *dupsha { + log.Tracef("skipping sha %v %v", dupsha, newheight) + continue + } + } + if newheight == 91880 { + dupsha, err := btcwire.NewShaHashFromStr("e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468") + if err != nil { + panic("invalid sha string in source") + } + if txsha == *dupsha { + log.Tracef("skipping sha %v %v", dupsha, newheight) + continue + } + } + spentbuflen := (len(tx.TxOut) + 7) / 8 + spentbuf := make([]byte, spentbuflen, spentbuflen) + if len(tx.TxOut)%8 != 0 { + for i := uint(len(tx.TxOut) % 8); i < 8; i++ { + spentbuf[spentbuflen-1] |= (byte(1) << i) + } + } + + err = db.insertTx(&txsha, newheight, txloc[txidx].TxStart, txloc[txidx].TxLen, spentbuf) + if err != nil { + log.Warnf("block %v idx %v failed to insert tx %v %v err %v", blocksha, newheight, &txsha, txidx, err) + var oBlkIdx int64 + oBlkIdx, _, _, err = db.fetchLocationBySha(&txsha) + log.Warnf("oblkidx %v err %v", oBlkIdx, err) + + return + } + 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 + } + } + db.syncPoint() + return newheight, nil +} + +// SetDBInsertMode provides hints to the database to how the application +// is running this allows the database to work in optimized modes when the +// database may be very busy. +func (db *LevelDb) SetDBInsertMode(newmode btcdb.InsertMode) { + + oldMode := db.dbInsertMode + switch newmode { + case btcdb.InsertNormal: + // Normal mode inserts tx directly into the tx table + db.UseTempTX = false + db.dbInsertMode = newmode + switch oldMode { + case btcdb.InsertFast: + if db.TempTblSz != 0 { + err := db.migrateTmpTable() + if err != nil { + return + } + } + case btcdb.InsertValidatedInput: + // generate tx indexes + txop := db.txop(txMigrateFinish) + _, err := txop.Exec() + if err != nil { + log.Warnf("Failed to create tx table index - %v", err) + } + } + case btcdb.InsertFast: + // Fast mode inserts tx into txtmp with validation, + // then dumps to tx then rebuilds indexes at thresholds + db.UseTempTX = true + if oldMode != btcdb.InsertNormal { + log.Warnf("switching between invalid DB modes") + break + } + db.dbInsertMode = newmode + case btcdb.InsertValidatedInput: + // ValidatedInput mode inserts into tx table with + // no duplicate checks, then builds index on exit from + // ValidatedInput mode + if oldMode != btcdb.InsertNormal { + log.Warnf("switching between invalid DB modes") + break + } + // remove tx table index + txop := db.txop(txMigratePrep) + _, err := txop.Exec() + if err != nil { + log.Warnf("Failed to clear tx table index - %v", err) + } + db.dbInsertMode = newmode + + // XXX + db.UseTempTX = false + } +} +func (db *LevelDb) doSpend(tx *btcwire.MsgTx) error { + for txinidx := range tx.TxIn { + txin := tx.TxIn[txinidx] + + inTxSha := txin.PreviousOutpoint.Hash + inTxidx := txin.PreviousOutpoint.Index + + if inTxidx == ^uint32(0) { + continue + } + + //log.Infof("spending %v %v", &inTxSha, inTxidx) + + err := db.setSpentData(&inTxSha, inTxidx) + if err != nil { + return err + } + } + return nil +} + +func (db *LevelDb) unSpend(tx *btcwire.MsgTx) error { + for txinidx := range tx.TxIn { + txin := tx.TxIn[txinidx] + + inTxSha := txin.PreviousOutpoint.Hash + inTxidx := txin.PreviousOutpoint.Index + + if inTxidx == ^uint32(0) { + continue + } + + err := db.clearSpentData(&inTxSha, inTxidx) + if err != nil { + return err + } + } + return nil +} + +func (db *LevelDb) setSpentData(sha *btcwire.ShaHash, idx uint32) error { + return db.setclearSpentData(sha, idx, true) +} + +func (db *LevelDb) clearSpentData(sha *btcwire.ShaHash, idx uint32) error { + return db.setclearSpentData(sha, idx, false) +} + +func (db *LevelDb) setclearSpentData(txsha *btcwire.ShaHash, idx uint32, set bool) error { + var spentdata []byte + usingtmp := false + txop := db.txop(txFetchUsedByShaStmt) + row := txop.QueryRow(txsha.String()) + err := row.Scan(&spentdata) + if err != nil { + // if the error is simply didn't fine continue otherwise + // retun failure + + usingtmp = true + txop = db.txop(txtmpFetchUsedByShaStmt) + row := txop.QueryRow(txsha.String()) + err := row.Scan(&spentdata) + if err != nil { + log.Warnf("Failed to locate spent data - %v %v", txsha, err) + return err + } + } + byteidx := idx / 8 + byteoff := idx % 8 + + if set { + spentdata[byteidx] |= (byte(1) << byteoff) + } else { + spentdata[byteidx] &= ^(byte(1) << byteoff) + } + txc, cached := db.fetchTxCache(txsha) + if cached { + txc.spent = spentdata + } + + if usingtmp { + txop = db.txop(txtmpUpdateUsedByShaStmt) + } else { + txop = db.txop(txUpdateUsedByShaStmt) + } + _, err = txop.Exec(spentdata, txsha.String()) + + return err +} diff --git a/leveldb/operational_test.go b/leveldb/operational_test.go new file mode 100644 index 00000000..71c92430 --- /dev/null +++ b/leveldb/operational_test.go @@ -0,0 +1,420 @@ +// 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 ldb_test + +import ( + "compress/bzip2" + "encoding/binary" + "github.com/conformal/btcdb" + "github.com/conformal/btcdb/sqlite3" + "github.com/conformal/btcutil" + "github.com/conformal/btcwire" + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +var network = btcwire.MainNet + +const ( + dbTmDefault = iota + dbTmNormal + dbTmFast + dbTmNoVerify +) + +func TestOperational(t *testing.T) { + testOperationalMode(t, dbTmDefault) + testOperationalMode(t, dbTmNormal) + testOperationalMode(t, dbTmFast) + testOperationalMode(t, dbTmNoVerify) +} + +func testOperationalMode(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.LevelDb); ok { + sqldb.TempTblMax = 100 + } else { + t.Errorf("not right type") + } + case dbTmNoVerify: // validated block + db.SetDBInsertMode(btcdb.InsertValidatedInput) + } + + // 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] + if mode != dbTmNoVerify { + // except for NoVerify which does not allow lookups check inputs + 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) + + if !db.ExistsTxSha(origintxsha) { + t.Errorf("referenced tx not found %v ", origintxsha) + } + + _, _, _, _, err := db.FetchTxAllBySha(origintxsha) + if err != nil { + t.Errorf("referenced tx not found %v err %v ", origintxsha, err) + } + _, _, _, _, err = db.FetchTxAllBySha(origintxsha) + if err != nil { + t.Errorf("referenced tx not found %v err %v ", origintxsha, err) + } + _, _, _, err = db.FetchTxBySha(origintxsha) + if err != nil { + t.Errorf("referenced tx not found %v err %v ", origintxsha, err) + } + _, _, err = db.FetchTxBufBySha(origintxsha) + if err != nil { + t.Errorf("referenced tx not found %v err %v ", origintxsha, err) + } + _, err = db.FetchTxUsedBySha(origintxsha) + if err != nil { + t.Errorf("tx used fetch fail %v err %v ", origintxsha, err) + } + } + } + txlist := db.FetchTxByShaList(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 := 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 := db.NewestSha() + if err != nil { + t.Errorf("failed to obtain latest sha %v %v", height, err) + } + + if blkid != height { + t.Errorf("height doe not match latest block height %v %v", blkid, height, err) + } + + blkSha, _ := block.Sha() + if *newSha != *blkSha { + t.Errorf("Newest block sha does not match freshly inserted one %v %v ", newSha, blkSha, err) + } + } + + // now that db is populated, do some additional test + testFetchRangeHeight(t, db, blocks) + + 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) + } +} + +func TestBackout(t *testing.T) { + testBackout(t, dbTmDefault) + testBackout(t, dbTmNormal) + testBackout(t, dbTmFast) +} + +func testBackout(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 := "tstdbop2" + _ = 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.LevelDb); ok { + sqldb.TempTblMax = 100 + } else { + t.Errorf("not right type") + } + } + + // 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 len(blocks) < 120 { + t.Errorf("test data too small") + return + } + + err = nil + for height := int64(0); height < int64(len(blocks)); height++ { + if height == 100 { + t.Logf("Syncing at block height 100") + db.Sync() + } + if height == 120 { + t.Logf("Simulating unexpected application quit") + // Simulate unexpected application quit + db.RollbackClose() + break + } + + block := blocks[height] + + newheight, err := db.InsertBlock(block) + if err != nil { + t.Errorf("failed to insert block %v err %v", height, err) + break + } + if newheight != height { + t.Errorf("height mismatch expect %v returned %v", height, newheight) + break + } + } + + // db was closed at height 120, so no cleanup is possible. + + // reopen db + db, err = btcdb.OpenDB("sqlite", dbname) + if err != nil { + t.Errorf("Failed to open test database %v", err) + return + } + defer db.Close() + + sha, err := blocks[99].Sha() + if err != nil { + t.Errorf("failed to get block 99 sha err %v", err) + return + } + _ = db.ExistsSha(sha) + _, err = db.FetchBlockBySha(sha) + if err != nil { + t.Errorf("failed to load block 99 from db %v", err) + } + + sha, err = blocks[110].Sha() + if err != nil { + t.Errorf("failed to get block 110 sha err %v", err) + return + } + _ = db.ExistsSha(sha) + _, err = db.FetchBlockBySha(sha) + if err == nil { + t.Errorf("loaded block 110 from db, failure expected") + return + } + + block := blocks[110] + mblock := block.MsgBlock() + txsha, err := mblock.Transactions[0].TxSha(block.ProtocolVersion()) + exists := db.ExistsTxSha(&txsha) + if exists { + t.Errorf("tx %v exists in db, failure expected", txsha) + } + + _, _, _, err = db.FetchTxBySha(&txsha) + _, err = db.FetchTxUsedBySha(&txsha) + + block = blocks[99] + mblock = block.MsgBlock() + txsha, err = mblock.Transactions[0].TxSha(block.ProtocolVersion()) + oldused, err := db.FetchTxUsedBySha(&txsha) + err = db.InsertTx(&txsha, 99, 1024, 1048, oldused) + if err == nil { + t.Errorf("dup insert of tx succeeded") + return + } +} + +func loadBlocks(t *testing.T, file string) (blocks []*btcutil.Block, err error) { + 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(&btcwire.GenesisBlock, btcwire.ProtocolVersion) + 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) + + var pver uint32 + switch { + case height < 200000: + pver = 1 + case height >= 200000: + pver = 2 + } + block, err = btcutil.NewBlockFromBytes(rbytes, pver) + if err != nil { + t.Errorf("failed to parse block %v", height) + return + } + blocks = append(blocks, block) + } + return +} + +func testFetchRangeHeight(t *testing.T, db btcdb.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("FetchRangeHeight: 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 = btcdb.AllShas + } + + shalist, err := db.FetchHeightRange(startheight, endheight) + if err != nil { + t.Errorf("FetchRangeHeight: unexpected failure looking up shas %v", err) + } + + if endheight == btcdb.AllShas { + if int64(len(shalist)) != nBlocks-startheight { + t.Errorf("FetchRangeHeight: expected A %v shas, got %v", nBlocks-startheight, len(shalist)) + } + } else { + if int64(len(shalist)) != testcnt { + t.Errorf("FetchRangeHeight: expected %v shas, got %v", testcnt, len(shalist)) + } + } + + for i := range shalist { + if *shanames[int64(i)+startheight] != shalist[i] { + t.Errorf("FetchRangeHeight: mismatch sha at %v requested range %v %v ", int64(i)+startheight, startheight, endheight) + } + } + } + +} diff --git a/leveldb/testdata/blocks1-256.bz2 b/leveldb/testdata/blocks1-256.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..6b8bda4429200c0566bb13c28c35d6397272e475 GIT binary patch literal 37555 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 literal 0 HcmV?d00001 diff --git a/leveldb/tx.go b/leveldb/tx.go new file mode 100644 index 00000000..c233777b --- /dev/null +++ b/leveldb/tx.go @@ -0,0 +1,335 @@ +// 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 ldb + +import ( + "database/sql" + "github.com/conformal/btcdb" + "github.com/conformal/btcwire" + _ "github.com/mattn/go-sqlite3" +) + +// InsertTx inserts a tx hash and its associated data into the database. +func (db *LevelDb) InsertTx(txsha *btcwire.ShaHash, height int64, txoff int, txlen int, usedbuf []byte) (err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + return db.insertTx(txsha, height, txoff, txlen, usedbuf) +} + +// insertTx inserts a tx hash and its associated data into the database. +// Must be called with db lock held. +func (db *LevelDb) insertTx(txsha *btcwire.ShaHash, height int64, txoff int, txlen int, usedbuf []byte) (err error) { + + tx := &db.txState + if tx.tx == nil { + err = db.startTx() + if err != nil { + return + } + } + blockid := height + 1 + txd := tTxInsertData{txsha: txsha, blockid: blockid, txoff: txoff, txlen: txlen, usedbuf: usedbuf} + + log.Tracef("inserting tx %v for block %v off %v len %v", + txsha, blockid, txoff, txlen) + + rowBytes := txsha.String() + + var op int // which table to insert data into. + if db.UseTempTX { + var tblockid int64 + var ttxoff int + var ttxlen int + txop := db.txop(txFetchLocationByShaStmt) + row := txop.QueryRow(rowBytes) + err = row.Scan(&tblockid, &ttxoff, &ttxlen) + if err != sql.ErrNoRows { + // sha already present + err = btcdb.DuplicateSha + return + } + op = txtmpInsertStmt + } else { + op = txInsertStmt + } + + txop := db.txop(op) + _, err = txop.Exec(rowBytes, blockid, txoff, txlen, usedbuf) + if err != nil { + log.Warnf("failed to insert %v %v %v", txsha, blockid, err) + return + } + if db.UseTempTX { + db.TempTblSz++ + } + + // put in insert list for replay + tx.txInsertList = append(tx.txInsertList, txd) + + return +} + +// ExistsTxSha returns if the given tx sha exists in the database +func (db *LevelDb) ExistsTxSha(txsha *btcwire.ShaHash) (exists bool) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + + if _, ok := db.fetchTxCache(txsha); ok { + return true + } + + return db.existsTxSha(txsha) +} + +// existsTxSha returns if the given tx sha exists in the database.o +// Must be called with the db lock held. +func (db *LevelDb) existsTxSha(txsha *btcwire.ShaHash) (exists bool) { + var blockid uint32 + + txop := db.txop(txExistsShaStmt) + row := txop.QueryRow(txsha.String()) + err := row.Scan(&blockid) + + if err == sql.ErrNoRows { + txop = db.txop(txtmpExistsShaStmt) + row = txop.QueryRow(txsha.String()) + err := row.Scan(&blockid) + + if err == sql.ErrNoRows { + return false + } + if err != nil { + log.Warnf("txTmpExistsTxSha: fail %v", err) + return false + } + log.Warnf("txtmpExistsTxSha: success") + return true + } + + if err != nil { + // ignore real errors? + log.Warnf("existsTxSha: fail %v", err) + return false + } + + return true +} + +// FetchLocationBySha looks up the Tx sha information by name. +func (db *LevelDb) FetchLocationBySha(txsha *btcwire.ShaHash) (blockidx int64, txoff int, txlen int, err error) { + db.dbLock.Lock() + defer db.dbLock.Unlock() + return db.fetchLocationBySha(txsha) +} + +// fetchLocationBySha look up the Tx sha information by name. +// Must be called with db lock held. +func (db *LevelDb) fetchLocationBySha(txsha *btcwire.ShaHash) (height int64, txoff int, txlen int, err error) { + var row *sql.Row + var blockid int64 + var ttxoff int + var ttxlen int + + rowBytes := txsha.String() + txop := db.txop(txFetchLocationByShaStmt) + row = txop.QueryRow(rowBytes) + + err = row.Scan(&blockid, &ttxoff, &ttxlen) + if err == sql.ErrNoRows { + txop = db.txop(txtmpFetchLocationByShaStmt) + row = txop.QueryRow(rowBytes) + + err = row.Scan(&blockid, &ttxoff, &ttxlen) + if err == sql.ErrNoRows { + err = btcdb.TxShaMissing + return + } + if err != nil { + log.Warnf("txtmp FetchLocationBySha: fail %v", + err) + return + } + } + if err != nil { + log.Warnf("FetchLocationBySha: fail %v", err) + return + } + height = blockid - 1 + txoff = ttxoff + txlen = ttxlen + return +} + +// fetchLocationUsedBySha look up the Tx sha information by name. +// Must be called with db lock held. +func (db *LevelDb) fetchLocationUsedBySha(txsha *btcwire.ShaHash) (rheight int64, rtxoff int, rtxlen int, rspentbuf []byte, err error) { + var row *sql.Row + var blockid int64 + var txoff int + var txlen int + var txspent []byte + + rowBytes := txsha.String() + txop := db.txop(txFetchLocUsedByShaStmt) + row = txop.QueryRow(rowBytes) + + err = row.Scan(&blockid, &txoff, &txlen, &txspent) + if err == sql.ErrNoRows { + txop = db.txop(txtmpFetchLocUsedByShaStmt) + row = txop.QueryRow(rowBytes) + + err = row.Scan(&blockid, &txoff, &txlen, &txspent) + if err == sql.ErrNoRows { + err = btcdb.TxShaMissing + return + } + if err != nil { + log.Warnf("txtmp FetchLocationBySha: fail %v", + err) + return + } + } + if err != nil { + log.Warnf("FetchLocationBySha: fail %v", err) + return + } + height := blockid - 1 + return height, txoff, txlen, txspent, nil +} + +// FetchTxUsedBySha returns the used/spent buffer for a given transaction. +func (db *LevelDb) FetchTxUsedBySha(txsha *btcwire.ShaHash) (spentbuf []byte, err error) { + var row *sql.Row + db.dbLock.Lock() + defer db.dbLock.Unlock() + + rowBytes := txsha.String() + txop := db.txop(txFetchUsedByShaStmt) + row = txop.QueryRow(rowBytes) + + var databytes []byte + err = row.Scan(&databytes) + if err == sql.ErrNoRows { + txop := db.txop(txtmpFetchUsedByShaStmt) + row = txop.QueryRow(rowBytes) + + err = row.Scan(&databytes) + if err == sql.ErrNoRows { + err = btcdb.TxShaMissing + return + } + if err != nil { + log.Warnf("txtmp FetchLocationBySha: fail %v", + err) + return + } + } + + if err != nil { + log.Warnf("FetchUsedBySha: fail %v", err) + return + } + spentbuf = databytes + return +} + +var vaccumDbNextMigrate bool + +// migrateTmpTable functions to perform internal db optimization when +// performing large numbers of database inserts. When in Fast operation +// mode, it inserts into txtmp, then when that table reaches a certain +// size limit it moves all tx in the txtmp table into the primary tx +// table and recomputes the index on the primary tx table. +func (db *LevelDb) migrateTmpTable() error { + db.endTx(true) + db.startTx() // ??? + + db.UseTempTX = false + db.TempTblSz = 0 + + var doVacuum bool + var nsteps int + if vaccumDbNextMigrate { + nsteps = 6 + vaccumDbNextMigrate = false + doVacuum = true + } else { + nsteps = 5 + vaccumDbNextMigrate = true + } + + log.Infof("db compaction Stage 1/%v: Preparing", nsteps) + txop := db.txop(txMigratePrep) + _, err := txop.Exec() + if err != nil { + log.Warnf("Failed to prepare migrate - %v", err) + return err + } + + log.Infof("db compaction Stage 2/%v: Copying", nsteps) + txop = db.txop(txMigrateCopy) + _, err = txop.Exec() + if err != nil { + log.Warnf("Migrate read failed - %v", err) + return err + } + + log.Tracef("db compaction Stage 2a/%v: Enable db vacuum", nsteps) + txop = db.txop(txPragmaVacuumOn) + _, err = txop.Exec() + if err != nil { + log.Warnf("Migrate error trying to enable vacuum on "+ + "temporary transaction table - %v", err) + return err + } + + log.Infof("db compaction Stage 3/%v: Clearing old data", nsteps) + txop = db.txop(txMigrateClear) + _, err = txop.Exec() + if err != nil { + log.Warnf("Migrate error trying to clear temporary "+ + "transaction table - %v", err) + return err + } + + log.Tracef("db compaction Stage 3a/%v: Disable db vacuum", nsteps) + txop = db.txop(txPragmaVacuumOff) + _, err = txop.Exec() + if err != nil { + log.Warnf("Migrate error trying to disable vacuum on "+ + "temporary transaction table - %v", err) + return err + } + + log.Infof("db compaction Stage 4/%v: Rebuilding index", nsteps) + txop = db.txop(txMigrateFinish) + _, err = txop.Exec() + if err != nil { + log.Warnf("Migrate error trying to clear temporary "+ + "transaction table - %v", err) + return err + } + + log.Infof("db compaction Stage 5/%v: Finalizing transaction", nsteps) + db.endTx(true) // ??? + + if doVacuum { + log.Infof("db compaction Stage 6/%v: Optimizing database", nsteps) + txop = db.txop(txVacuum) + _, err = txop.Exec() + if err != nil { + log.Warnf("migrate error trying to clear txtmp tbl %v", err) + return err + } + } + + log.Infof("db compaction: Complete") + + // TODO(drahn) - determine if this should be turned back on or not + db.UseTempTX = true + + return nil +}