diff --git a/lib/chain/chain.js b/lib/chain/chain.js index 2bddba67..407ee85f 100644 --- a/lib/chain/chain.js +++ b/lib/chain/chain.js @@ -760,7 +760,7 @@ Chain.prototype.reorganize = co(function* reorganize(competitor, block) { var connect = []; var i, entry; - assert(fork); + assert(fork, 'No free space or data corruption.'); // Blocks to disconnect. entry = tip; @@ -796,7 +796,8 @@ Chain.prototype.reorganize = co(function* reorganize(competitor, block) { }); /** - * Reorganize the blockchain for SPV. + * Reorganize the blockchain for SPV. This + * will reset the chain to the fork block. * @private * @param {ChainEntry} competitor - The competing chain's tip. * @param {Block|MerkleBlock} block - The being being added. @@ -806,22 +807,32 @@ Chain.prototype.reorganize = co(function* reorganize(competitor, block) { Chain.prototype.reorganizeSPV = co(function* reorganizeSPV(competitor, block) { var tip = this.tip; var fork = yield this.findFork(tip, competitor); - var entry; + var disconnect = []; + var entry = tip; + var i; - assert(fork); + assert(fork, 'No free space or data corruption.'); - // Blocks to disconnect. - entry = tip; + // Buffer disconnected blocks. while (entry.hash !== fork.hash) { - this.emit('disconnect', entry, entry.toHeaders()); + disconnect.push(entry); entry = yield entry.getPrevious(); assert(entry); } // Reset the main chain back - // to the fork block. + // to the fork block, causing + // us to redownload the blocks + // on the new main chain. yield this._reset(fork.hash); + // Emit disconnection events now that + // the chain has successfully reset. + for (i = 0; i < disconnect.length; i++) { + entry = disconnect[i]; + this.emit('disconnect', entry, entry.toHeaders()); + } + this.emit('reorganize', block, tip.height, tip.hash); }); @@ -983,46 +994,40 @@ Chain.prototype.saveAlternate = co(function* saveAlternate(entry, block, prev) { }); /** - * Reset the chain to the desired height. This + * Reset the chain to the desired block. This * is useful for replaying the blockchain download * for SPV. - * @param {Number} height + * @param {Hash|Number} block * @returns {Promise} */ -Chain.prototype.reset = co(function* reset(height) { +Chain.prototype.reset = co(function* reset(block) { var unlock = yield this.locker.lock(); try { - return yield this._reset(height); + return yield this._reset(block); } finally { unlock(); } }); /** - * Reset the chain to the desired height without a lock. + * Reset the chain to the desired block without a lock. * @private - * @param {Number} height + * @param {Hash|Number} block * @returns {Promise} */ -Chain.prototype._reset = co(function* reset(height) { - var tip = yield this.db.reset(height); - - this.synced = false; +Chain.prototype._reset = co(function* reset(block) { + var tip = yield this.db.reset(block); + // Reset state. this.tip = tip; this.height = tip.height; - this.state = yield this.getDeploymentState(); + this.synced = this.isFull(true); this.emit('tip', tip); - if (this.isFull()) { - this.synced = true; - this.emit('full'); - } - // Reset the orphan map completely. There may // have been some orphans on a forked chain we // no longer need. @@ -1583,8 +1588,8 @@ Chain.prototype.getOrphan = function getOrphan(hash) { * @returns {Boolean} */ -Chain.prototype.isFull = function isFull() { - return !this.isInitial(); +Chain.prototype.isFull = function isFull(force) { + return !this.isInitial(force); }; /** @@ -1595,8 +1600,8 @@ Chain.prototype.isFull = function isFull() { * @returns {Boolean} */ -Chain.prototype.isInitial = function isInitial() { - if (this.synced) +Chain.prototype.isInitial = function isInitial(force) { + if (!force && this.synced) return false; if (this.height < this.network.checkpoints.lastHeight) diff --git a/lib/chain/chaindb.js b/lib/chain/chaindb.js index e5c19cd0..67077333 100644 --- a/lib/chain/chaindb.js +++ b/lib/chain/chaindb.js @@ -661,7 +661,7 @@ ChainDB.prototype.getEntries = function getEntries() { return this.db.values({ gte: layout.e(constants.ZERO_HASH), lte: layout.e(constants.MAX_HASH), - parse: function(key, value) { + parse: function(value) { return ChainEntry.fromRaw(self.chain, value); } }); @@ -1229,34 +1229,34 @@ ChainDB.prototype.save = co(function* save(entry, block, view) { ChainDB.prototype._save = co(function* save(entry, block, view) { var hash = block.hash(); - // Hash->height index + // Hash->height index. this.put(layout.h(hash), U32(entry.height)); - // Entry data + // Entry data. this.put(layout.e(hash), entry.toRaw()); this.cacheHash.push(entry.hash, entry); - // Tip index + // Tip index. this.del(layout.a(entry.prevBlock)); this.put(layout.a(hash), DUMMY); if (!view) { - // Save block data + // Save block data. yield this.saveBlock(block); return; } - // Hash->next-block index + // Hash->next-block index. this.put(layout.n(entry.prevBlock), hash); - // Height->hash index + // Height->hash index. this.put(layout.H(entry.height), hash); this.cacheHeight.push(entry.height, entry); - // Connect block and save data + // Connect block and save data. yield this.saveBlock(block, view); - // New chain state + // Commit new chain state. this.put(layout.R, this.pending.commit(hash)); }); @@ -1291,14 +1291,20 @@ ChainDB.prototype.reconnect = co(function* reconnect(entry, block, view) { ChainDB.prototype._reconnect = co(function* reconnect(entry, block, view) { var hash = block.hash(); + // We can now add a hash->next-block index. this.put(layout.n(entry.prevBlock), hash); - this.put(layout.H(entry.height), hash); - this.cacheHash.push(entry.hash, entry); + // We can now add a height->hash index. + this.put(layout.H(entry.height), hash); this.cacheHeight.push(entry.height, entry); + // Re-insert into cache. + this.cacheHash.push(entry.hash, entry); + + // Connect inputs. yield this.connectBlock(block, view); + // Update chain state. this.put(layout.R, this.pending.commit(hash)); }); @@ -1335,9 +1341,11 @@ ChainDB.prototype.disconnect = co(function* disconnect(entry) { ChainDB.prototype._disconnect = co(function* disconnect(entry) { var block; + // Remove hash->next-block index. this.del(layout.n(entry.prevBlock)); - this.del(layout.H(entry.height)); + // Remove height->hash index. + this.del(layout.H(entry.height)); this.cacheHeight.unpush(entry.height); block = yield this.getBlock(entry.hash); @@ -1345,8 +1353,10 @@ ChainDB.prototype._disconnect = co(function* disconnect(entry) { if (!block) throw new Error('Block not found.'); + // Disconnect inputs. yield this.disconnectBlock(block); + // Revert chain state to previous tip. this.put(layout.R, this.pending.commit(entry.prevBlock)); return block; @@ -1386,6 +1396,7 @@ ChainDB.prototype.reset = co(function* reset(block) { for (;;) { this.start(); + // Stop once we hit our target tip. if (tip.hash === entry.hash) { this.put(layout.R, this.pending.commit(tip.hash)); yield this.commit(); @@ -1394,14 +1405,18 @@ ChainDB.prototype.reset = co(function* reset(block) { assert(!tip.isGenesis()); + // Revert the tip index. this.del(layout.a(tip.hash)); this.put(layout.a(tip.prevBlock), DUMMY); + // Remove all records (including + // main-chain-only records). this.del(layout.H(tip.height)); this.del(layout.h(tip.hash)); this.del(layout.e(tip.hash)); this.del(layout.n(tip.prevBlock)); + // Disconnect and remove block data. try { yield this.removeBlock(tip.hash); } catch (e) { @@ -1409,10 +1424,12 @@ ChainDB.prototype.reset = co(function* reset(block) { throw e; } + // Revert chain state to previous tip. this.put(layout.R, this.pending.commit(tip.prevBlock)); yield this.commit(); + // Update caches _after_ successful commit. this.cacheHeight.remove(tip.height); this.cacheHash.remove(tip.hash); @@ -1432,17 +1449,29 @@ ChainDB.prototype.removeChains = co(function* removeChains() { var tips = yield this.getTips(); var i; - for (i = 0; i < tips.length; i++) - yield this.removeChain(tips[i]); + // Note that this has to be + // one giant atomic write! + this.start(); + + try { + for (i = 0; i < tips.length; i++) + yield this._removeChain(tips[i]); + } catch (e) { + this.drop(); + throw e; + } + + yield this.commit(); }); /** * Remove an alternate chain. + * @private * @param {Hash} hash - Alternate chain tip. * @returns {Promise} */ -ChainDB.prototype.removeChain = co(function* removeChain(hash) { +ChainDB.prototype._removeChain = co(function* removeChain(hash) { var tip = yield this.get(hash); if (!tip) @@ -1450,27 +1479,25 @@ ChainDB.prototype.removeChain = co(function* removeChain(hash) { this.logger.debug('Removing alternate chain: %s.', tip.rhash); - this.start(); - - while (tip) { + for (;;) { if (yield tip.isMainChain()) break; assert(!tip.isGenesis()); + // Remove all non-main-chain records. this.del(layout.a(tip.hash)); this.del(layout.h(tip.hash)); this.del(layout.e(tip.hash)); this.del(layout.b(tip.hash)); + // Queue up hash to be removed + // on successful write. this.cacheHash.unpush(tip.hash); tip = yield this.get(tip.prevBlock); + assert(tip); } - - yield this.commit(); - - return tip; }); /** @@ -1485,6 +1512,8 @@ ChainDB.prototype.saveBlock = co(function* saveBlock(block, view) { if (this.options.spv) return; + // Write actual block data (this may be + // better suited to flat files in the future). this.put(layout.b(block.hash()), block.toRaw()); if (!view)