From 14c54a2fbfc2769d36438d47b6d1963448efa23d Mon Sep 17 00:00:00 2001 From: Chris Kleeschulte Date: Wed, 5 Jul 2017 19:12:25 -0400 Subject: [PATCH] wip --- lib/services/block/index.js | 932 ++++++++++++++++-------------- lib/services/db/index.js | 47 +- lib/services/fee/index.js | 3 - test/services/block/index.unit.js | 722 +++++++++++++++-------- 4 files changed, 1042 insertions(+), 662 deletions(-) diff --git a/lib/services/block/index.js b/lib/services/block/index.js index 31b96111..cc722771 100644 --- a/lib/services/block/index.js +++ b/lib/services/block/index.js @@ -12,16 +12,24 @@ var assert = require('assert'); var BN = require('bn.js'); var consensus = require('bcoin').consensus; var constants = require('../../constants'); +var async = require('async'); var BlockService = function(options) { BaseService.call(this, options); this._tip = null; + this._headerTip = null; this._p2p = this.node.services.p2p; this._db = this.node.services.db; this._subscriptions = {}; this._subscriptions.block = []; this._subscriptions.reorg = []; - this._blockHeaderQueue = LRU(20000); // hash -> header, height -> header + this._heightIndex = []; // tracks block height to block hash + this._blockHeaderQueue = LRU({ + max: 50, + length: function(n) { + return n.length * (1 * 1024 * 1024); // 50 MB of block headers, that should be plenty to hold all the headers + } + }); // hash -> header this._blockQueue = LRU({ max: 50, length: function(n) { @@ -38,6 +46,7 @@ BlockService.dependencies = [ 'p2p', 'db' ]; BlockService.MAX_CHAINWORK = new BN(1).ushln(256); +// --- public prototype functions BlockService.prototype.start = function(callback) { var self = this; @@ -116,28 +125,6 @@ BlockService.prototype._broadcast = function(subscribers, name, entity) { } }; -BlockService.prototype._cacheHeader = function(header) { - - // 1. save to in-memory cache first - this._blockHeaderQueue.set(header.hash, header.toObject()); - - // 2. get operations - return this._getHeaderOperations(header); - - -}; - -BlockService.prototype._cacheHeaders = function(headers) { - - var self = this; - var operations = headers.map(self._cacheHeader.bind(self)); - operations = _.flatten(operations); - - log.debug('Saving: ' + headers.length + ' headers to the database.'); - self._db.batch(operations); - self._syncHeaders(); -}; - BlockService.prototype._cacheBlock = function(block) { log.debug('Setting block: "' + block.hash + '" in the block cache.'); @@ -161,6 +148,36 @@ BlockService.prototype._cacheBlock = function(block) { }; +BlockService.prototype._cacheHeader = function(header) { + + // 1. save to in-memory cache first + this._blockHeaderQueue.set(header.hash, header.toObject()); + + // 2. save to in-memory map block index + this._heightIndex.push(header); + + // 3. get operations + return this._getHeaderOperations(header); + +}; + +BlockService.prototype._cacheHeaders = function(headers) { + + var self = this; + var operations = headers.map(self._cacheHeader.bind(self)); + operations = _.flatten(operations); + + log.debug('Saving: ' + headers.length + ' headers to the database.'); + self._db.batch(operations, function(err) { + if (!err) { + self._setHeaderTip(); + } + }); + + self._sync('headers'); + +}; + BlockService.prototype._checkChain = function(tip) { var _tip = tip; @@ -198,318 +215,6 @@ BlockService.prototype._computeChainwork = function(bits, prev) { }; - -BlockService.prototype._startSyncHeaders = function() { - - var numHeadersNeeded = this._bestHeight - this._tip.height; - if (numHeadersNeeded <= 0) { - return; - } - - log.debug('Gathering: ' + numHeadersNeeded + ' block headers from the peer-to-peer network.'); - - this._p2pHeaderCallsNeeded = numHeadersNeeded % 2000 + 1; // we may make one extra call, but this is ok - this._latestHeaderHash = this._tip.hash || constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName]; - this._syncHeaders(); -}; - - -BlockService.prototype._syncHeaders = function() { - - if (--this._p2pHeaderCallsNeeded > 0) { - this._p2p.getHeaders({ startHash: this._latestHeaderHash }); - return; - } - - log.info('Header sync complete.'); - this._startSyncBlocks(); - -}; - -BlockService.prototype._startSyncBlocks = function() { - - var numHeadersNeeded = this._bestHeight - this._tip.height; - if (numHeadersNeeded <= 0) { - return; - } - - log.info('Gathering: ' + numHeadersNeeded + ' blocks from the peer-to-peer network.'); - - this._p2pBlockCallsNeeded = numHeadersNeeded % 500 + 1; // we may make one extra call, but this is ok - this._latestBlockHash = this._tip.hash || constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName]; - this._syncBlocks(); - -}; - -BlockService.prototype._syncBlocks = function() { - - if (--this._p2pBlockCallsNeeded > 0) { - this._p2p.getBlocks({ startHash: this._latestBlockHash }); - return; - } - - log.info('Block sync complete.'); - - this._startSyncBlocks(); - -}; - -BlockService.prototype._reportBootStatus = function() { - - var blockInfoString = utils.getBlockInfoString(this._tip.height, this._bestHeight); - - log.info('Block Service tip is currently height: ' + this._tip.height + ' hash: ' + - this._tip.hash + ' P2P network best height: ' + this._bestHeight + '. Block Service is: ' + - blockInfoString); - -}; - -BlockService.prototype._startSubscriptions = function() { - - var self = this; - - if (self._subscribed) { - return; - } - - self._subscribed = true; - self.bus = self.node.openBus({remoteAddress: 'localhost'}); - - self.bus.on('p2p/block', self._onBlock.bind(self)); - self.bus.on('p2p/headers', self._onHeaders.bind(self)); - - self.bus.subscribe('p2p/block'); - self.bus.subscribe('p2p/headers'); - -}; - -BlockService.prototype._onHeaders = function(headers) { - - log.debug('New headers received, count: ' + headers.length); - this._cacheHeaders(headers); - this._latestBlockHash = headers[headers.length - 1].hash; - -}; - -BlockService.prototype._updateChainTips = function(block, state) { - - var prevHash = utils.reverseBufferToString(block.header.prevHash); - - // otherwise we are not an orphan, so we could be a reorg or normal - // if this is a normal state, then we are just adding ourselves as the tip on the main chain - if (state === 'normal') { - - var index = this._chainTips.indexOf(prevHash); - assert(index > -1, 'Block state is normal, ' + - 'yet the previous block hash is missing from the chain tips collection.'); - - this._chainTips.push(block.hash); - this._chainTips.splice(index, 1); - return; - - } - - // if this is a reorg state, then a new chain tip will be added to the list - if (state === 'reorg') { - - this._chainTips.push(block.hash); - - } - -}; - -BlockService.prototype._onBlock = function(block) { - - // 1. have we already seen this block? - if (this._blockAlreadyProcessed(block)) { - return; - } - - // 2. log the reception - log.debug('New block received: ' + block.hash); - - // 3. store the block for safe keeping - this._cacheBlock(block); - - // 4. determine block state, reorg, orphaned, normal - var blockState = this._determineBlockState(block); - - // 5. add block hash to chain tips - this._updateChainTips(block); - - // 6. react to state of block - switch (blockState) { - case 'orphaned': - // nothing to do, but wait until ancestor blocks come in - break; - case 'reorg': - this.emit('reorg', block); - break; - default: - // send all unsent blocks now that we have a complete chain - this._sendDelta(); - break; - } -}; - -/* - Since blocks can arrive out of order from our trusted peer, we can't rely on the latest block - being the tip of the main/active chain. We should, instead, take the chain with the most work completed. - We need not concern ourselves whether or not the block is valid, we trust our peer to do this validation. -*/ -BlockService.prototype._selectActiveChain = function() { - - var chainTip; - var mostChainWork = new BN(0); - - var self = this; - - for(var i = 0; i < this._chainTips.length; i++) { - - var work = self._getChainwork(this._chainTips[i]); - - if (work.gt(mostChainWork)) { - mostChainWork = work; - chainTip = this._chainTips[i]; - } - } - - return chainTip; - -}; - - -BlockService.prototype._getChainwork = function(tipHash) { - - var header = this._blockHeaderQueue.get(tipHash); - assert(header, 'expected to find a header in block header queue, but did not find any.'); - - var prevHeader = this._blockHeaderQueue.get(header.prevHash); - assert(header, 'expected to find a previous header in block header queue, but did not find any.'); - - //we persist the chainWork to avoid recalculating it on boot - var chainwork = new BN(new Buffer(header.chainwork || '00', 'hex')); - - if (chainwork.gtn(0)) { - return chainwork; - } - - var prevChainwork = new BN(new Buffer(prevHeader.chainwork || '00', 'hex')); - - chainwork = this._computeChainwork(header.bits, prevChainwork); - - header.chainwork = chainwork.toBuffer().toString('hex'); - this._blockHeaderQueue.set(tipHash, header); - - return chainwork; -}; - -BlockService.prototype._getDelta = function(tip) { - - var blocks = []; - var _tip = tip; - - while (_tip !== this._tip.hash) { - var hdr = this._blockHeaderQueue.get(_tip); - var blk = this._blockQueue.get(_tip); - _tip = hdr.prevHash; - blocks.push(blk); - } - - return blocks; - -}; - -BlockService.prototype._sendDelta = function() { - - // when this function is called, we know, for sure, that we have a complete chain of unsent block(s). - // our task is to send all blocks between active chain's tip and our tip. - var activeChainTip = this._selectActiveChain(); - - // it is essential that the activeChainTip be in the same chain as our current tip - assert(this._checkChain(activeChainTip), 'The chain with the greatest work does not include our current tip.'); - - var blocks = this._getDelta(activeChainTip); - - for(var i = 0; i < blocks.length; i++) { - this._broadcast(this._subscriptions.block, 'block/block', blocks[i]); - } - - this._setTip(blocks[i-1]); - -}; - -BlockService.prototype._getBlocksFromHashes = function(hashes) { - - var self = this; - - var blocks = hashes.map(function(hash) { - - var block = self._blockQueue.get(hash); - - 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.warn('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 (!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) { /* @@ -553,28 +258,120 @@ BlockService.prototype._determineBlockState = function(block) { }; -BlockService.prototype._isOrphanBlock = function(block) { +BlockService.prototype._findCommonAncestor = function(block) { - // if any of our ancestors are missing from the queue, then this is an orphan block - var prevHash = utils.reverseBufferToString(block.header.prevHash); + assert(this._chainTips.length > 1, + 'chain tips collection should have at least 2 chains in order to find a common ancestor.'); - var pastBlockCount = Math.min(this._maxLookback, this._blockHeaderQueue.length); - for(var i = 0; i < pastBlockCount; i++) { - var prevHeader = this._blockHeaderQueue.get(prevHash); - if (!prevHeader) { - return true; + var _oldTip = this._tip.hash; + var _newTip = block.hash; + + assert(_newTip && _oldTip, 'current chain and/or new chain do not exist in our list of chain tips.'); + + for(var i = 0; i < this._maxLookback; i++) { + + var oldHdr = this._blockHeaderQueue.get(_oldTip); + var newHdr = this._blockHeaderQueue.get(_newTip); + + if (!oldHdr || !newHdr) { + return; } - prevHash = prevHeader.prevHash; - } - return false; + _oldTip = oldHdr.prevHash; + _newTip = newHdr.prevHash; + + if (_newTip === _oldTip) { + return _newTip; + } + } }; -BlockService.prototype._isChainReorganizing = function(block) { +BlockService.prototype._getBlockOperations = function(block) { - var prevHash = utils.reverseBufferToString(block.header.prevHash); + var self = this; - return prevHash !== this._tip.hash; + // block or [block] + if (_.isArray(block)) { + var ops = []; + _.forEach(block, function(b) { + ops.push(self._getBlockOperations(b)); + }); + return _.flatten(ops); + } + + var operations = []; + + // block + operations.push({ + type: 'put', + key: self._encoding.encodeBlockKey(block.hash), + value: self._encoding.encodeBlockValue(block) + }); + + return operations; + +}; + +BlockService.prototype._getBlocksFromHashes = function(hashes) { + + var self = this; + + var blocks = hashes.map(function(hash) { + + var block = self._blockQueue.get(hash); + + 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._getChainwork = function(tipHash) { + + var header = this._blockHeaderQueue.get(tipHash); + assert(header, 'expected to find a header in block header queue, but did not find any.'); + + var prevHeader = this._blockHeaderQueue.get(header.prevHash); + assert(header, 'expected to find a previous header in block header queue, but did not find any.'); + + //we persist the chainWork to avoid recalculating it on boot + var chainwork = new BN(new Buffer(header.chainwork || '00', 'hex')); + + if (chainwork.gtn(0)) { + return chainwork; + } + + var prevChainwork = new BN(new Buffer(prevHeader.chainwork || '00', 'hex')); + + chainwork = this._computeChainwork(header.bits, prevChainwork); + + header.chainwork = chainwork.toBuffer().toString('hex'); + this._blockHeaderQueue.set(tipHash, header); + + return chainwork; +}; + +BlockService.prototype._getDelta = function(tip) { + + var blocks = []; + var _tip = tip; + + while (_tip !== this._tip.hash) { + var hdr = this._blockHeaderQueue.get(_tip); + var blk = this._blockQueue.get(_tip); + _tip = hdr.prevHash; + blocks.push(blk); + } + + return blocks; }; @@ -590,61 +387,6 @@ BlockService.prototype._getHeader = function(block) { }; }; -BlockService.prototype._setBlockHeaderQueue = function(header) { - - this._blockHeaderQueue.set(header.height, header); - this._blockHeaderQueue.set(header.hash, header); - -}; - -BlockService.prototype._setListeners = function() { - - var self = this; - - self._p2p.once('bestHeight', function(height) { - - self._bestHeight = height; - - // once we have best height, we know the p2p network is ready to go - self._db.once('tip-block', function(tip) { - - if (tip) { - - self._tip = tip; - - } else { - - self._tip = { - height: 0, - hash: constants.BITCOIN_GENESIS_HASH[self.node.getNetworkName()] - }; - - } - - self._startSubscriptions(); - self._startSyncHeaders(); - - }); - - self._loadTip(); - - }); - - self._db.on('error', self._onDbError.bind(self)); - self.on('reorg', self._handleReorg.bind(self)); - -}; - - -BlockService.prototype._loadTip = function() { - this._db.getServiceTip('block'); -}; - -BlockService.prototype._onDbError = function(err) { - log.error('Block Service: Error: ' + err.message + ' not recovering.'); - this.node.stop(); -}; - BlockService.prototype._getHeaderOperations = function(header) { var self = this; @@ -678,32 +420,374 @@ BlockService.prototype._getHeaderOperations = function(header) { }; -BlockService.prototype._getBlockOperations = function(block) { +BlockService.prototype._handleReorg = function(block) { - var self = this; + this._reorging = true; + log.warn('Chain reorganization detected! Our current block tip is: ' + + this._tip.hash + ' the current block: ' + block.hash + '.'); - // block or [block] - if (_.isArray(block)) { - var ops = []; - _.forEach(block, function(b) { - ops.push(self._getBlockOperations(b)); - }); - return _.flatten(ops); + 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; } - var operations = []; - - // block - operations.push({ - type: 'put', - key: self._encoding.encodeBlockKey(block.hash), - value: self._encoding.encodeBlockValue(block) - }); - - - return operations; + log.warn('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._isChainReorganizing = function(block) { + + var prevHash = utils.reverseBufferToString(block.header.prevHash); + + return prevHash !== this._tip.hash; + +}; + +BlockService.prototype._isOrphanBlock = function(block) { + + // if any of our ancestors are missing from the queue, then this is an orphan block + var prevHash = utils.reverseBufferToString(block.header.prevHash); + + var pastBlockCount = Math.min(this._maxLookback, this._blockHeaderQueue.length); + for(var i = 0; i < pastBlockCount; i++) { + var prevHeader = this._blockHeaderQueue.get(prevHash); + if (!prevHeader) { + return true; + } + prevHash = prevHeader.prevHash; + } + + return false; +}; + +BlockService.prototype._loadHeaders = function(cb) { + + var self = this; + var start = self._encoding.encodeHashKey(new Array(65).join('0')); + var end = Buffer.concat([ utils.getTerminalKey(start.slice(0, 3)), start.slice(3) ]); + var stream = self._db.createReadStream({ gte: start, lt: end }); + + stream.on('end', function() { + cb(); + }); + + stream.on('data', function(data) { + self._blockHeaderQueue.set(self._encoding.decodeHashKey(data.key), self._encoding.decodeHeaderValue(data.value)); + }); + + stream.on('error', function(err) { + cb(err); + }); + +}; + +BlockService.prototype._loadHeights = function(cb) { + var self = this; + var start = self._encoding.encodeHeightKey(0); + var end = self._encoding.encodeHeightKey(0xffffffff); + var stream = self._db.createReadStream({ gte: start, lte: end }); + + stream.on('end', function() { + cb(); + }); + + stream.on('data', function(data) { + // I think this should stream in-order + self._heightIndex.push(self._encoding.decodeHeaderValue(data.value).hash); + }); + + stream.on('error', function(err) { + cb(err); + }); + +}; + +BlockService.prototype._loadTip = function() { + var self = this; + self._db.getServiceTip('block'); + async.parallel([ + self._loadHeaders.bind(self), + self._loadHeights.bind(self) + ], function(err) { + if (err) { + self._onDbError(err); + return; + } + self._setHeaderTip(); + self._startSubscriptions(); + self._startSync('header'); + }); +}; + +BlockService.prototype._onBestHeight = function(height) { + + var self = this; + self._bestHeight = height; + + // once we have best height, we know the p2p network is ready to go + self._db.once('tip-block', self._onTipBlock.bind(self)); + + self._loadTip(); + +}; + +BlockService.prototype._onBlock = function(block) { + + // 1. have we already seen this block? + if (this._blockAlreadyProcessed(block)) { + return; + } + + // 2. log the reception + log.debug('New block received: ' + block.hash); + + // 3. store the block for safe keeping + this._cacheBlock(block); + + // 4. determine block state, reorg, orphaned, normal + var blockState = this._determineBlockState(block); + + // 5. add block hash to chain tips + this._updateChainTips(block); + + // 6. react to state of block + switch (blockState) { + case 'orphaned': + // nothing to do, but wait until ancestor blocks come in + break; + case 'reorg': + this.emit('reorg', block); + break; + default: + // send all unsent blocks now that we have a complete chain + this._sendDelta(); + break; + } +}; + +BlockService.prototype._onDbError = function(err) { + + log.error('Block Service: Error: ' + err.message + ' not recovering.'); + this.node.stop(); + +}; + +BlockService.prototype._onHeaders = function(headers) { + + log.debug('New headers received, count: ' + headers.length); + this._numCompleted += headers.length; + this._cacheHeaders(headers); + this._latestHeaderHash = headers[headers.length - 1].hash; + +}; + + +BlockService.prototype._onTipBlock = function(tip) { + + var self = this; + if (tip) { + + self._tip = tip; + + } else { + + self._tip = { + height: 0, + hash: constants.BITCOIN_GENESIS_HASH[self.node.getNetworkName()] + }; + + } + +}; + +BlockService.prototype._reportBootStatus = function() { + + var blockInfoString = utils.getBlockInfoString(this._tip.height, this._bestHeight); + + log.info('Block Service tip is currently height: ' + this._tip.height + ' hash: ' + + this._tip.hash + ' P2P network best height: ' + this._bestHeight + '. Block Service is: ' + + blockInfoString); + +}; + + +/* + Since blocks can arrive out of order from our trusted peer, we can't rely on the latest block + being the tip of the main/active chain. We should, instead, take the chain with the most work completed. + We need not concern ourselves whether or not the block is valid, we trust our peer to do this validation. +*/ +BlockService.prototype._selectActiveChain = function() { + + var chainTip; + var mostChainWork = new BN(0); + + var self = this; + + for(var i = 0; i < this._chainTips.length; i++) { + + var work = self._getChainwork(this._chainTips[i]); + + if (work.gt(mostChainWork)) { + mostChainWork = work; + chainTip = this._chainTips[i]; + } + } + + return chainTip; + +}; + + +BlockService.prototype._sendDelta = function() { + + // when this function is called, we know, for sure, that we have a complete chain of unsent block(s). + // our task is to send all blocks between active chain's tip and our tip. + var activeChainTip = this._selectActiveChain(); + + // it is essential that the activeChainTip be in the same chain as our current tip + assert(this._checkChain(activeChainTip), 'The chain with the greatest work does not include our current tip.'); + + var blocks = this._getDelta(activeChainTip); + + for(var i = 0; i < blocks.length; i++) { + this._broadcast(this._subscriptions.block, 'block/block', blocks[i]); + } + + this._setTip(blocks[i-1]); + +}; + +BlockService.prototype._setListeners = function() { + + var self = this; + + self._p2p.once('bestHeight', self._onBestHeight.bind(self)); + self._db.on('error', self._onDbError.bind(self)); + self.on('reorg', self._handleReorg.bind(self)); + +}; + +BlockService.prototype._setHeaderTip = function() { + var index = this._heightIndex; + var length = index.length; + this._headerTip = { + hash: index[length - 1] || constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName()], + height: length || 0 + }; +}; + +BlockService.prototype._setTip = function(block) { + this._tip.height = block.height; + this._tip.hash = block.hash; + this._db.setServiceTip('block', this._tip); +}; + +BlockService.prototype._startSync = function(type) { + + var currentHeight = type === 'block' ? this._tip.height : this._headerTip.height; + this._numNeeded = this._bestHeight - currentHeight; + this._numCompleted = currentHeight; + if (this._numNeeded <= 0) { + return; + } + + log.info('Gathering: ' + this._numNeeded + ' ' + type + '(s) from the peer-to-peer network.'); + + var hash = this._tip.hash || constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName()]; + if (type === 'block') { + this._p2pBlockCallsNeeded = Math.ceil(this._numNeeded / 500); + this._latestBlockHash = hash; + this._sync('block'); + } else { + this._p2pHeaderCallsNeeded = Math.ceil(this._numNeeded / 2000); + this._latestHeaderHash = hash; + this._sync('header'); + } + +}; + +BlockService.prototype._startSubscriptions = function() { + + var self = this; + + if (self._subscribed) { + return; + } + + self._subscribed = true; + self._bus = self.node.openBus({remoteAddress: 'localhost'}); + + self._bus.on('p2p/block', self._onBlock.bind(self)); + self._bus.on('p2p/headers', self._onHeaders.bind(self)); + + self._bus.subscribe('p2p/block'); + self._bus.subscribe('p2p/headers'); + +}; + +BlockService.prototype._sync = function(type) { + + var counter, func, obj, name; + if (type === 'block') { + counter = --this._p2pBlockCallsNeeded; + obj = { startHash: this._latestBlockHash }; + func = this._p2p.getBlocks; + name = 'Blocks'; + } else { + counter = --this._p2pHeaderCallsNeeded; + obj = { startHash: this._latestHeaderHash }; + func = this._p2p.getHeaders; + name = 'Headers'; + } + + if (counter > 0) { + + log.info(name + ' download progress: ' + this._numCompleted + '/' + + this._numNeeded + ' (' + (this._numCompleted/this._numNeeded*100).toFixed(2) + '%)'); + func.call(this._p2p, obj); + return; + + } + + if ('header') { + this._startSync('block'); + } + +}; + +BlockService.prototype._updateChainTips = function(block, state) { + + var prevHash = utils.reverseBufferToString(block.header.prevHash); + + // otherwise we are not an orphan, so we could be a reorg or normal + // if this is a normal state, then we are just adding ourselves as the tip on the main chain + if (state === 'normal') { + + var index = this._chainTips.indexOf(prevHash); + assert(index > -1, 'Block state is normal, ' + + 'yet the previous block hash is missing from the chain tips collection.'); + + this._chainTips.push(block.hash); + this._chainTips.splice(index, 1); + return; + + } + + // if this is a reorg state, then a new chain tip will be added to the list + if (state === 'reorg') { + + this._chainTips.push(block.hash); + + } + +}; + module.exports = BlockService; diff --git a/lib/services/db/index.js b/lib/services/db/index.js index b5288b0c..b3a26e37 100644 --- a/lib/services/db/index.js +++ b/lib/services/db/index.js @@ -36,7 +36,7 @@ function DB(options) { this.subscriptions = {}; - this._operationsQueue = []; + this._operationsQueue = 0; } util.inherits(DB, Service); @@ -142,30 +142,65 @@ DB.prototype.get = function(key, options, callback) { }; DB.prototype.put = function(key, value, options, callback) { + var self = this; var cb = callback; var opts = options; if (typeof callback !== 'function') { cb = options; opts = {}; } - if (!this._stopping) { - this._store.put(key, value, opts, cb); + if (!self._stopping) { + + self._operationsQueue++; + self._store.put(key, value, opts, function(err) { + + self._operationsQueue--; + + if (err) { + self.emit('error', err); + return; + } + + if (typeof cb === 'function') { + cb(err); + } + + }); + } else { setImmediate(cb); } }; DB.prototype.batch = function(ops, options, callback) { + var self = this; var cb = callback; var opts = options; if (typeof callback !== 'function') { cb = options; opts = {}; } - if (!this._stopping) { - this._store.batch(ops, opts, cb); - } else if (cb) { + + if (!self._stopping) { + self._operationsQueue += ops.length; + self._store.batch(ops, opts, function(err) { + + self._operationsQueue -= ops.length; + + if (err) { + self.emit('error', err); + return; + } + + if (typeof cb === 'function') { + cb(err); + } + + }); + } else { + setImmediate(cb); + } }; diff --git a/lib/services/fee/index.js b/lib/services/fee/index.js index 9c853732..7cd6a9ea 100644 --- a/lib/services/fee/index.js +++ b/lib/services/fee/index.js @@ -1,10 +1,7 @@ 'use strict'; -var assert = require('assert'); var BaseService = require('../../service'); var inherits = require('util').inherits; -var index = require('../../'); -var log = index.log; var BitcoreRPC = require('bitcoind-rpc'); var FeeService = function(options) { diff --git a/test/services/block/index.unit.js b/test/services/block/index.unit.js index e82a5b54..c593cfbe 100644 --- a/test/services/block/index.unit.js +++ b/test/services/block/index.unit.js @@ -10,6 +10,7 @@ var Block = require('bitcore-lib').Block; var Encoding = require('../../../lib/services/block/encoding'); var EventEmitter = require('events').EventEmitter; var LRU = require('lru-cache'); +var constants = require('../../../lib/constants'); describe('Block Service', function() { @@ -32,63 +33,156 @@ describe('Block Service', function() { }); - describe('#_updateChainTips', function() { + describe('#_cacheBlock', function() { - it('should set chain tips under normal block arrival conditions, in order arrival' , 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._updateChainTips(block, 'normal'); - }); - - expect(blockService._chainTips.length).to.equal(1); - expect(blockService._chainTips).to.deep.equal(['ee']); + it('should set the block in the block queue and db', function() { + var sandbox = sinon.sandbox.create(); + var spy1 = sandbox.spy(); + var stub1 = sandbox.stub(); + blockService._blockQueue = { set: stub1 }; + var block = {}; + sandbox.stub(blockService, '_getBlockOperations'); + blockService._db = { batch: spy1 }; + blockService._cacheBlock(block); + expect(spy1.calledOnce).to.be.true; + expect(stub1.calledOnce).to.be.true; + sandbox.restore(); }); - it('should not set chain tips if not in normal or reorg state' , function() { - - var block = { header: { prevHash: new Buffer('aa', 'hex') }}; - blockService._updateChainTips(block, 'orphan'); - expect(blockService._chainTips).to.deep.equal(['00']); - - }); - - it('should set chain tips when there is a reorg taking place' , function() { - - var block = { hash: 'ee', header: { prevHash: 'dd' } }; - blockService._updateChainTips(block, 'reorg'); - expect(blockService._chainTips).to.deep.equal(['00', 'ee']); + }); + describe('#_cacheHeader', function() { + it('should cache header', function() { + var toObj = sinon.stub().returns('header'); + var header = { hash: 'aa', toObject: toObj }; + blockService._blockHeaderQueue = LRU(1); + blockService._cacheHeader(header); + expect(toObj.calledOnce); + expect(blockService._blockHeaderQueue.get('aa')).to.equal('header'); }); }); - describe('#_isOrphanBlock', function() { + describe('#_checkChain', function() { + var sandbox; beforeEach(function() { - var prevHash = '00000000'; - for(var i = 0; i < 110; i++) { - var newHash = crypto.randomBytes(4); - blockService._blockHeaderQueue.set(newHash, { prevHash: new Buffer(prevHash, 'hex') }); - prevHash = newHash; - } + sandbox = sinon.sandbox.create(); }); - it('should detect an orphaned block', function() { - var block = { hash: 'ee', header: { prevHash: new Buffer('aa', 'hex') }}; - expect(blockService._isOrphanBlock(block)).to.be.true; + after(function() { + sandbox.restore(); }); - it('should not detect an orphaned block', function() { - var block = { hash: 'new', header: { prevHash: '00' }}; - expect(blockService._isOrphanBlock(block)).to.be.true; + it('should check that blocks between the active chain tip and the block service tip are in the same chain and the chain is complete.', function() { + + blockService._tip = { hash: 'aa' }; + blockService._blockHeaderQueue = LRU(5); + blockService._blockQueue = LRU(5); + blockService._blockHeaderQueue.set('cc', { prevHash: 'bb' }); + blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); + blockService._blockQueue.set('bb', 'block cc'); + blockService._blockQueue.set('cc', 'block bb'); + var result = blockService._checkChain('cc'); + expect(result).to.be.true; + blockService._blockQueue.reset(); + blockService._blockHeaderQueue.reset(); + }); + + it('should check that blocks between the active chain tip and the block service tip are in a different chain.', function() { + + blockService._tip = { hash: 'aa' }; + blockService._blockHeaderQueue = LRU(5); + blockService._blockQueue = LRU(5); + blockService._blockHeaderQueue.set('cc', { prevHash: 'xx' }); + blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); + blockService._blockQueue.set('bb', 'block cc'); + blockService._blockQueue.set('cc', 'block bb'); + var result = blockService._checkChain('cc'); + expect(result).to.be.false; + }); + }); + + describe('#_computeChainwork', function() { + + it('should calculate chain work correctly', function() { + var expected = new BN(new Buffer('000000000000000000000000000000000000000000677c7b8122f9902c79f4e0', 'hex')); + var prev = new BN(new Buffer('000000000000000000000000000000000000000000677bd68118a98f8779ea90', 'hex')); + + var actual = blockService._computeChainwork(0x18018d30, prev); + assert(actual.eq(expected), 'not equal: actual: ' + actual + ' expected: ' + expected); + }); + + }); + + describe('#_determineBlockState', function() { + + it('should determine the block in a normal state', function() { + var sandbox = sinon.sandbox.create(); + var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(false); + var stub2 = sandbox.stub(blockService, '_isOrphanBlock').returns(false); + expect(blockService._determineBlockState({})).to.equal('normal'); + sandbox.restore(); + }); + + it('should determine the block in a orphan state', function() { + var sandbox = sinon.sandbox.create(); + var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(false); + var stub2 = sandbox.stub(blockService, '_isOrphanBlock').returns(true); + expect(blockService._determineBlockState({})).to.equal('orphaned'); + sandbox.restore(); + }); + + it('should determine the block in a reorg state', function() { + var sandbox = sinon.sandbox.create(); + var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(true); + var stub2 = sandbox.stub(blockService, '_isOrphanBlock').returns(false); + expect(blockService._determineBlockState({})).to.equal('reorg'); + sandbox.restore(); + }); + + }); + + describe('#_findCommonAncestor', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + after(function() { + sandbox.restore(); + }); + + it('should find the common ancestor between the current chain and the new chain', function() { + var block = { hash: 'cc' }; + blockService._tip = { hash: 'bb' } + blockService._blockHeaderQueue = new LRU(5); + blockService._blockHeaderQueue.set('aa', { prevHash: '00' }); + blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); + blockService._blockHeaderQueue.set('cc', { prevHash: 'aa' }); + blockService._chainTips = [ 'cc', 'bb' ]; + var commonAncestor = blockService._findCommonAncestor(block); + expect(commonAncestor).to.equal('aa'); + }); + + }); + + describe('#_getBlockOperations', function() { + + it('should get block operations when given one block', function() { + var block = new Block(new Buffer('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex')); + var ops = blockService._getBlockOperations(block); + + expect(ops[0]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) }); + + }); + + it('should get block operations when given more than one block', function() { + var block = new Block(new Buffer('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex')); + var ops = blockService._getBlockOperations([block, block]); + + expect(ops[0]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) }); + expect(ops[1]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) }); + }); }); @@ -114,52 +208,31 @@ describe('Block Service', function() { }); }); + describe('#_getDelta', function() { - describe('#_computeChainwork', function() { - - it('should calculate chain work correctly', function() { - var expected = new BN(new Buffer('000000000000000000000000000000000000000000677c7b8122f9902c79f4e0', 'hex')); - var prev = new BN(new Buffer('000000000000000000000000000000000000000000677bd68118a98f8779ea90', 'hex')); - - var actual = blockService._computeChainwork(0x18018d30, prev); - assert(actual.eq(expected), 'not equal: actual: ' + actual + ' expected: ' + expected); + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); }); - }); - - describe('#_selectActiveChain', function() { - - it('should select active chain based on most chain work', function() { - blockService._blockHeaderQueue.set('cc', { prevHash: '00', bits: 0x18018d30 }); - blockService._blockHeaderQueue.set('bb', { prevHash: 'aa', bits: 0x18018d30 }); - blockService._blockHeaderQueue.set('aa', { chainwork: '000000000000000000000000000000000000000000677bd68118a98f8779ea90'}); - blockService._blockHeaderQueue.set('00', { chainwork: '000000000000000000000000000000000000000000677bd68118a98f8779ea8f'}); - blockService._chainTips.push('bb'); - blockService._chainTips.push('cc'); - - var expected = 'bb'; - var actual = blockService._selectActiveChain(); - expect(actual).to.equal(expected); - }); - - }); - - describe('#_cacheBlock', function() { - - it('should set the block in the block queue and db', function() { - var sandbox = sinon.sandbox.create(); - var spy1 = sandbox.spy(); - var stub1 = sandbox.stub(); - blockService._blockQueue = { set: stub1 }; - var block = {}; - sandbox.stub(blockService, '_getBlockOperations'); - blockService._db = { batch: spy1 }; - blockService._cacheBlock(block); - expect(spy1.calledOnce).to.be.true; - expect(stub1.calledOnce).to.be.true; + after(function() { sandbox.restore(); }); + it('should get all unsent blocks for the active chain', function() { + + var expected = [ 'block bb', 'block cc' ]; + blockService._tip = { hash: 'aa' }; + blockService._blockHeaderQueue = LRU(5); + blockService._blockQueue = LRU(5); + blockService._blockHeaderQueue.set('cc', { prevHash: 'bb' }); + blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); + blockService._blockQueue.set('bb', 'block cc'); + blockService._blockQueue.set('aa', 'block 00'); + blockService._blockQueue.set('cc', 'block bb'); + var actual = blockService._getDelta('cc'); + expect(actual).to.deep.equal(expected); + }); }); describe('#_isChainReorganizing', function() { @@ -178,23 +251,77 @@ describe('Block Service', function() { }); - describe('#_getBlockOperations', function() { - - it('should get block operations when given one block', function() { - var block = new Block(new Buffer('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex')); - var ops = blockService._getBlockOperations(block); - - expect(ops[0]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) }); + describe('#_isOrphanBlock', function() { + beforeEach(function() { + var prevHash = '00000000'; + for(var i = 0; i < 110; i++) { + var newHash = crypto.randomBytes(4); + blockService._blockHeaderQueue.set(newHash, { prevHash: new Buffer(prevHash, 'hex') }); + prevHash = newHash; + } }); - it('should get block operations when given more than one block', function() { - var block = new Block(new Buffer('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex')); - var ops = blockService._getBlockOperations([block, block]); + it('should detect an orphaned block', function() { + var block = { hash: 'ee', header: { prevHash: new Buffer('aa', 'hex') }}; + expect(blockService._isOrphanBlock(block)).to.be.true; + }); - expect(ops[0]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) }); - expect(ops[1]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) }); + it('should not detect an orphaned block', function() { + var block = { hash: 'new', header: { prevHash: '00' }}; + expect(blockService._isOrphanBlock(block)).to.be.true; + }); + }); + + describe('#_loadTip', function() { + + var tip = { hash: 'aa', height: 1 }; + var sandbox; + var testEmitter = new EventEmitter(); + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + blockService._db = { + + getServiceTip: function(name) { + testEmitter.emit('tip-' + name, tip); + } + + }; + }); + + after(function() { + sandbox.restore(); + }); + + it('should load the tip from the db service', function() { + testEmitter.on('tip-block', function(_tip) { + expect(_tip).to.deep.equal(tip); + }); + blockService._loadTip(); + }); + }); + + describe('#_onBestHeight', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + after(function() { + sandbox.restore(); + }); + + it('should set best height', function() { + var tips = sandbox.stub(); + var loadTip = sandbox.stub(blockService, '_loadTip'); + blockService._db = { once: tips }; + blockService._onBestHeight(123); + expect(blockService._bestHeight).to.equal(123); + expect(tips.calledTwice).to.be.true; + expect(loadTip.calledOnce).to.be.true; }); }); @@ -263,34 +390,256 @@ describe('Block Service', function() { }); }); - describe('#_determineBlockState', function() { + describe('#_onDbError', function() { + it('should handle db errors', function() { + var stop = sinon.stub(); + blockService.node = { stop: stop }; + expect(stop.calledOnce); + }); + }); - it('should determine the block in a normal state', function() { - var sandbox = sinon.sandbox.create(); - var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(false); - var stub2 = sandbox.stub(blockService, '_isOrphanBlock').returns(false); - expect(blockService._determineBlockState({})).to.equal('normal'); + describe('#_onHeaders', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + after(function() { sandbox.restore(); }); - it('should determine the block in a orphan state', function() { - var sandbox = sinon.sandbox.create(); - var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(false); - var stub2 = sandbox.stub(blockService, '_isOrphanBlock').returns(true); - expect(blockService._determineBlockState({})).to.equal('orphaned'); - sandbox.restore(); - }); - - it('should determine the block in a reorg state', function() { - var sandbox = sinon.sandbox.create(); - var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(true); - var stub2 = sandbox.stub(blockService, '_isOrphanBlock').returns(false); - expect(blockService._determineBlockState({})).to.equal('reorg'); - sandbox.restore(); + it('should handle header delivery', function() { + var headers = [ { hash: '00' }, { hash: 'aa' } ]; + var cache = sandbox.stub(blockService, '_cacheHeaders'); + blockService._onHeaders(headers); + expect(cache.calledOnce).to.be.true; + expect(blockService._latestHeaderHash).to.equal('aa'); }); }); + describe('#_onTipHeader', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + after(function() { + sandbox.restore(); + }); + + it('should set initial tip after receiving from db', function() { + var startSubs = sandbox.stub(blockService, '_startSubscriptions'); + var startSync = sandbox.stub(blockService, '_startSync'); + var tip = { height: 100, hash: 'aa' }; + blockService._onTipHeader(tip); + expect(blockService._headerTip).to.deep.equal(tip); + expect(startSubs.calledOnce).to.be.true; + expect(startSync.calledOnce).to.be.true; + }); + + it('should set initial tip after not receiving from db', function() { + var startSubs = sandbox.stub(blockService, '_startSubscriptions'); + var startSync = sandbox.stub(blockService, '_startSync'); + var tip = null; + blockService.node = { getNetworkName: function() { return 'regtest'; } }; + blockService._onTipHeader(tip); + expect(blockService._headerTip).to.deep.equal({ height: 0, hash: constants.BITCOIN_GENESIS_HASH['regtest']}); + expect(startSubs.calledOnce).to.be.true; + expect(startSync.calledOnce).to.be.true; + }); + + }); + + describe('#_onTipBlock', function() { + + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + after(function() { + sandbox.restore(); + }); + + it('should set initial tip after receiving from db', function() { + var tip = { height: 100, hash: 'aa' }; + blockService._onTipBlock(tip); + expect(blockService._tip).to.deep.equal(tip); + }); + + it('should set initial tip after not receiving from db', function() { + var tip = null; + blockService.node = { getNetworkName: function() { return 'regtest'; } }; + blockService._onTipBlock(tip); + expect(blockService._tip).to.deep.equal({ height: 0, hash: constants.BITCOIN_GENESIS_HASH['regtest']}); + }); + + }); + + describe('#_reportBootStatus', function() { + it('should report info on boot', function() { + blockService._tip = { height: 100, hash: 'aa' }; + blockService._reportBootStatus(); + }); + }); + + describe('#_selectActiveChain', function() { + + it('should select active chain based on most chain work', function() { + blockService._blockHeaderQueue.set('cc', { prevHash: '00', bits: 0x18018d30 }); + blockService._blockHeaderQueue.set('bb', { prevHash: 'aa', bits: 0x18018d30 }); + blockService._blockHeaderQueue.set('aa', { chainwork: '000000000000000000000000000000000000000000677bd68118a98f8779ea90'}); + blockService._blockHeaderQueue.set('00', { chainwork: '000000000000000000000000000000000000000000677bd68118a98f8779ea8f'}); + blockService._chainTips.push('bb'); + blockService._chainTips.push('cc'); + + var expected = 'bb'; + var actual = blockService._selectActiveChain(); + expect(actual).to.equal(expected); + }); + + }); + + describe('#_sendDelta', function() { + + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + after(function() { + sandbox.restore(); + }); + + it('should send all unsent blocks for the active chain', function() { + var activeChain = sandbox.stub(blockService, '_selectActiveChain').returns('aa'); + var checkChain = sandbox.stub(blockService, '_checkChain').returns(true); + var getDelta = sandbox.stub(blockService, '_getDelta').returns(['aa', '00']); + var broadcast = sandbox.stub(blockService, '_broadcast'); + var setTip = sandbox.stub(blockService, '_setTip'); + + blockService._sendDelta(); + expect(activeChain.calledOnce).to.be.true; + expect(checkChain.calledOnce).to.be.true; + expect(getDelta.calledOnce).to.be.true; + expect(broadcast.calledTwice).to.be.true; + expect(setTip.calledOnce).to.be.true; + }); + }); + + describe('#_setListeners', function() { + + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + }); + + after(function() { + sandbox.restore(); + }); + + it('should set listeners for bestHeight, db error and reorg', function() { + var bestHeight = sandbox.stub(); + var dbError = sandbox.stub(); + var on = sandbox.stub(blockService, 'on'); + + blockService._p2p = { once: bestHeight }; + blockService._db = { on: dbError }; + + blockService._setListeners(); + expect(bestHeight.calledOnce).to.be.true; + expect(dbError.calledOnce).to.be.true; + expect(on.calledOnce).to.be.true; + + }); + }); + + describe('#_setTip', function() { + + it('should set the tip if given a block', function() { + var stub = sinon.stub(); + blockService._db = {}; + blockService._db.setServiceTip = stub; + blockService._tip = { height: 99, hash: '00' }; + blockService._setTip({ height: 100, hash: 'aa' }); + expect(stub.calledOnce).to.be.true; + }); + + }); + + describe('#_startSubscriptions', function() { + it('should start the subscriptions if not already subscribed', function() { + var on = sinon.stub(); + var subscribe = sinon.stub(); + var openBus = sinon.stub().returns({ on: on, subscribe: subscribe }); + blockService.node = { openBus: openBus }; + blockService._startSubscriptions(); + expect(blockService._subscribed).to.be.true; + expect(openBus.calledOnce).to.be.true; + expect(on.calledTwice).to.be.true; + expect(subscribe.calledTwice).to.be.true; + }); + }); + + describe('#_startSync', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + blockService.node.getNetworkName = function() { return 'regtest'; }; + blockService._tip = { height: 100 }; + blockService._bestHeight = 201; + }); + + after(function() { + sandbox.restore(); + }); + + it('should start the sync of headers if type set', function() { + blockService._tip = { height: 100 }; + blockService._bestHeight = 200; + var stub = sandbox.stub(blockService, '_sync'); + blockService._startSync('header'); + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith('header')).to.be.true; + expect(blockService._latestHeaderHash).to.equal(constants.BITCOIN_GENESIS_HASH[blockService.node.getNetworkName()]); + expect(blockService._p2pHeaderCallsNeeded).to.equal(1); + }); + + it('should start the sync of blocks if type set', function() { + var stub = sandbox.stub(blockService, '_sync'); + blockService._startSync('block'); + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith('block')).to.be.true; + expect(blockService._latestBlockHash).to.equal(constants.BITCOIN_GENESIS_HASH[blockService.node.getNetworkName()]); + expect(blockService._p2pBlockCallsNeeded).to.equal(1); + }); + }); + + describe('#_sync', function() { + it('should sync blocks', function() { + blockService._p2pBlockCallsNeeded = 2; + var getBlocks = sinon.stub(); + blockService._p2p = { getBlocks: getBlocks }; + blockService._sync('block'); + expect(blockService._p2pBlockCallsNeeded).to.equal(1); + expect(getBlocks.calledOnce).to.be.true; + }); + }); + + describe('#_sync', function() { + it('should sync headers', function() { + blockService._p2pHeaderCallsNeeded = 2; + var getHeaders = sinon.stub(); + blockService._p2p = { getHeaders: getHeaders }; + blockService._sync('header'); + expect(blockService._p2pHeaderCallsNeeded).to.equal(1); + expect(getHeaders.calledOnce).to.be.true; + }); + }); + + describe('#start', function() { var sandbox; @@ -322,128 +671,43 @@ describe('Block Service', function() { }); - describe('#_loadTip', function() { + describe('#_updateChainTips', function() { - var tip = { hash: 'aa', height: 1 }; - var sandbox; - var testEmitter = new EventEmitter(); + it('should set chain tips under normal block arrival conditions, in order arrival' , function() { - beforeEach(function() { - sandbox = sinon.sandbox.create(); + var blocks = ['aa','bb','cc','dd','ee']; - blockService._db = { + blocks.forEach(function(n, index) { - getServiceTip: function(name) { - testEmitter.emit('tip-' + name, tip); + var buf = new Buffer('00', 'hex'); + if (index) { + buf = new Buffer(blocks[index-1], 'hex'); } - }; - }); - - after(function() { - sandbox.restore(); - }); - - it('should load the tip from the db service', function() { - testEmitter.on('tip-block', function(_tip) { - expect(_tip).to.deep.equal(tip); + var block = { header: { prevHash: buf }, hash: n }; + blockService._updateChainTips(block, 'normal'); }); - blockService._loadTip(); + + expect(blockService._chainTips.length).to.equal(1); + expect(blockService._chainTips).to.deep.equal(['ee']); + }); + + it('should not set chain tips if not in normal or reorg state' , function() { + + var block = { header: { prevHash: new Buffer('aa', 'hex') }}; + blockService._updateChainTips(block, 'orphan'); + expect(blockService._chainTips).to.deep.equal(['00']); + + }); + + it('should set chain tips when there is a reorg taking place' , function() { + + var block = { hash: 'ee', header: { prevHash: 'dd' } }; + blockService._updateChainTips(block, 'reorg'); + expect(blockService._chainTips).to.deep.equal(['00', 'ee']); + }); }); - describe('#_sendDelta', function() { - - var sandbox; - beforeEach(function() { - sandbox = sinon.sandbox.create(); - }); - - after(function() { - sandbox.restore(); - }); - - it('should send all unsent blocks for the active chain', function() { - var activeChain = sandbox.stub(blockService, '_selectActiveChain').returns('aa'); - var checkChain = sandbox.stub(blockService, '_checkChain').returns(true); - var getDelta = sandbox.stub(blockService, '_getDelta').returns(['aa', '00']); - var broadcast = sandbox.stub(blockService, '_broadcast'); - var setTip = sandbox.stub(blockService, '_setTip'); - - blockService._sendDelta(); - expect(activeChain.calledOnce).to.be.true; - expect(checkChain.calledOnce).to.be.true; - expect(getDelta.calledOnce).to.be.true; - expect(broadcast.calledTwice).to.be.true; - expect(setTip.calledOnce).to.be.true; - }); - }); - - describe('#_getDelta', function() { - - var sandbox; - beforeEach(function() { - sandbox = sinon.sandbox.create(); - }); - - after(function() { - sandbox.restore(); - }); - - it('should get all unsent blocks for the active chain', function() { - - var expected = [ 'block bb', 'block cc' ]; - blockService._tip = { hash: 'aa' }; - blockService._blockHeaderQueue = LRU(5); - blockService._blockQueue = LRU(5); - blockService._blockHeaderQueue.set('cc', { prevHash: 'bb' }); - blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); - blockService._blockQueue.set('bb', 'block cc'); - blockService._blockQueue.set('aa', 'block 00'); - blockService._blockQueue.set('cc', 'block bb'); - var actual = blockService._getDelta('cc'); - expect(actual).to.deep.equal(expected); - }); - }); - - describe('#_checkChain', function() { - - var sandbox; - beforeEach(function() { - sandbox = sinon.sandbox.create(); - }); - - after(function() { - sandbox.restore(); - }); - - it('should check that blocks between the active chain tip and the block service tip are in the same chain and the chain is complete.', function() { - - blockService._tip = { hash: 'aa' }; - blockService._blockHeaderQueue = LRU(5); - blockService._blockQueue = LRU(5); - blockService._blockHeaderQueue.set('cc', { prevHash: 'bb' }); - blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); - blockService._blockQueue.set('bb', 'block cc'); - blockService._blockQueue.set('cc', 'block bb'); - var result = blockService._checkChain('cc'); - expect(result).to.be.true; - blockService._blockQueue.reset(); - blockService._blockHeaderQueue.reset(); - }); - - it('should check that blocks between the active chain tip and the block service tip are in a different chain.', function() { - - blockService._tip = { hash: 'aa' }; - blockService._blockHeaderQueue = LRU(5); - blockService._blockQueue = LRU(5); - blockService._blockHeaderQueue.set('cc', { prevHash: 'xx' }); - blockService._blockHeaderQueue.set('bb', { prevHash: 'aa' }); - blockService._blockQueue.set('bb', 'block cc'); - blockService._blockQueue.set('cc', 'block bb'); - var result = blockService._checkChain('cc'); - expect(result).to.be.false; - }); - }); });