spv: redownload blocks after reorg. handle reset properly. always save to main chain.

This commit is contained in:
Christopher Jeffrey 2016-11-10 14:32:51 -08:00
parent b91d0cd9f1
commit e68a64d1f9
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
3 changed files with 114 additions and 22 deletions

View File

@ -15,6 +15,7 @@ var constants = require('../protocol/constants');
var utils = require('../utils/utils');
var Locker = require('../utils/locker');
var ChainEntry = require('./chainentry');
var CoinView = require('./coinview');
var assert = require('assert');
var VerifyError = require('../utils/errors').VerifyError;
var VerifyResult = utils.VerifyResult;
@ -610,10 +611,10 @@ Chain.prototype.verifyInputs = co(function* verifyInputs(block, prev, state) {
var i, view, tx, valid;
if (this.options.spv)
return;
return new CoinView();
if (this.isGenesis(block))
return;
return new CoinView();
view = yield this.db.getCoinView(block);
@ -794,6 +795,42 @@ Chain.prototype.reorganize = co(function* reorganize(competitor, block) {
this.emit('reorganize', block, tip.height, tip.hash);
});
/**
* Reorganize the blockchain for SPV.
* @private
* @param {ChainEntry} competitor - The competing chain's tip.
* @param {Block|MerkleBlock} block - The being being added.
* @returns {Promise}
*/
Chain.prototype.reorganizeSPV = co(function* reorganizeSPV(competitor, block) {
var tip = this.tip;
var fork = yield this.findFork(tip, competitor);
var entry, prev;
assert(fork);
// Blocks to disconnect.
entry = tip;
while (entry.hash !== fork.hash) {
this.emit('disconnect', entry, entry.toHeaders());
entry = yield entry.getPrevious();
assert(entry);
}
// We need to remove the alternate
// chain to prevent further reorgs.
prev = yield competitor.getPrevious();
assert(prev);
yield this.db.removeChain(prev, fork);
// Reset the main chain back
// to the fork block.
yield this._reset(fork.hash);
this.emit('reorganize', block, tip.height, tip.hash);
});
/**
* Disconnect an entry from the chain (updates the tip).
* @param {ChainEntry} entry
@ -829,7 +866,12 @@ Chain.prototype.reconnect = co(function* reconnect(entry) {
var block = yield this.db.getBlock(entry.hash);
var prev, view;
assert(block);
if (this.options.spv) {
assert(!block);
block = entry.toHeaders();
} else {
assert(block);
}
prev = yield entry.getPrevious();
assert(prev);
@ -871,14 +913,18 @@ Chain.prototype.reconnect = co(function* reconnect(entry) {
*/
Chain.prototype.setBestChain = co(function* setBestChain(entry, block, prev) {
var view;
assert(this.tip);
var view, fork;
// A higher fork has arrived.
// Time to reorganize the chain.
if (entry.prevBlock !== this.tip.hash) {
this.logger.warning('WARNING: Reorganizing chain.');
// In spv-mode, we reset the
// chain and redownload the blocks.
if (this.options.spv)
return yield this.reorganizeSPV(entry, block);
yield this.reorganize(entry, block);
}
@ -967,16 +1013,50 @@ Chain.prototype.reset = co(function* reset(height) {
*/
Chain.prototype._reset = co(function* reset(height) {
yield this.db.reset(height);
var tip = yield this.db.reset(height);
this.synced = false;
this.tip = tip;
this.height = tip.height;
this.state = yield this.getDeploymentState();
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.
this.purgeOrphans();
});
/**
* Reset the chain to a height or hash. Useful for replaying
* the blockchain download for SPV.
* @param {Hash|Number} block - hash/height
* @returns {Promise}
*/
Chain.prototype.replay = co(function* replay(block) {
var entry = yield this.db.get(block);
if (!entry)
throw new Error('Block not found.');
if (!(yield entry.isMainChain()))
throw new Error('Cannot reset on alternate chain.');
if (entry.hash === this.network.genesis.hash)
return yield this.reset(entry.hash);
yield this.reset(entry.prevBlock);
});
/**
* Reset the chain to the desired timestamp (within 2
* hours). This is useful for replaying the blockchain

View File

@ -1351,7 +1351,8 @@ ChainDB.prototype.reset = co(function* reset(block) {
if (tip.hash === entry.hash) {
this.put(layout.R, this.pending.commit(tip.hash));
return yield this.commit();
yield this.commit();
break;
}
this.del(layout.H(tip.height));
@ -1376,28 +1377,39 @@ ChainDB.prototype.reset = co(function* reset(block) {
tip = yield this.get(tip.prevBlock);
assert(tip);
}
return tip;
});
/**
* Reset the chain to a height or hash. Useful for replaying
* the blockchain download for SPV.
* @param {Hash|Number} block - hash/height
* Remove an alternate chain.
* @param {ChainEntry} tip
* @param {ChainEntry} entry
* @returns {Promise}
*/
ChainDB.prototype.replay = co(function* replay(block) {
var entry = yield this.get(block);
ChainDB.prototype.removeChain = co(function* removeChain(tip, entry) {
this.logger.debug('Removing alternate chain: %s->%s', tip.rhash, entry.rhash);
if (!entry)
throw new Error('Block not found.');
while (!tip.isGenesis()) {
if (tip.hash === entry.hash)
break;
if (!(yield entry.isMainChain()))
throw new Error('Cannot reset on alternate chain.');
this.start();
if (entry.hash === this.network.genesis.hash)
return yield this.reset(entry.hash);
this.del(layout.h(tip.hash));
this.del(layout.e(tip.hash));
this.del(layout.b(tip.hash));
yield this.reset(entry.prevBlock);
yield this.commit();
this.cacheHash.remove(tip.hash);
tip = yield this.get(tip.prevBlock);
assert(tip);
}
return tip;
});
/**

View File

@ -147,7 +147,7 @@ SPVNode.prototype._init = function _init() {
});
this.chain.on('connect', function(entry, block) {
self.walletdb.addBlock(entry, block.txs).catch(onError);
self.walletdb.addBlock(entry, block.txs || []).catch(onError);
});
this.chain.on('disconnect', function(entry, block) {
@ -220,7 +220,7 @@ SPVNode.prototype.watchData = function watchData(chunks) {
*/
SPVNode.prototype.scan = function rescan(start) {
return this.chain.db.replay(start);
return this.chain.replay(start);
};
/**