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 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)

View File

@ -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)