diff --git a/lib/services/block/index.js b/lib/services/block/index.js index 240d2eb0..2a90600a 100644 --- a/lib/services/block/index.js +++ b/lib/services/block/index.js @@ -7,8 +7,6 @@ var Encoding = require('./encoding'); var index = require('../../'); var log = index.log; var BufferUtil = bitcore.util.buffer; -var Reorg = require('./reorg'); -var async = require('async'); var LRU = require('lru-cache'); var utils = require('../../utils'); var _ = require('lodash'); @@ -29,7 +27,7 @@ var BlockService = function(options) { return n.length * (1 * 1024 * 1024); // 50 MB of blocks } }); // header -> block - this._orphanedBlockList = []; // this is a list of block hashes for which one or more ancestor blocks has not yet arrived + this._chainTips = LRU(50); // chain tip -> [ tip-1 hash, tip-2 hash, tip-N hash] }; inherits(BlockService, BaseService); @@ -194,8 +192,8 @@ BlockService.prototype.getBlocks = function(startHash, endHash) { } if (lockedOut) { - return false; log.debug('Block Service: getBlocks called, but service is still in a lock out period.'); + return false; } }; @@ -265,6 +263,69 @@ BlockService.prototype._blockAlreadyProcessed = function(block) { return this._blockHeaderQueue.get(block.hash); }; +BlockService.prototype._mergeBlockIntoChainTips = function(block) { + + function getPrevHashChain(keys, hash) { + + for(var i = 0; i < keys.length; i++) { +console.log('here'); + + var key = keys[i]; + + var searchChain = this._chainTips.get(key); + console.log(searchChain); + + var chainIndex = searchChain.indexOf(hash); + + if (chainIndex > -1) { + return searchChain.slice(chainIndex); + } + } + + } + + + var prevHash = utils.reverseBufferToString(block.header.prevHash); + var hasChildren = false; + + var chain = this._chainTips.get(prevHash); + if (chain) { + chain.unshift(prevHash); + } + + var keys = this._chainTips.keys(); + + // looking for chains where this block is an ancestor of the tip of the chain + for(var i = 0; i < keys.length; i++) { + + var key = keys[i]; + var searchChain = this._chainTips.get(key); + + var chainIndex = searchChain.indexOf(block.hash); + + if (chainIndex > -1) { + hasChildren = true; + var newChain = searchChain.concat(chain || getPrevHashChain(keys, prevHash)); + this._chainTips.set(key, newChain); + } + + } + + if (chain && !hasChildren) { + + this._chainTips.set(block.hash, chain); + + } + + this._chainTips.del(prevHash); + + // if we have don't have any parents or children in chainTips, then create a new chain with this block + if (!hasChildren && !chain) { + this._chainTips.set(block.hash, getPrevHashChain(keys, prevHash)); + } + +}; + BlockService.prototype._onBlock = function(block) { // 1. have we already seen this block? @@ -278,13 +339,16 @@ BlockService.prototype._onBlock = function(block) { // 3. store the block for safe keeping this._cacheBlock(block); - // 4. determine block state -- see the function for details about block state - var blockSteta = this._determineBlockState(block); + // 4. merge this block into the set of chain tips + this._mergeBlockIntoChainTips(block); - // 5. react to state of block + // 5. determine block state, reorg, orphaned, normal + var blockState = this._determineBlockState(block); + + // 6. react to state of block switch (blockState) { case 'orphaned': - this._handleOrphanedBlock(block); + this._queueOrphanedBlock(block); break; case 'reorg': this.emit('reorg', block); @@ -292,11 +356,15 @@ BlockService.prototype._onBlock = function(block) { 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. break; } }; +BlockService.prototype._setTip = function() { +}; + BlockService.prototype._determineBlockState = function(block) { /* @@ -328,7 +396,7 @@ BlockService.prototype._determineBlockState = function(block) { */ - if (this._hasBlockArrivedOutOfOrder(block)) { + if (this._isOrphanBlock(block)) { return 'orphaned'; } @@ -340,8 +408,10 @@ BlockService.prototype._determineBlockState = function(block) { }; -BlockService.prototype._hasBlockArrivedOutOfOrder = function(block) { +BlockService.prototype._isOrphanBlock = function(block) { + // all blocks that we've seen before should be in the blockHeaderQueue, if this block's prev + // hash isn't, then we definitely have an orphan block. var prevHash = utils.reverseBufferToString(block.header.prevHash); return !this._blockHeaderQueue.get(prevHash); @@ -349,15 +419,13 @@ BlockService.prototype._hasBlockArrivedOutOfOrder = function(block) { BlockService.prototype._isChainReorganizing = function(block) { - var prevHash = utils.reverseBufferToString(block.header.prevHash); - return prevHash !== this.tip.hash + if (!this._isOrphanBlock()) { + var prevHash = utils.reverseBufferToString(block.header.prevHash); + return prevHash !== this.tip.hash; + } -}; + return false; - -BlockService.prototypee._findCommonAncestor = function(tipHash, forkedHash) { - // find the common ancestor between these 2 hashes - // most likely, we will already have the block headers }; BlockService.prototype._broadcast = function(subscribers, name, entity) { @@ -369,8 +437,14 @@ BlockService.prototype._broadcast = function(subscribers, name, entity) { BlockService.prototype._cacheBlock = function(block) { log.debug('Setting block: ' + block.hash + ' in the block cache.'); + + // 1. set the block queue, which holds full blocks in memory this._blockQueue.set(block.hash, block); + // 2. set the block header queue, which holds hash -> header -and- height -> headeer in memory + this._blockHeader.set(block.hash, block.header.toObject()); // we don't know about the height yet + + // 3. store the block in the database var operations = this._getBlockOperations(block); this._db.batch(operations, function(err) { @@ -439,13 +513,13 @@ BlockService.prototype._onDbError = function(err) { BlockService.prototype._isGetP2PBlocksLockedOut = function() { - return Date.now() < (self._getP2PBlocksLockoutPeriod + (self._previousdGetP2PBlocksLockTime || 0)); + return Date.now() < (this._getP2PBlocksLockoutPeriod + (this._previousdGetP2PBlocksLockTime || 0)); }; -BlockService.prototype._getP2PBlocks = function(block) { +BlockService.prototype._getP2PBlocks = function() { if (!!this._isGetP2PBlocksLockedOut()) { this._previousdGetP2PBlocksLockTime = Date.now(); - self._p2p.getBlocks({ startHash: startHash }); + this._p2p.getBlocks({ startHash: startHash }); } }; @@ -557,31 +631,35 @@ BlockService.prototype._getBlockOperations = function(obj) { }; BlockService.prototype._handleReorg = function(hash, callback) { - 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(); + // 1. log out that we are in a reorg state. + //log.warn('Chain reorganization detected! Our current block tip is: this._tip.hash ' + - var self = this; - self._printTipInfo('Reorg detected!'); + //log.error('A common ancestor block between hash: ' + this.tip.hash + ' (our current tip) and: ' + + // block.hash + ' (the forked block) could not be found.'); - self.reorg = true; - self.emit('reorg'); + //this.node.stop(); - var reorg = new Reorg(self.node, self); + //var self = this; + //self._printTipInfo('Reorg detected!'); - reorg.handleReorg(hash, function(err) { + //self.reorg = true; + //self.emit('reorg'); - if(err) { - log.error('Reorg failed! ' + err); - self.node.stop(); - } + //var reorg = new Reorg(self.node, self); - self._printTipInfo('Reorg successful!'); - self.reorg = false; - self.cleanupAfterReorg(callback); + //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); + + //}); }; diff --git a/test/services/block/index.unit.js b/test/services/block/index.unit.js new file mode 100644 index 00000000..5b140f4a --- /dev/null +++ b/test/services/block/index.unit.js @@ -0,0 +1,72 @@ +'use strict'; + +var expect = require('chai').expect; +var BlockService = require('../../../lib/services/block'); +var LRU = require('lru-cache'); + +describe('Block Service', function() { + + var blockService; + + beforeEach(function() { + blockService = new BlockService({ node: { services: []}}); + blockService._chainTips = LRU(50); + }); + + + describe('Chain Tips', function() { + it('should merge blocks into chain tips using active chain blocks only' , function() { + + var blocks = ['aa','bb','cc','dd','ee']; + + blocks.forEach(function(n, index) { + + var buf = new Buffer('00', 'hex'); + if (index) { + buf = new Buffer(blocks[index-1], 'hex'); + } + + var block = { header: { prevHash: buf }, hash: n }; + blockService._mergeBlockIntoChainTips(block); + }); + + expect(blockService._chainTips.length).to.equal(1); + expect(blockService._chainTips.get('ee')).to.deep.equal(['dd', 'cc', 'bb', 'aa', '00']); + }); + + it('should merge blocks into chain tips using out of order blocks' , 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); + }); + + + expect(blockService._chainTips.length).to.equal(1); + expect(blockService._chainTips.get('ee')).to.deep.equal(['dd', 'cc', 'bb', 'aa', '00']); + }); + + it('should merge chain tips where there is a fork (a parent block has more than one child)' , function() { + + var blocks = ['aa','bb','cc','dd','ee']; + var prevBlocks = ['00','aa','aa','cc','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(2); + expect(blockService._chainTips.get('ee')).to.deep.equal(['dd', 'cc', 'aa', '00']); + }); + + + }); + +}); + +