diff --git a/lib/services/block/index.js b/lib/services/block/index.js index 0c5ab433..eb704776 100644 --- a/lib/services/block/index.js +++ b/lib/services/block/index.js @@ -111,12 +111,6 @@ BlockService.prototype._reportBootStatus = function() { blockInfoString); }; -BlockService.prototype._setTip = function(block) { - this.tip.height = block.height; - this.tip.hash = block.hash; - this._db.setServiceTip('block', this.tip); -}; - BlockService.prototype.processBlockOperations = function(opts, callback) { if (!_.isArray(opts.operations)) { @@ -221,18 +215,6 @@ BlockService.prototype.getBlockHeader = function(hash, callback) { timer.unref(); }; -BlockService.prototype.getBlockHash = function(height, callback) { - - this._getBlockValue(height, callback); - -}; - -BlockService.prototype.getBlockHeight = function(hash, callback) { - - this._getBlockValue(hash, callback); - -}; - BlockService.prototype._startSubscriptions = function() { var self = this; @@ -276,48 +258,57 @@ BlockService.prototype._mergeBlockIntoChainTips = function(block) { chain.unshift(prevHash); } - var keys = this._chainTips.keys(); - for(var i = 0; i < keys.length; i++) { - var key = keys[i]; - var searchChain = this._chainTips.get(key); + var longestChain = this._findLongestChainForHash(prevHash); + hasChildren = this._setChainOnTip(block.hash, chain, longestChain); - assert(searchChain.length > 0, 'chain tips collection appears to be invalid'); - var chainIndex = searchChain.indexOf(block.hash); - - if (chainIndex > -1) { - hasChildren = true; - this._chainTips.set(key, searchChain.concat(chain || [prevHash])); - } + if (!hasChildren) { + this._chainTips.set(block.hash, chain || longestChain); } - if (chain && !hasChildren) { - this._chainTips.set(block.hash, chain); - } - - if (!chain && !hasChildren) { - - var longestChain = []; - for(var j = 0; j < keys.length; j++) { - var key = keys[j]; - var searchChain = this._chainTips.get(key); - var chainIndex = searchChain.indexOf(prevHash); - if (chainIndex > -1) { - var chain = searchChain.slice(chainIndex); - if (chain.length > longestChain.length) { - longestChain = chain; - } - } - } - longestChain = longestChain.length <= 1 ? [prevHash] : longestChain; - this._chainTips.set(block.hash, longestChain); - } -console.log(block.hash); -console.log(this._chainTips); - }; -BlockService.prototype._onBlock = function(block) { +BlockService.prototype._setChainOnTip = function(hash, chain, longestPrevChain) { + + var keys = this._chainTips.keys(); + var hasChildren = false; + + for(var i = 0; i < keys.length; i++) { + + var key = keys[i]; + var searchChain = this._chainTips.get(key); + + + var chainIndex = searchChain.indexOf(hash); + + if (chainIndex > -1) { + hasChildren = true; + this._chainTips.set(key, searchChain.concat(chain || longestPrevChain)); + } + } + + return hasChildren; +}; + +BlockService.prototype._findLongestChainForHash = function(hash) { + var longestChain = []; + var keys = this._chainTips.keys(); + for(var j = 0; j < keys.length; j++) { + var key = keys[j]; + var searchChain = this._chainTips.get(key); + assert(searchChain.length > 0, 'chain tips collection appears to be invalid'); + var chainIndex = searchChain.indexOf(hash); + if (chainIndex > -1) { + var chain = searchChain.slice(chainIndex); + if (chain.length > longestChain.length) { + longestChain = chain; + } + } + } + longestChain = longestChain.length <= 1 ? [hash] : longestChain; + return longestChain; +}; + BlockService.prototype._onBlock = function(block) { // 1. have we already seen this block? @@ -340,21 +331,128 @@ BlockService.prototype._onBlock = function(block) { // 6. react to state of block switch (blockState) { case 'orphaned': - this._queueOrphanedBlock(block); break; case 'reorg': this.emit('reorg', block); break; default: - this.setTip(block); - this._broadcast(this.subscriptions.blocks, 'block/block', block); - // check to see if we can broadcast, previously orphaned blocks on the main chain. + this._sendAllUnsentBlocksFromMainChain(block); break; } }; -BlockService.prototype._setTip = function() { +BlockService.prototype._sendAllUnsentBlocksFromMainChain = function(block) { + + var blocksToSend = [block]; + + if (!this._chainTips.get(block.hash)) { + + var keys = this._chainTips.keys(); + + for(var i = 0; i < keys.length; i++) { + var key = keys[i]; + var searchChain = this._chainTips.get(key); + var index = searchChain.indexOf(block.hash); + + if (index > -1) { + var additionalBlockHashes = [key].concat(searchChain.slice(0, index)); + var additionalBlocks = this._getBlocksFromHashes(additionalBlockHashes); + blocksToSend.concat(additionalBlocks); + blocksToSend.reverse(); + break; + + } + } + } + + for(var j = 0; j < blocksToSend.length; j++) { + var block = blocksToSend[j]; + self._broadcast(self._subscriptions.block, 'block/block', block); + } + + self._setTip(blocksToSend[j-1]); + +}; + +BlockService.prototype._getBlocksFromHashes = function(hashes) { + + var self = this; + + var blocks = hashes.map(function(hash) { + + var hdr = self._blockHeaderQueue.get(hash); + + if (!hdr) { + log.error('header for hash: ' + hash + ' could not found in our in-memory block header cache.'); + this.node.stop(); + return; + } + + var block = self._blockQueue.get(hdr); + if (!block) { + log.error('block: ' + hash + ' was not found in our in-memory block cache.'); + this.node.stop(); + return; + } + + return block; + + }); + + return blocks; + +}; + +BlockService.prototype._handleReorg = function(block) { + + this._reorging = true; + log.warn('Chain reorganization detected! Our current block tip is: ' + + this.tip.hash + ' the current block: ' + block.hash + '.'); + + var commonAncestor = this._findCommonAncestor(block); + + if (!commonAncestor) { + log.error('A common ancestor block between hash: ' + this.tip.hash + ' (our current tip) and: ' + + block.hash + ' (the forked block) could not be found. Bitcore-node must exit.'); + this.node.stop(); + return; + } + + log.info('A common ancestor block was found to at hash: ' + commonAncestor + '.'); + this._setTip(block); + this._broadcast(this.subscriptions.reorg, 'block/reorg', [block, commonAncestor]); + this._reorging = false; + +}; + +BlockService.prototype._findCommonAncestor = function(block) { + + assert(this._chainTips.length > 1, + 'chain tips collection should have at least 2 chains in order to find a common ancestor.'); + + var oldChain = this._chainTips.get(this.tip.hash); + var newChain = this._chainTips.get(block.hash); + + // if there is more than one chain tip collection that contains this blocks, how do we know which one if the real chain? + // the blocks to come will decide for sure. + if (!newChain) { + newChain = this._findLongestChainForHash(block.hash); + } + + for(var i = 0; i < oldChain.length; i++) { + var commonIndex = newChain.indexOf(oldChain[i]); + if (commonIndex > -1) { + return oldChain[i]; + } + } + +}; + +BlockService.prototype._setTip = function(block) { + this.tip.height = block.height; + this.tip.hash = block.hash; + this._db.setServiceTip('block', this.tip); }; BlockService.prototype._determineBlockState = function(block) { @@ -503,96 +601,6 @@ BlockService.prototype._onDbError = function(err) { this.node.stop(); }; - -BlockService.prototype._isGetP2PBlocksLockedOut = function() { - return Date.now() < (this._getP2PBlocksLockoutPeriod + (this._previousdGetP2PBlocksLockTime || 0)); -}; - -BlockService.prototype._getP2PBlocks = function() { - if (!!this._isGetP2PBlocksLockedOut()) { - this._previousdGetP2PBlocksLockTime = Date.now(); - this._p2p.getBlocks({ startHash: startHash }); - } -}; - -BlockService.prototype._getBlockValue = function(hashOrHeight, callback) { - - var self = this; - - var key, valueFn; - - if (hashOrHeight.length < 64) { - key = self.encoding.encodeBlockHeightKey(parseInt(hashOrHeight)); - valueFn = self.encoding.decodeBlockHashValue.bind(self.encoding); - } else { - key = self.encoding.encodeBlockHashKey(hashOrHeight); - valueFn = self.encoding.decodeBlockHeightValue.bind(self.encoding); - } - - self._db.get(key, function(err, buf) { - - if (err) { - return callback(err); - } - callback(null, valueFn(buf)); - - }); - -}; - -BlockService.prototype._isGenesisBlock = function(blockArg, callback) { - - if (blockArg.length === 64) { - - return this._getBlockValue(blockArg, function(err, value) { - - if (err) { - return callback(null, false); - } - - if (value === 0) { - return callback(null, true); - } - - callback(null, false); - - }); - - } - - setImmediate(function() { - - if (blockArg === 0) { - return callback(null, true); - } - callback(null, false); - }); - -}; - -BlockService.prototype._getReorgOperations = function(hash, height) { - - if (!hash || !height) { - return; - } - - var self = this; - - var heightKey = self.encoding.encodeBlockHeightKey(height); - var hashKey = self.encoding.encodeBlockHashKey(hash); - var heightValue = self.encoding.encodeBlockHeightValue(height); - var newHashKey = self.encoding.encodeBlockHashKey(hash + '-REORG'); - var newHashValue = self.encoding.encodeBlockHashValue(hash + '-REORG'); - - return [ - { action: 'del', key: heightKey }, - { action: 'del', key: hashKey }, - { action: 'put', key: newHashKey, value: heightValue }, - { action: 'put', key: heightKey, value: newHashValue } - ]; - -}; - BlockService.prototype._getBlockOperations = function(obj) { var self = this; @@ -622,38 +630,6 @@ BlockService.prototype._getBlockOperations = function(obj) { return operations; }; -BlockService.prototype._handleReorg = function(hash, callback) { - - // 1. log out that we are in a reorg state. - //log.warn('Chain reorganization detected! Our current block tip is: this._tip.hash ' + - - //log.error('A common ancestor block between hash: ' + this.tip.hash + ' (our current tip) and: ' + - // block.hash + ' (the forked block) could not be found.'); - - //this.node.stop(); - - //var self = this; - //self._printTipInfo('Reorg detected!'); - - //self.reorg = true; - //self.emit('reorg'); - - //var reorg = new Reorg(self.node, self); - - //reorg.handleReorg(hash, function(err) { - - // if(err) { - // log.error('Reorg failed! ' + err); - // self.node.stop(); - // } - - // self._printTipInfo('Reorg successful!'); - // self.reorg = false; - // self.cleanupAfterReorg(callback); - - //}); - -}; module.exports = BlockService; diff --git a/test/services/block/index.unit.js b/test/services/block/index.unit.js index 7651a8d6..ab886ebe 100644 --- a/test/services/block/index.unit.js +++ b/test/services/block/index.unit.js @@ -100,6 +100,73 @@ describe('Block Service', function() { }); + it('shoudd merge blocks where there is three-way fork and blocks are received in order.', function() { + + var blocks = ['aa','bb','cc','dd','ee']; + var prevBlocks = ['00','aa','aa','aa','dd']; + + blocks.forEach(function(n, index) { + var block = { header: { prevHash: new Buffer(prevBlocks[index], 'hex') }, hash: n }; + blockService._mergeBlockIntoChainTips(block); + }); + + + expect(blockService._chainTips.length).to.equal(3); + expect(blockService._chainTips.get('ee')).to.deep.equal(['dd', 'aa', '00']); + expect(blockService._chainTips.get('bb')).to.deep.equal(['aa', '00']); + expect(blockService._chainTips.get('cc')).to.deep.equal(['aa', '00']); + }); + + describe('Reorgs', function() { + + it('should find a common ancestor in the normal case', function() { + + var blocks = ['aa', 'bb', 'cc', 'dd']; + var prevBlocks = ['00', 'aa', 'bb', 'bb']; + + blocks.forEach(function(n, index) { + var block = { header: { prevHash: new Buffer(prevBlocks[index], 'hex') }, hash: n }; + blockService._mergeBlockIntoChainTips(block); + }); + + blockService.tip = { hash: 'cc', height: 3 }; + var commonAncestor = blockService._findCommonAncestor({ hash: 'dd' }); + expect(commonAncestor).to.equal('bb'); + + }); + + it('should find a common ancestor in the case where more than one block is built on an alternative chain before reorg is discovered', function() { + + // even though 'ee' is the tip on the alt chain, 'dd' was the last block to come in that was not an orphan block. So 'dd' was the first to allow + // reorg validation + var blocks = ['aa', 'bb', 'cc', 'ee', 'dd']; + var prevBlocks = ['00', 'aa', 'bb', 'dd', 'bb']; + + blocks.forEach(function(n, index) { + var block = { header: { prevHash: new Buffer(prevBlocks[index], 'hex') }, hash: n }; + blockService._mergeBlockIntoChainTips(block); + }); + + blockService.tip = { hash: 'cc', height: 3 }; + var commonAncestor = blockService._findCommonAncestor({ hash: 'dd' }); + expect(commonAncestor).to.equal('bb'); + + }); + + }); + + describe('Send all unsent blocks from main chain', function() { + var blocks = ['ee','aa','bb','dd','cc']; + var prevBlocks = ['dd','00','aa','cc','bb']; + + blocks.forEach(function(n, index) { + var block = { header: { prevHash: new Buffer(prevBlocks[index], 'hex') }, hash: n }; + blockService._mergeBlockIntoChainTips(block); + }); + + + + }); });