diff --git a/lib/chain/chain.js b/lib/chain/chain.js index 7333f344..19feb01c 100644 --- a/lib/chain/chain.js +++ b/lib/chain/chain.js @@ -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 diff --git a/lib/chain/chaindb.js b/lib/chain/chaindb.js index 51519b1f..e1fe2c77 100644 --- a/lib/chain/chaindb.js +++ b/lib/chain/chaindb.js @@ -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; }); /** diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 3d2de8a0..3c3717fe 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -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); }; /**