diff --git a/blockchain/chain.go b/blockchain/chain.go index 7ffbc097..c2b84409 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -2165,14 +2165,20 @@ func New(config *Config) (*BlockChain, error) { } } - tip := b.bestChain.Tip() - b.subsidyCache = NewSubsidyCache(tip.height, b.chainParams) + b.subsidyCache = NewSubsidyCache(b.bestChain.Tip().height, b.chainParams) b.pruner = newChainPruner(&b) + // The version 5 database upgrade requires a full reindex. Perform, or + // resume, the reindex as needed. + if err := b.maybeFinishV5Upgrade(); err != nil { + return nil, err + } + log.Infof("Blockchain database version info: chain: %d, compression: "+ "%d, block index: %d", b.dbInfo.version, b.dbInfo.compVer, b.dbInfo.bidxVer) + tip := b.bestChain.Tip() log.Infof("Chain state: height %d, hash %v, total transactions %d, "+ "work %v, stake version %v", tip.height, tip.hash, b.stateSnapshot.TotalTxns, tip.workSum, 0) diff --git a/blockchain/chainio.go b/blockchain/chainio.go index db8bc6a5..16439a57 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -1711,20 +1711,6 @@ func (b *BlockChain) initChainState() error { } b.bestChain.SetTip(tip) - // Ensure all ancestors of the current best chain tip are marked as - // valid. This is necessary due to older software versions not marking - // nodes before the final checkpoint as valid. - // - // Note that the nodes are not marked as modified here, so the database - // is not updated unless the node is otherwise modified and written back - // out a later point. Ultimately, the nodes should be updated in the - // database accordingly as part of a database upgrade, however, since - // the nodes are all in memory, they can be updated very quickly here - // without requiring a database version bump. - for node := tip; node != nil; node = node.parent { - node.status |= statusValid - } - log.Debugf("Block index loaded in %v", time.Since(bidxStart)) // Exception for version 1 blockchains: skip loading the stake diff --git a/blockchain/indexers/addrindex.go b/blockchain/indexers/addrindex.go index 0f84ba38..fe86d196 100644 --- a/blockchain/indexers/addrindex.go +++ b/blockchain/indexers/addrindex.go @@ -25,6 +25,9 @@ const ( // addrIndexName is the human-readable name for the index. addrIndexName = "address index" + // addrIndexVersion is the current version of the address index. + addrIndexVersion = 2 + // level0MaxEntries is the maximum number of transactions that are // stored in level 0 of an address index entry. Subsequent levels store // 2^n * level0MaxEntries entries, or in words, double the maximum of @@ -652,6 +655,13 @@ func (idx *AddrIndex) Name() string { return addrIndexName } +// Version returns the current version of the index. +// +// This is part of the Indexer interface. +func (idx *AddrIndex) Version() uint32 { + return addrIndexVersion +} + // Create is invoked when the indexer manager determines the index needs // to be created for the first time. It creates the bucket for the address // index. diff --git a/blockchain/indexers/cfindex.go b/blockchain/indexers/cfindex.go index 88de84e6..677b50dc 100644 --- a/blockchain/indexers/cfindex.go +++ b/blockchain/indexers/cfindex.go @@ -22,6 +22,9 @@ import ( const ( // cfIndexName is the human-readable name for the index. cfIndexName = "committed filter index" + + // cfIndexVersion is the current version of the committed filter index. + cfIndexVersion = 2 ) // Committed filters come in two flavors: basic and extended. They are @@ -126,6 +129,13 @@ func (idx *CFIndex) Name() string { return cfIndexName } +// Version returns the current version of the index. +// +// This is part of the Indexer interface. +func (idx *CFIndex) Version() uint32 { + return cfIndexVersion +} + // Create is invoked when the indexer manager determines the index needs to // be created for the first time. It creates buckets for the two hash-based cf // indexes (simple, extended). diff --git a/blockchain/indexers/common.go b/blockchain/indexers/common.go index f422cd33..f465fdbf 100644 --- a/blockchain/indexers/common.go +++ b/blockchain/indexers/common.go @@ -42,6 +42,9 @@ type Indexer interface { // Name returns the human-readable name of the index. Name() string + // Return the current version of the index. + Version() uint32 + // Create is invoked when the indexer manager determines the index needs // to be created for the first time. Create(dbTx database.Tx) error diff --git a/blockchain/indexers/existsaddrindex.go b/blockchain/indexers/existsaddrindex.go index bb286af2..4df6085c 100644 --- a/blockchain/indexers/existsaddrindex.go +++ b/blockchain/indexers/existsaddrindex.go @@ -16,10 +16,16 @@ import ( "github.com/decred/dcrd/wire" ) -var ( +const ( // existsAddressIndexName is the human-readable name for the index. existsAddressIndexName = "exists address index" + // existsAddrIndexVersion is the current version of the exists address + // index. + existsAddrIndexVersion = 2 +) + +var ( // existsAddrIndexKey is the key of the ever seen address index and // the db bucket used to house it. existsAddrIndexKey = []byte("existsaddridx") @@ -99,6 +105,13 @@ func (idx *ExistsAddrIndex) Name() string { return existsAddressIndexName } +// Version returns the current version of the index. +// +// This is part of the Indexer interface. +func (idx *ExistsAddrIndex) Version() uint32 { + return existsAddrIndexVersion +} + // Create is invoked when the indexer manager determines the index needs // to be created for the first time. It creates the bucket for the address // index. diff --git a/blockchain/indexers/manager.go b/blockchain/indexers/manager.go index a5c56569..710cfcf4 100644 --- a/blockchain/indexers/manager.go +++ b/blockchain/indexers/manager.go @@ -26,8 +26,9 @@ var ( ) // ----------------------------------------------------------------------------- -// The index manager tracks the current tip of each index by using a parent -// bucket that contains an entry for index. +// The index manager tracks the current tip and version of each index by using a +// parent bucket that contains an entry for index and a separate entry for its +// version. // // The serialized format for an index tip is: // @@ -36,6 +37,13 @@ var ( // Field Type Size // block hash chainhash.Hash chainhash.HashSize // block height uint32 4 bytes +// +// The serialized format for an index version is: +// +// [] +// +// Field Type Size +// index version uint32 4 bytes // ----------------------------------------------------------------------------- // dbPutIndexerTip uses an existing database transaction to update or add the @@ -68,6 +76,47 @@ func dbFetchIndexerTip(dbTx database.Tx, idxKey []byte) (*chainhash.Hash, int32, return &hash, height, nil } +// indexVersionKey returns the key for an index which houses the current version +// of the index. +func indexVersionKey(idxKey []byte) []byte { + verKey := make([]byte, len(idxKey)+1) + verKey[0] = 'v' + copy(verKey[1:], idxKey) + return verKey +} + +// dbPutIndexerVersion uses an existing database transaction to update the +// version for the given index to the provided value. +func dbPutIndexerVersion(dbTx database.Tx, idxKey []byte, version uint32) error { + serialized := make([]byte, 4) + byteOrder.PutUint32(serialized[0:4], version) + + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + return indexesBucket.Put(indexVersionKey(idxKey), serialized) +} + +// dbFetchIndexerVersion uses an existing database transaction to retrieve the +// version of the provided index. It will return one if the version has not +// previously been stored. +func dbFetchIndexerVersion(dbTx database.Tx, idxKey []byte) (uint32, error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + serialized := indexesBucket.Get(indexVersionKey(idxKey)) + if len(serialized) == 0 { + return 1, nil + } + + if len(serialized) < 4 { + return 0, database.Error{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf("unexpected end of data for "+ + "index %q version", string(idxKey)), + } + } + + version := byteOrder.Uint32(serialized) + return version, nil +} + // dbIndexConnectBlock adds all of the index entries associated with the // given block using the provided indexer and updates the tip of the indexer // accordingly. An error will be returned if the current tip for the indexer is @@ -208,32 +257,107 @@ func (m *Manager) maybeFinishDrops(interrupt <-chan struct{}) error { // maybeCreateIndexes determines if each of the enabled indexes have already // been created and creates them if not. -func (m *Manager) maybeCreateIndexes(dbTx database.Tx) error { - indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) - for _, indexer := range m.enabledIndexes { - // Nothing to do if the index tip already exists. - idxKey := indexer.Key() - if indexesBucket.Get(idxKey) != nil { - continue - } - - // The tip for the index does not exist, so create it and - // invoke the create callback for the index so it can perform - // any one-time initialization it requires. - if err := indexer.Create(dbTx); err != nil { - return err - } - - // Set the tip for the index to values which represent an - // uninitialized index (the genesis block hash and height). - genesisBlockHash := m.params.GenesisBlock.BlockHash() - err := dbPutIndexerTip(dbTx, idxKey, &genesisBlockHash, 0) +func (m *Manager) maybeCreateIndexes() error { + return m.db.Update(func(dbTx database.Tx) error { + // Create the bucket for the current tips as needed. + meta := dbTx.Metadata() + indexesBucket, err := meta.CreateBucketIfNotExists(indexTipsBucketName) if err != nil { return err } + + for _, indexer := range m.enabledIndexes { + // Nothing to do if the index tip already exists. + idxKey := indexer.Key() + if indexesBucket.Get(idxKey) != nil { + continue + } + + // Store the index version. + err := dbPutIndexerVersion(dbTx, idxKey, indexer.Version()) + if err != nil { + return err + } + + // The tip for the index does not exist, so create it and + // invoke the create callback for the index so it can perform + // any one-time initialization it requires. + if err := indexer.Create(dbTx); err != nil { + return err + } + + // Set the tip for the index to values which represent an + // uninitialized index (the genesis block hash and height). + genesisBlockHash := m.params.GenesisBlock.BlockHash() + err = dbPutIndexerTip(dbTx, idxKey, &genesisBlockHash, 0) + if err != nil { + return err + } + } + + return nil + }) +} + +// upgradeIndexes determines if each of the enabled indexes need to be upgraded +// and drops them when they do. +func (m *Manager) upgradeIndexes(interrupt <-chan struct{}) error { + indexNeedsDrop := make([]bool, len(m.enabledIndexes)) + err := m.db.View(func(dbTx database.Tx) error { + // None of the indexes needs to be updated if the index tips bucket + // hasn't been created yet. + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + if indexesBucket == nil { + return nil + } + + for i, indexer := range m.enabledIndexes { + idxKey := indexer.Key() + version, err := dbFetchIndexerVersion(dbTx, idxKey) + if err != nil { + return err + } + + // Upgrade is not needed if the index hasn't been created yet. + if version < indexer.Version() { + indexNeedsDrop[i] = true + } + } + + return nil + }) + if err != nil { + return err } - return nil + if interruptRequested(interrupt) { + return errInterruptRequested + } + + // Drop any of the enabled indexes that have bumped their version. + for i, indexer := range m.enabledIndexes { + if !indexNeedsDrop[i] { + continue + } + + log.Infof("Dropping %s due to new version", indexer.Name()) + + switch d := indexer.(type) { + case IndexDropper: + err := d.DropIndex(m.db, interrupt) + if err != nil { + return err + } + default: + err := dropIndex(m.db, indexer.Key(), indexer.Name()) + if err != nil { + return err + } + } + } + + // Create the initial state for the indexes that were dropped as needed. + return m.maybeCreateIndexes() } // dbFetchBlockByHash uses an existing database transaction to retrieve the raw @@ -270,17 +394,12 @@ func (m *Manager) Init(chain *blockchain.BlockChain, interrupt <-chan struct{}) } // Create the initial state for the indexes as needed. - err := m.db.Update(func(dbTx database.Tx) error { - // Create the bucket for the current tips as needed. - meta := dbTx.Metadata() - _, err := meta.CreateBucketIfNotExists(indexTipsBucketName) - if err != nil { - return err - } + if err := m.maybeCreateIndexes(); err != nil { + return err + } - return m.maybeCreateIndexes(dbTx) - }) - if err != nil { + // Upgrade the indexes as needed. + if err := m.upgradeIndexes(interrupt); err != nil { return err } @@ -295,6 +414,7 @@ func (m *Manager) Init(chain *blockchain.BlockChain, interrupt <-chan struct{}) // This is fairly unlikely, but it can happen if the chain is // reorganized while the index is disabled. This has to be done in // reverse order because later indexes can depend on earlier ones. + var err error var cachedBlock *dcrutil.Block for i := len(m.enabledIndexes); i > 0; i-- { indexer := m.enabledIndexes[i-1] @@ -734,6 +854,11 @@ func dropIndexMetadata(db database.DB, idxKey []byte, idxName string) error { return err } + err = indexesBucket.Delete(indexVersionKey(idxKey)) + if err != nil { + return err + } + return indexesBucket.Delete(indexDropKey(idxKey)) }) } @@ -776,8 +901,8 @@ func dropFlatIndex(db database.DB, idxKey []byte, idxName string, interrupt <-ch return err } - // Remove the index tip, index bucket, and in-progress drop flag now - // that all index entries have been removed. + // Remove the index tip, version, bucket, and in-progress drop flag now that + // all index entries have been removed. err = dropIndexMetadata(db, idxKey, idxName) if err != nil { return err @@ -812,8 +937,9 @@ func dropIndex(db database.DB, idxKey []byte, idxName string) error { return err } - // Remove the index tip, index bucket, and in-progress drop flag. Removing - // the index bucket also recursively removes all values saved to the index. + // Remove the index tip, version, bucket, and in-progress drop flag. + // Removing the index bucket also recursively removes all values saved to + // the index. err = dropIndexMetadata(db, idxKey, idxName) if err != nil { return err diff --git a/blockchain/indexers/txindex.go b/blockchain/indexers/txindex.go index fb92550c..b0298c3e 100644 --- a/blockchain/indexers/txindex.go +++ b/blockchain/indexers/txindex.go @@ -20,6 +20,9 @@ const ( // txIndexName is the human-readable name for the index. txIndexName = "transaction index" + // txIndexVersion is the current version of the transaction index. + txIndexVersion = 2 + // txEntrySize is the size of a transaction entry. It consists of 4 // bytes block id + 4 bytes offset + 4 bytes length + 4 bytes block // index. @@ -408,6 +411,13 @@ func (idx *TxIndex) Name() string { return txIndexName } +// Version returns the current version of the index. +// +// This is part of the Indexer interface. +func (idx *TxIndex) Version() uint32 { + return txIndexVersion +} + // Create is invoked when the indexer manager determines the index needs // to be created for the first time. It creates the buckets for the hash-based // transaction index and the internal block ID indexes. @@ -577,7 +587,7 @@ func DropTxIndex(db database.DB, interrupt <-chan struct{}) error { return err } - // Remove the index tip, index bucket, and in-progress drop flag now + // Remove the index tip, version, bucket, and in-progress drop flag now // that all index entries have been removed. err = dropIndexMetadata(db, txIndexKey, txIndexName) if err != nil { diff --git a/blockchain/stake/internal/ticketdb/chainio.go b/blockchain/stake/internal/ticketdb/chainio.go index 372da6ed..de58e3c5 100644 --- a/blockchain/stake/internal/ticketdb/chainio.go +++ b/blockchain/stake/internal/ticketdb/chainio.go @@ -662,6 +662,32 @@ func DbLoadAllTickets(dbTx database.Tx, ticketBucket []byte) (*tickettreap.Immut return treap, nil } +// DbRemoveAllBuckets removes all buckets from the database. +func DbRemoveAllBuckets(dbTx database.Tx) error { + meta := dbTx.Metadata() + err := meta.DeleteBucket(dbnamespace.StakeDbInfoBucketName) + if err != nil { + return err + } + err = meta.DeleteBucket(dbnamespace.LiveTicketsBucketName) + if err != nil { + return err + } + err = meta.DeleteBucket(dbnamespace.MissedTicketsBucketName) + if err != nil { + return err + } + err = meta.DeleteBucket(dbnamespace.RevokedTicketsBucketName) + if err != nil { + return err + } + err = meta.DeleteBucket(dbnamespace.StakeBlockUndoDataBucketName) + if err != nil { + return err + } + return meta.DeleteBucket(dbnamespace.TicketsInBlockBucketName) +} + // DbCreate initializes all the buckets required for the database and stores // the current database version information. func DbCreate(dbTx database.Tx) error { diff --git a/blockchain/stake/tickets.go b/blockchain/stake/tickets.go index fdfa9f27..908a98f5 100644 --- a/blockchain/stake/tickets.go +++ b/blockchain/stake/tickets.go @@ -225,6 +225,19 @@ func genesisNode(params *chaincfg.Params) *Node { } } +// ResetDatabase resets the ticket database back to the genesis block. +func ResetDatabase(dbTx database.Tx, params *chaincfg.Params) error { + // Remove all of the database buckets. + err := ticketdb.DbRemoveAllBuckets(dbTx) + if err != nil { + return err + } + + // Initialize the database. + _, err = InitDatabaseState(dbTx, params) + return err +} + // InitDatabaseState initializes the chain with the best state being the // genesis block. func InitDatabaseState(dbTx database.Tx, params *chaincfg.Params) (*Node, error) { diff --git a/blockchain/upgrade.go b/blockchain/upgrade.go index 7728f052..e8a8acf0 100644 --- a/blockchain/upgrade.go +++ b/blockchain/upgrade.go @@ -509,6 +509,215 @@ func upgradeToVersion4(db database.DB, dbInfo *databaseInfo, interrupt <-chan st }) } +// incrementalFlatDrop uses multiple database updates to remove key/value pairs +// saved to a flag bucket. +func incrementalFlatDrop(db database.DB, bucketKey []byte, humanName string, interrupt <-chan struct{}) error { + const maxDeletions = 2000000 + var totalDeleted uint64 + for numDeleted := maxDeletions; numDeleted == maxDeletions; { + numDeleted = 0 + err := db.Update(func(dbTx database.Tx) error { + bucket := dbTx.Metadata().Bucket(bucketKey) + cursor := bucket.Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() && + numDeleted < maxDeletions { + + if err := cursor.Delete(); err != nil { + return err + } + numDeleted++ + } + return nil + }) + if err != nil { + return err + } + + if numDeleted > 0 { + totalDeleted += uint64(numDeleted) + log.Infof("Deleted %d keys (%d total) from %s", numDeleted, + totalDeleted, humanName) + } + + if interruptRequested(interrupt) { + return errInterruptRequested + } + } + return nil +} + +// upgradeToVersion5 upgrades a version 4 blockchain database to version 5. +func upgradeToVersion5(db database.DB, chainParams *chaincfg.Params, dbInfo *databaseInfo, interrupt <-chan struct{}) error { + // Hardcoded bucket and key names so updates to the global values do not + // affect old upgrades. + utxoSetBucketName := []byte("utxoset") + spendJournalBucketName := []byte("spendjournal") + chainStateKeyName := []byte("chainstate") + v5ReindexTipKeyName := []byte("v5reindextip") + + log.Info("Clearing database utxoset and spend journal for upgrade...") + start := time.Now() + + // Clear the utxoset. + err := incrementalFlatDrop(db, utxoSetBucketName, "utxoset", interrupt) + if err != nil { + return err + } + log.Infof("Cleared utxoset.") + + if interruptRequested(interrupt) { + return errInterruptRequested + } + + // Clear the spend journal. + err = incrementalFlatDrop(db, spendJournalBucketName, "spend journal", + interrupt) + if err != nil { + return err + } + log.Infof("Cleared spend journal.") + + if interruptRequested(interrupt) { + return errInterruptRequested + } + + err = db.Update(func(dbTx database.Tx) error { + // Reset the ticket database to the genesis block. + log.Infof("Resetting the ticket database. This might take a while...") + if err := stake.ResetDatabase(dbTx, chainParams); err != nil { + return err + } + + // Fetch the stored best chain state from the database metadata. + meta := dbTx.Metadata() + serializedData := meta.Get(chainStateKeyName) + best, err := deserializeBestChainState(serializedData) + if err != nil { + return err + } + + // Store the current best chain tip as the reindex target. + if err := meta.Put(v5ReindexTipKeyName, best.hash[:]); err != nil { + return err + } + + // Reset the state related to the best block to the genesis block. + genesisBlock := chainParams.GenesisBlock + numTxns := uint64(len(genesisBlock.Transactions)) + serializedData = serializeBestChainState(bestChainState{ + hash: genesisBlock.BlockHash(), + height: 0, + totalTxns: numTxns, + totalSubsidy: 0, + workSum: CalcWork(genesisBlock.Header.Bits), + }) + return meta.Put(chainStateKeyName, serializedData) + }) + if err != nil { + return err + } + + elapsed := time.Since(start).Round(time.Millisecond) + log.Infof("Done upgrading database in %v.", elapsed) + + // Update and persist the updated database versions. + dbInfo.version = 5 + return db.Update(func(dbTx database.Tx) error { + return dbPutDatabaseInfo(dbTx, dbInfo) + }) + + return nil +} + +// maybeFinishV5Upgrade potentially reindexes the chain due to a version 5 +// database upgrade. It will resume previously uncompleted attempts. +func (b *BlockChain) maybeFinishV5Upgrade() error { + // Nothing to do if the database is not version 5. + if b.dbInfo.version != 5 { + return nil + } + + // Hardcoded key name so updates to the global values do not affect old + // upgrades. + v5ReindexTipKeyName := []byte("v5reindextip") + + // Finish the version 5 reindex as needed. + var v5ReindexTipHash *chainhash.Hash + err := b.db.View(func(dbTx database.Tx) error { + hash := dbTx.Metadata().Get(v5ReindexTipKeyName) + if hash != nil { + v5ReindexTipHash = new(chainhash.Hash) + copy(v5ReindexTipHash[:], hash) + } + return nil + }) + if err != nil { + return err + } + if v5ReindexTipHash != nil { + // Look up the final target tip to reindex to in the block index. + targetTip := b.index.LookupNode(v5ReindexTipHash) + if targetTip == nil { + return AssertError(fmt.Sprintf("maybeFinishV5Upgrade: cannot find "+ + "chain tip %s in block index", v5ReindexTipHash)) + } + + // Ensure all ancestors of the current best chain tip are marked as + // valid. This is necessary due to older software versions not marking + // nodes before the final checkpoint as valid. + for node := targetTip; node != nil; node = node.parent { + b.index.SetStatusFlags(node, statusValid) + } + if err := b.index.flush(); err != nil { + return err + } + + // Disable notifications during the reindex. + ntfnCallback := b.notifications + b.notifications = nil + defer func() { + b.notifications = ntfnCallback + }() + + tip := b.bestChain.Tip() + for tip != targetTip { + if interruptRequested(b.interrupt) { + return errInterruptRequested + } + + // Limit to a reasonable number of blocks at a time. + const maxReindexBlocks = 250 + intermediateTip := targetTip + if intermediateTip.height-tip.height > maxReindexBlocks { + intermediateTip = intermediateTip.Ancestor(tip.height + + maxReindexBlocks) + } + + log.Infof("Reindexing to height %d of %d (progress %.2f%%)...", + intermediateTip.height, targetTip.height, + float64(intermediateTip.height)/float64(targetTip.height)*100) + b.chainLock.Lock() + if err := b.reorganizeChainInternal(intermediateTip); err != nil { + b.chainLock.Unlock() + return err + } + b.chainLock.Unlock() + + tip = b.bestChain.Tip() + } + + // Mark the v5 reindex as complete by removing the associated key. + err := b.db.Update(func(dbTx database.Tx) error { + return dbTx.Metadata().Delete(v5ReindexTipKeyName) + }) + if err != nil { + return err + } + } + + return nil +} + // upgradeDB upgrades old database versions to the newest version by applying // all possible upgrades iteratively. // @@ -536,18 +745,13 @@ func upgradeDB(db database.DB, chainParams *chaincfg.Params, dbInfo *databaseInf } } - // NOTE: The next time a new database version is needed, the code in - // initChainState which marks all ancestors of the current chain tip as - // valid should be converted to updgrade all nodes in the database in the - // upgrade path here and removed from the chain init. The version was not - // bumped when applying the update since it is possible to perform very - // quickly at startup on the block nodes in memory without requiring a - // database version bump. - - // TODO(davec): Replace with proper upgrade code for utxo set semantics - // reversal and index updates. + // Clear the utxoset, clear the spend journal, reset the best chain back to + // the genesis block, and mark that a v5 reindex is required if needed. if dbInfo.version == 4 { - return errors.New("Upgrade from version 4 database not supported yet") + err := upgradeToVersion5(db, chainParams, dbInfo, interrupt) + if err != nil { + return err + } } return nil