2013-05-29 02:07:21 +02:00
|
|
|
// 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
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"fmt"
|
|
|
|
"github.com/conformal/btcdb"
|
|
|
|
"github.com/conformal/btcutil"
|
|
|
|
"github.com/conformal/btcwire"
|
|
|
|
"github.com/conformal/seelog"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
"os"
|
|
|
|
"sync"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
dbVersion int = 2
|
|
|
|
dbMaxTransCnt = 20000
|
|
|
|
dbMaxTransMem = 64 * 1024 * 1024 // 64 MB
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
blkInsertSha = iota
|
|
|
|
blkFetchSha
|
|
|
|
blkExistsSha
|
|
|
|
blkFetchIdx
|
|
|
|
blkFetchIdxList
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
txInsertStmt = iota
|
|
|
|
txFetchUsedByShaStmt
|
|
|
|
txFetchLocationByShaStmt
|
2013-07-11 22:35:50 +02:00
|
|
|
txFetchLocUsedByShaStmt
|
|
|
|
txUpdateUsedByShaStmt
|
|
|
|
|
2013-05-29 02:07:21 +02:00
|
|
|
txtmpInsertStmt
|
|
|
|
txtmpFetchUsedByShaStmt
|
|
|
|
txtmpFetchLocationByShaStmt
|
2013-07-11 22:35:50 +02:00
|
|
|
txtmpFetchLocUsedByShaStmt
|
|
|
|
txtmpUpdateUsedByShaStmt
|
|
|
|
|
2013-05-29 02:07:21 +02:00
|
|
|
txMigrateCopy
|
|
|
|
txMigrateClear
|
|
|
|
txMigratePrep
|
|
|
|
txMigrateFinish
|
|
|
|
txMigrateCount
|
|
|
|
txPragmaVacuumOn
|
|
|
|
txPragmaVacuumOff
|
|
|
|
txVacuum
|
2013-07-10 01:11:02 +02:00
|
|
|
txExistsShaStmt
|
|
|
|
txtmpExistsShaStmt
|
2013-05-29 02:07:21 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var blkqueries []string = []string{
|
|
|
|
blkInsertSha: "INSERT INTO block (key, pver, data) VALUES(?, ?, ?);",
|
|
|
|
blkFetchSha: "SELECT pver, data, blockid FROM block WHERE key = ?;",
|
|
|
|
blkExistsSha: "SELECT pver FROM block WHERE key = ?;",
|
|
|
|
blkFetchIdx: "SELECT key FROM block WHERE blockid = ?;",
|
|
|
|
blkFetchIdxList: "SELECT key FROM block WHERE blockid >= ? AND blockid < ? ORDER BY blockid ASC LIMIT 500;",
|
|
|
|
}
|
|
|
|
|
|
|
|
var txqueries []string = []string{
|
2013-07-11 22:35:50 +02:00
|
|
|
txInsertStmt: "INSERT INTO tx (key, blockid, txoff, txlen, data) VALUES(?, ?, ?, ?, ?);",
|
|
|
|
txFetchUsedByShaStmt: "SELECT data FROM tx WHERE key = ?;",
|
|
|
|
txFetchLocationByShaStmt: "SELECT blockid, txoff, txlen FROM tx WHERE key = ?;",
|
|
|
|
txFetchLocUsedByShaStmt: "SELECT blockid, txoff, txlen, data FROM tx WHERE key = ?;",
|
|
|
|
txUpdateUsedByShaStmt: "UPDATE tx SET data = ? WHERE key = ?;",
|
|
|
|
|
2013-05-29 02:07:21 +02:00
|
|
|
txtmpInsertStmt: "INSERT INTO txtmp (key, blockid, txoff, txlen, data) VALUES(?, ?, ?, ?, ?);",
|
|
|
|
txtmpFetchUsedByShaStmt: "SELECT data FROM txtmp WHERE key = ?;",
|
|
|
|
txtmpFetchLocationByShaStmt: "SELECT blockid, txoff, txlen FROM txtmp WHERE key = ?;",
|
2013-07-11 22:35:50 +02:00
|
|
|
txtmpFetchLocUsedByShaStmt: "SELECT blockid, txoff, txlen, data FROM txtmp WHERE key = ?;",
|
|
|
|
txtmpUpdateUsedByShaStmt: "UPDATE txtmp SET data = ? WHERE key = ?;",
|
|
|
|
|
|
|
|
txMigrateCopy: "INSERT INTO tx (key, blockid, txoff, txlen, data) SELECT key, blockid, txoff, txlen, data FROM txtmp;",
|
|
|
|
txMigrateClear: "DELETE from txtmp;",
|
|
|
|
txMigratePrep: "DROP index IF EXISTS uniquetx;",
|
|
|
|
txMigrateFinish: "CREATE UNIQUE INDEX IF NOT EXISTS uniquetx ON tx (key);",
|
|
|
|
txMigrateCount: "SELECT COUNT(*) FROM txtmp;",
|
|
|
|
txPragmaVacuumOn: "PRAGMA auto_vacuum = FULL;",
|
|
|
|
txPragmaVacuumOff: "PRAGMA auto_vacuum = NONE;",
|
|
|
|
txVacuum: "VACUUM;",
|
|
|
|
txExistsShaStmt: "SELECT blockid FROM tx WHERE key = ?;",
|
|
|
|
txtmpExistsShaStmt: "SELECT blockid FROM txtmp WHERE key = ?;",
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 SqliteDb struct {
|
|
|
|
sqldb *sql.DB
|
|
|
|
blkStmts []*sql.Stmt
|
|
|
|
blkBaseStmts []*sql.Stmt
|
|
|
|
txStmts []*sql.Stmt
|
|
|
|
txBaseStmts []*sql.Stmt
|
|
|
|
txState txState
|
|
|
|
dbLock sync.Mutex
|
|
|
|
|
|
|
|
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 SqliteDb
|
|
|
|
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{}
|
2013-07-11 22:35:50 +02:00
|
|
|
bdb.blockCache.blockMap = map[btcwire.ShaHash]*blockCacheObj{}
|
|
|
|
bdb.blockCache.blockHeightMap = map[int64]*blockCacheObj{}
|
2013-05-29 02:07:21 +02:00
|
|
|
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 *SqliteDb) 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 *SqliteDb) 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 *SqliteDb) 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 *SqliteDb) 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)
|
2013-07-11 22:35:50 +02:00
|
|
|
} else {
|
|
|
|
tx.tx = nil
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
db.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// close performs the internal shutdown/close operation.
|
|
|
|
func (db *SqliteDb) 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 *SqliteDb) 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 *SqliteDb) 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 *SqliteDb) 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 *SqliteDb) 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.
|
2013-06-25 17:15:58 +02:00
|
|
|
func (db *SqliteDb) DropAfterBlockBySha(sha *btcwire.ShaHash) (err error) {
|
2013-05-29 02:07:21 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2013-07-11 22:35:50 +02:00
|
|
|
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
|
|
|
|
}
|
2013-05-29 02:07:21 +02:00
|
|
|
// 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())
|
|
|
|
|
2013-07-11 22:35:50 +02:00
|
|
|
var keepidx int64
|
|
|
|
err = row.Scan(&keepidx)
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
// XXX
|
|
|
|
db.endTx(false)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2013-07-11 22:35:50 +02:00
|
|
|
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
|
2013-08-05 20:03:41 +02:00
|
|
|
buf, _, _, err = db.fetchSha(*sha)
|
2013-07-11 22:35:50 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-08-05 20:03:41 +02:00
|
|
|
blk, err = btcutil.NewBlockFromBytes(buf)
|
2013-07-11 22:35:50 +02:00
|
|
|
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()
|
|
|
|
|
2013-07-29 22:39:48 +02:00
|
|
|
return db.delFromDB(keepidx)
|
|
|
|
}
|
|
|
|
|
2013-08-22 15:32:23 +02:00
|
|
|
func (db *SqliteDb) delFromDB(keepidx int64) error {
|
2013-07-29 22:39:48 +02:00
|
|
|
tx := &db.txState
|
|
|
|
_, err := tx.tx.Exec("DELETE FROM txtmp WHERE blockid > ?", keepidx)
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
// XXX
|
|
|
|
db.endTx(false)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2013-07-11 22:35:50 +02:00
|
|
|
_, err = tx.tx.Exec("DELETE FROM tx WHERE blockid > ?", keepidx)
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
// XXX
|
|
|
|
db.endTx(false)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// delete from block last in case of foreign keys
|
2013-07-11 22:35:50 +02:00
|
|
|
_, err = tx.tx.Exec("DELETE FROM block WHERE blockid > ?", keepidx)
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
// XXX
|
|
|
|
db.endTx(false)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = db.endTx(true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2013-07-29 22:39:48 +02:00
|
|
|
return err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
|
2013-07-25 23:44:18 +02:00
|
|
|
// 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.
|
2013-07-29 22:39:48 +02:00
|
|
|
func (db *SqliteDb) InsertBlock(block *btcutil.Block) (int64, error) {
|
2013-05-29 02:07:21 +02:00
|
|
|
db.dbLock.Lock()
|
|
|
|
defer db.dbLock.Unlock()
|
|
|
|
|
|
|
|
blocksha, err := block.Sha()
|
|
|
|
if err != nil {
|
|
|
|
log.Warnf("Failed to compute block sha %v", blocksha)
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
2013-07-29 22:39:48 +02:00
|
|
|
|
2013-05-29 02:07:21 +02:00
|
|
|
mblock := block.MsgBlock()
|
2013-08-05 20:03:41 +02:00
|
|
|
rawMsg, err := block.Bytes()
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Warnf("Failed to obtain raw block sha %v", blocksha)
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
txloc, err := block.TxLoc()
|
|
|
|
if err != nil {
|
|
|
|
log.Warnf("Failed to obtain raw block sha %v", blocksha)
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Insert block into database
|
|
|
|
newheight, err := db.insertBlockData(blocksha, &mblock.Header.PrevBlock,
|
2013-08-05 20:03:41 +02:00
|
|
|
0, rawMsg)
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Warnf("Failed to insert block %v %v %v", blocksha,
|
|
|
|
&mblock.Header.PrevBlock, err)
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
|
2013-07-29 22:39:48 +02:00
|
|
|
txinsertidx := -1
|
|
|
|
success := false
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if success {
|
|
|
|
return
|
|
|
|
}
|
2013-08-22 15:32:23 +02:00
|
|
|
|
2013-07-29 22:39:48 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-08-22 15:32:23 +02:00
|
|
|
err = db.delFromDB(newheight - 1)
|
2013-07-29 22:39:48 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Warnf("Error during block insert unwind %v %v", blocksha, err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2013-05-29 02:07:21 +02:00
|
|
|
// 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
|
2013-08-05 20:03:41 +02:00
|
|
|
txsha, err = tx.TxSha()
|
2013-05-29 02:07:21 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Warnf("failed to compute tx name block %v idx %v err %v", blocksha, txidx, err)
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
2013-07-29 22:39:48 +02:00
|
|
|
|
|
|
|
// num tx inserted, thus would need unwind if failure occurs
|
|
|
|
txinsertidx = txidx
|
|
|
|
|
2013-05-29 02:07:21 +02:00
|
|
|
// 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)
|
2013-07-18 22:43:36 +02:00
|
|
|
if len(tx.TxOut)%8 != 0 {
|
|
|
|
for i := uint(len(tx.TxOut) % 8); i < 8; i++ {
|
|
|
|
spentbuf[spentbuflen-1] |= (byte(1) << i)
|
|
|
|
}
|
2013-07-11 22:35:50 +02:00
|
|
|
}
|
2013-05-29 02:07:21 +02:00
|
|
|
|
|
|
|
err = db.insertTx(&txsha, newheight, txloc[txidx].TxStart, txloc[txidx].TxLen, spentbuf)
|
|
|
|
if err != nil {
|
2013-07-29 17:52:39 +02:00
|
|
|
log.Warnf("block %v idx %v failed to insert tx %v %v err %v", blocksha, newheight, &txsha, txidx, err)
|
2013-05-29 02:07:21 +02:00
|
|
|
var oBlkIdx int64
|
|
|
|
oBlkIdx, _, _, err = db.fetchLocationBySha(&txsha)
|
|
|
|
log.Warnf("oblkidx %v err %v", oBlkIdx, err)
|
|
|
|
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-07-11 22:35:50 +02:00
|
|
|
}
|
|
|
|
err = db.doSpend(tx)
|
|
|
|
if err != nil {
|
2013-07-29 17:52:39 +02:00
|
|
|
log.Warnf("block %v idx %v failed to spend tx %v %v err %v", blocksha, newheight, &txsha, txidx, err)
|
2013-07-11 22:35:50 +02:00
|
|
|
|
2013-07-29 22:39:48 +02:00
|
|
|
return -1, err
|
2013-05-29 02:07:21 +02:00
|
|
|
}
|
|
|
|
}
|
2013-07-29 22:39:48 +02:00
|
|
|
success = true
|
2013-05-29 02:07:21 +02:00
|
|
|
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 *SqliteDb) 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
|
|
|
|
}
|
|
|
|
}
|
2013-07-11 22:35:50 +02:00
|
|
|
func (db *SqliteDb) 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 *SqliteDb) 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 *SqliteDb) setSpentData(sha *btcwire.ShaHash, idx uint32) error {
|
|
|
|
return db.setclearSpentData(sha, idx, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (db *SqliteDb) clearSpentData(sha *btcwire.ShaHash, idx uint32) error {
|
|
|
|
return db.setclearSpentData(sha, idx, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (db *SqliteDb) 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
|
|
|
|
}
|