chain: improve spv reorgs.

This commit is contained in:
Christopher Jeffrey 2016-11-11 14:40:16 -08:00
parent 41f711987c
commit 708c4a2bd2
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
2 changed files with 84 additions and 50 deletions

View File

@ -760,7 +760,7 @@ Chain.prototype.reorganize = co(function* reorganize(competitor, block) {
var connect = []; var connect = [];
var i, entry; var i, entry;
assert(fork); assert(fork, 'No free space or data corruption.');
// Blocks to disconnect. // Blocks to disconnect.
entry = tip; 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 * @private
* @param {ChainEntry} competitor - The competing chain's tip. * @param {ChainEntry} competitor - The competing chain's tip.
* @param {Block|MerkleBlock} block - The being being added. * @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) { Chain.prototype.reorganizeSPV = co(function* reorganizeSPV(competitor, block) {
var tip = this.tip; var tip = this.tip;
var fork = yield this.findFork(tip, competitor); 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. // Buffer disconnected blocks.
entry = tip;
while (entry.hash !== fork.hash) { while (entry.hash !== fork.hash) {
this.emit('disconnect', entry, entry.toHeaders()); disconnect.push(entry);
entry = yield entry.getPrevious(); entry = yield entry.getPrevious();
assert(entry); assert(entry);
} }
// Reset the main chain back // 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); 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); 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 * is useful for replaying the blockchain download
* for SPV. * for SPV.
* @param {Number} height * @param {Hash|Number} block
* @returns {Promise} * @returns {Promise}
*/ */
Chain.prototype.reset = co(function* reset(height) { Chain.prototype.reset = co(function* reset(block) {
var unlock = yield this.locker.lock(); var unlock = yield this.locker.lock();
try { try {
return yield this._reset(height); return yield this._reset(block);
} finally { } finally {
unlock(); unlock();
} }
}); });
/** /**
* Reset the chain to the desired height without a lock. * Reset the chain to the desired block without a lock.
* @private * @private
* @param {Number} height * @param {Hash|Number} block
* @returns {Promise} * @returns {Promise}
*/ */
Chain.prototype._reset = co(function* reset(height) { Chain.prototype._reset = co(function* reset(block) {
var tip = yield this.db.reset(height); var tip = yield this.db.reset(block);
this.synced = false;
// Reset state.
this.tip = tip; this.tip = tip;
this.height = tip.height; this.height = tip.height;
this.state = yield this.getDeploymentState(); this.state = yield this.getDeploymentState();
this.synced = this.isFull(true);
this.emit('tip', tip); this.emit('tip', tip);
if (this.isFull()) {
this.synced = true;
this.emit('full');
}
// Reset the orphan map completely. There may // Reset the orphan map completely. There may
// have been some orphans on a forked chain we // have been some orphans on a forked chain we
// no longer need. // no longer need.
@ -1583,8 +1588,8 @@ Chain.prototype.getOrphan = function getOrphan(hash) {
* @returns {Boolean} * @returns {Boolean}
*/ */
Chain.prototype.isFull = function isFull() { Chain.prototype.isFull = function isFull(force) {
return !this.isInitial(); return !this.isInitial(force);
}; };
/** /**
@ -1595,8 +1600,8 @@ Chain.prototype.isFull = function isFull() {
* @returns {Boolean} * @returns {Boolean}
*/ */
Chain.prototype.isInitial = function isInitial() { Chain.prototype.isInitial = function isInitial(force) {
if (this.synced) if (!force && this.synced)
return false; return false;
if (this.height < this.network.checkpoints.lastHeight) if (this.height < this.network.checkpoints.lastHeight)

View File

@ -661,7 +661,7 @@ ChainDB.prototype.getEntries = function getEntries() {
return this.db.values({ return this.db.values({
gte: layout.e(constants.ZERO_HASH), gte: layout.e(constants.ZERO_HASH),
lte: layout.e(constants.MAX_HASH), lte: layout.e(constants.MAX_HASH),
parse: function(key, value) { parse: function(value) {
return ChainEntry.fromRaw(self.chain, 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) { ChainDB.prototype._save = co(function* save(entry, block, view) {
var hash = block.hash(); var hash = block.hash();
// Hash->height index // Hash->height index.
this.put(layout.h(hash), U32(entry.height)); this.put(layout.h(hash), U32(entry.height));
// Entry data // Entry data.
this.put(layout.e(hash), entry.toRaw()); this.put(layout.e(hash), entry.toRaw());
this.cacheHash.push(entry.hash, entry); this.cacheHash.push(entry.hash, entry);
// Tip index // Tip index.
this.del(layout.a(entry.prevBlock)); this.del(layout.a(entry.prevBlock));
this.put(layout.a(hash), DUMMY); this.put(layout.a(hash), DUMMY);
if (!view) { if (!view) {
// Save block data // Save block data.
yield this.saveBlock(block); yield this.saveBlock(block);
return; return;
} }
// Hash->next-block index // Hash->next-block index.
this.put(layout.n(entry.prevBlock), hash); this.put(layout.n(entry.prevBlock), hash);
// Height->hash index // Height->hash index.
this.put(layout.H(entry.height), hash); this.put(layout.H(entry.height), hash);
this.cacheHeight.push(entry.height, entry); this.cacheHeight.push(entry.height, entry);
// Connect block and save data // Connect block and save data.
yield this.saveBlock(block, view); yield this.saveBlock(block, view);
// New chain state // Commit new chain state.
this.put(layout.R, this.pending.commit(hash)); 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) { ChainDB.prototype._reconnect = co(function* reconnect(entry, block, view) {
var hash = block.hash(); var hash = block.hash();
// We can now add a hash->next-block index.
this.put(layout.n(entry.prevBlock), hash); 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); this.cacheHeight.push(entry.height, entry);
// Re-insert into cache.
this.cacheHash.push(entry.hash, entry);
// Connect inputs.
yield this.connectBlock(block, view); yield this.connectBlock(block, view);
// Update chain state.
this.put(layout.R, this.pending.commit(hash)); 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) { ChainDB.prototype._disconnect = co(function* disconnect(entry) {
var block; var block;
// Remove hash->next-block index.
this.del(layout.n(entry.prevBlock)); 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); this.cacheHeight.unpush(entry.height);
block = yield this.getBlock(entry.hash); block = yield this.getBlock(entry.hash);
@ -1345,8 +1353,10 @@ ChainDB.prototype._disconnect = co(function* disconnect(entry) {
if (!block) if (!block)
throw new Error('Block not found.'); throw new Error('Block not found.');
// Disconnect inputs.
yield this.disconnectBlock(block); yield this.disconnectBlock(block);
// Revert chain state to previous tip.
this.put(layout.R, this.pending.commit(entry.prevBlock)); this.put(layout.R, this.pending.commit(entry.prevBlock));
return block; return block;
@ -1386,6 +1396,7 @@ ChainDB.prototype.reset = co(function* reset(block) {
for (;;) { for (;;) {
this.start(); this.start();
// Stop once we hit our target tip.
if (tip.hash === entry.hash) { if (tip.hash === entry.hash) {
this.put(layout.R, this.pending.commit(tip.hash)); this.put(layout.R, this.pending.commit(tip.hash));
yield this.commit(); yield this.commit();
@ -1394,14 +1405,18 @@ ChainDB.prototype.reset = co(function* reset(block) {
assert(!tip.isGenesis()); assert(!tip.isGenesis());
// Revert the tip index.
this.del(layout.a(tip.hash)); this.del(layout.a(tip.hash));
this.put(layout.a(tip.prevBlock), DUMMY); 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.height));
this.del(layout.h(tip.hash)); this.del(layout.h(tip.hash));
this.del(layout.e(tip.hash)); this.del(layout.e(tip.hash));
this.del(layout.n(tip.prevBlock)); this.del(layout.n(tip.prevBlock));
// Disconnect and remove block data.
try { try {
yield this.removeBlock(tip.hash); yield this.removeBlock(tip.hash);
} catch (e) { } catch (e) {
@ -1409,10 +1424,12 @@ ChainDB.prototype.reset = co(function* reset(block) {
throw e; throw e;
} }
// Revert chain state to previous tip.
this.put(layout.R, this.pending.commit(tip.prevBlock)); this.put(layout.R, this.pending.commit(tip.prevBlock));
yield this.commit(); yield this.commit();
// Update caches _after_ successful commit.
this.cacheHeight.remove(tip.height); this.cacheHeight.remove(tip.height);
this.cacheHash.remove(tip.hash); this.cacheHash.remove(tip.hash);
@ -1432,17 +1449,29 @@ ChainDB.prototype.removeChains = co(function* removeChains() {
var tips = yield this.getTips(); var tips = yield this.getTips();
var i; var i;
for (i = 0; i < tips.length; i++) // Note that this has to be
yield this.removeChain(tips[i]); // 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. * Remove an alternate chain.
* @private
* @param {Hash} hash - Alternate chain tip. * @param {Hash} hash - Alternate chain tip.
* @returns {Promise} * @returns {Promise}
*/ */
ChainDB.prototype.removeChain = co(function* removeChain(hash) { ChainDB.prototype._removeChain = co(function* removeChain(hash) {
var tip = yield this.get(hash); var tip = yield this.get(hash);
if (!tip) if (!tip)
@ -1450,27 +1479,25 @@ ChainDB.prototype.removeChain = co(function* removeChain(hash) {
this.logger.debug('Removing alternate chain: %s.', tip.rhash); this.logger.debug('Removing alternate chain: %s.', tip.rhash);
this.start(); for (;;) {
while (tip) {
if (yield tip.isMainChain()) if (yield tip.isMainChain())
break; break;
assert(!tip.isGenesis()); assert(!tip.isGenesis());
// Remove all non-main-chain records.
this.del(layout.a(tip.hash)); this.del(layout.a(tip.hash));
this.del(layout.h(tip.hash)); this.del(layout.h(tip.hash));
this.del(layout.e(tip.hash)); this.del(layout.e(tip.hash));
this.del(layout.b(tip.hash)); this.del(layout.b(tip.hash));
// Queue up hash to be removed
// on successful write.
this.cacheHash.unpush(tip.hash); this.cacheHash.unpush(tip.hash);
tip = yield this.get(tip.prevBlock); 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) if (this.options.spv)
return; return;
// Write actual block data (this may be
// better suited to flat files in the future).
this.put(layout.b(block.hash()), block.toRaw()); this.put(layout.b(block.hash()), block.toRaw());
if (!view) if (!view)