From 16eef1279c3bf452347d7bdb2aecfb1902350b4c Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Mon, 31 Aug 2015 09:00:00 -0400 Subject: [PATCH] Combine chain with db module. --- index.js | 1 - integration/regtest-node.js | 10 +- lib/chain.js | 247 -------------- lib/modules/address.js | 4 +- lib/modules/db.js | 374 ++++++++++++++++++++- lib/node.js | 411 +++++------------------ lib/scaffold/start.js | 24 +- package.json | 2 +- test/chain.unit.js | 249 -------------- test/modules/address.unit.js | 21 +- test/modules/db.unit.js | 427 ++++++++++++++++++++++-- test/node.unit.js | 613 +++++++++++------------------------ 12 files changed, 1058 insertions(+), 1325 deletions(-) delete mode 100644 lib/chain.js delete mode 100644 test/chain.unit.js diff --git a/index.js b/index.js index 1d07625d..94317300 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,6 @@ module.exports = require('./lib'); module.exports.Node = require('./lib/node'); -module.exports.Chain = require('./lib/chain'); module.exports.Transaction = require('./lib/transaction'); module.exports.Module = require('./lib/module'); module.exports.errors = require('./lib/errors'); diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 398704e9..6edcb693 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -102,7 +102,7 @@ describe('Node Functionality', function() { }); var syncedHandler = function() { - if (node.chain.tip.__height === 150) { + if (node.modules.db.tip.__height === 150) { node.removeListener('synced', syncedHandler); done(); } @@ -178,18 +178,18 @@ describe('Node Functionality', function() { blocksRemoved++; }; - node.chain.on('removeblock', removeBlock); + node.modules.db.on('removeblock', removeBlock); var addBlock = function() { blocksAdded++; if (blocksAdded === 2 && blocksRemoved === 1) { - node.chain.removeListener('addblock', addBlock); - node.chain.removeListener('removeblock', removeBlock); + node.modules.db.removeListener('addblock', addBlock); + node.modules.db.removeListener('removeblock', removeBlock); done(); } }; - node.chain.on('addblock', addBlock); + node.modules.db.on('addblock', addBlock); // We need to add a transaction to the mempool so that the next block will // have a different hash as the hash has been invalidated. diff --git a/lib/chain.js b/lib/chain.js deleted file mode 100644 index 81690a81..00000000 --- a/lib/chain.js +++ /dev/null @@ -1,247 +0,0 @@ -'use strict'; - -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var bitcore = require('bitcore'); -var BN = bitcore.crypto.BN; -var $ = bitcore.util.preconditions; -var Block = bitcore.Block; -var index = require('./index'); -var log = index.log; -var utils = require('./utils'); - -var MAX_STACK_DEPTH = 1000; - -/** - * Will instantiate a new Chain instance - * @param {Object} options - The options for the chain - * @param {Number} options.minBits - The minimum number of bits - * @param {Number} options.maxBits - The maximum number of bits - * @param {BN|Number} options.targetTimespan - The number of milliseconds for difficulty retargeting - * @param {BN|Number} options.targetSpacing - The number of milliseconds between blocks - * @returns {Chain} - * @extends BaseChain - * @constructor - */ -function Chain(opts) { - /* jshint maxstatements: 30 */ - if (!(this instanceof Chain)) { - return new Chain(opts); - } - - var self = this; - if(!opts) { - opts = {}; - } - - this.genesis = opts.genesis; - this.genesisOptions = opts.genesisOptions; - this.genesisWeight = new BN(0); - this.tip = null; - this.overrideTip = opts.overrideTip; - this.cache = { - hashes: {}, // dictionary of hash -> prevHash - chainHashes: {} - }; - this.lastSavedMetadata = null; - this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance - this.blockQueue = []; - this.processingBlockQueue = false; - this.builder = opts.builder || false; - this.ready = false; - - this.on('initialized', function() { - self.initialized = true; - }); - - this.on('initialized', this._onInitialized.bind(this)); - - this.on('ready', function() { - log.debug('Chain is ready'); - self.ready = true; - self.startBuilder(); - }); - - this.minBits = opts.minBits || Chain.DEFAULTS.MIN_BITS; - this.maxBits = opts.maxBits || Chain.DEFAULTS.MAX_BITS; - - this.maxHashes = opts.maxHashes || Chain.DEFAULTS.MAX_HASHES; - - this.targetTimespan = opts.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN; - this.targetSpacing = opts.targetSpacing || Chain.DEFAULTS.TARGET_SPACING; - - this.node = opts.node; - - return this; -} - -util.inherits(Chain, EventEmitter); - -Chain.DEFAULTS = { - MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'), - TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks - TARGET_SPACING: 10 * 60 * 1000, // ten minutes - MAX_BITS: 0x1d00ffff, - MIN_BITS: 0x03000000 -}; - -Chain.prototype._onInitialized = function() { - this.emit('ready'); -}; - -Chain.prototype.start = function(callback) { - this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer); - this.once('initialized', callback); - this.initialize(); -}; - -Chain.prototype.initialize = function() { - var self = this; - - // Does our database already have a tip? - self.node.modules.db.getMetadata(function getMetadataCallback(err, metadata) { - if(err) { - return self.emit('error', err); - } else if(!metadata || !metadata.tip) { - self.tip = self.genesis; - self.tip.__height = 0; - self.tip.__weight = self.genesisWeight; - self.node.modules.db.connectBlock(self.genesis, function(err) { - if(err) { - return self.emit('error', err); - } - - self.emit('addblock', self.genesis); - self.saveMetadata(); - self.emit('initialized'); - }); - } else { - metadata.tip = metadata.tip; - self.node.modules.db.getBlock(metadata.tip, function getBlockCallback(err, tip) { - if(err) { - return self.emit('error', err); - } - - self.tip = tip; - self.tip.__height = metadata.tipHeight; - self.tip.__weight = new BN(metadata.tipWeight, 'hex'); - self.cache = metadata.cache; - self.emit('initialized'); - }); - } - }); -}; - -Chain.prototype.stop = function(callback) { - setImmediate(callback); -}; - -Chain.prototype._validateBlock = function(block, callback) { - // All validation is done by bitcoind - setImmediate(callback); -}; - -Chain.prototype.startBuilder = function() { - // Unused in bitcoind.js -}; - -Chain.prototype.getWeight = function getWeight(blockHash, callback) { - var self = this; - var blockIndex = self.node.modules.bitcoind.getBlockIndex(blockHash); - - setImmediate(function() { - if (blockIndex) { - callback(null, new BN(blockIndex.chainWork, 'hex')); - } else { - return callback(new Error('Weight not found for ' + blockHash)); - } - }); -}; - -/** - * Will get an array of hashes all the way to the genesis block for - * the chain based on "block hash" as the tip. - * - * @param {String} block hash - a block hash - * @param {Function} callback - A function that accepts: Error and Array of hashes - */ -Chain.prototype.getHashes = function getHashes(tipHash, callback) { - var self = this; - - $.checkArgument(utils.isHash(tipHash)); - - var hashes = []; - var depth = 0; - - getHashAndContinue(null, tipHash); - - function getHashAndContinue(err, hash) { - if (err) { - return callback(err); - } - - depth++; - - hashes.unshift(hash); - - if (hash === self.genesis.hash) { - // Stop at the genesis block - self.cache.chainHashes[tipHash] = hashes; - callback(null, hashes); - } else if(self.cache.chainHashes[hash]) { - hashes.shift(); - hashes = self.cache.chainHashes[hash].concat(hashes); - delete self.cache.chainHashes[hash]; - self.cache.chainHashes[tipHash] = hashes; - callback(null, hashes); - } else { - // Continue with the previous hash - // check cache first - var prevHash = self.cache.hashes[hash]; - if(prevHash) { - // Don't let the stack get too deep. Otherwise we will crash. - if(depth >= MAX_STACK_DEPTH) { - depth = 0; - return setImmediate(function() { - getHashAndContinue(null, prevHash); - }); - } else { - return getHashAndContinue(null, prevHash); - } - } else { - // do a db call if we don't have it - self.node.modules.db.getPrevHash(hash, function(err, prevHash) { - if(err) { - return callback(err); - } - - return getHashAndContinue(null, prevHash); - }); - } - } - } - -}; - -Chain.prototype.saveMetadata = function saveMetadata(callback) { - var self = this; - - callback = callback || function() {}; - - if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) { - return callback(); - } - - var metadata = { - tip: self.tip ? self.tip.hash : null, - tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, - tipWeight: self.tip && self.tip.__weight ? self.tip.__weight.toString(16) : '0', - cache: self.cache - }; - - self.lastSavedMetadata = new Date(); - - self.node.modules.db.putMetadata(metadata, callback); -}; - -module.exports = Chain; diff --git a/lib/modules/address.js b/lib/modules/address.js index ba47848f..5da5b577 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -347,7 +347,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback satoshis: Number(value[0]), script: value[1], blockHeight: Number(value[2]), - confirmations: self.node.chain.tip.__height - Number(value[2]) + 1 + confirmations: self.node.modules.db.tip.__height - Number(value[2]) + 1 }; outputs.push(output); @@ -504,7 +504,7 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem var confirmations = 0; if(transaction.__height >= 0) { - confirmations = self.node.chain.tip.__height - transaction.__height; + confirmations = self.node.modules.db.tip.__height - transaction.__height; } txinfos[transaction.hash] = { diff --git a/lib/modules/db.js b/lib/modules/db.js index b5b5ff7f..eb22de74 100644 --- a/lib/modules/db.js +++ b/lib/modules/db.js @@ -7,6 +7,7 @@ var levelup = require('levelup'); var leveldown = require('leveldown'); var mkdirp = require('mkdirp'); var bitcore = require('bitcore'); +var BufferUtil = bitcore.util.buffer; var Networks = bitcore.Networks; var Block = bitcore.Block; var $ = bitcore.util.preconditions; @@ -15,9 +16,12 @@ var errors = index.errors; var log = index.log; var Transaction = require('../transaction'); var Module = require('../module'); +var utils = require('../utils'); + +var MAX_STACK_DEPTH = 1000; /** - * Represents the current state of the bitcoin blockchain transaction data. Other modules + * Represents the current state of the bitcoin blockchain. Other modules * can extend the data that is indexed by implementing a `blockHandler` method. * * @param {Object} options @@ -25,6 +29,8 @@ var Module = require('../module'); * @param {Node} options.node - A reference to the node */ function DB(options) { + /* jshint maxstatements: 20 */ + if (!(this instanceof DB)) { return new DB(options); } @@ -34,11 +40,21 @@ function DB(options) { Module.call(this, options); + this.tip = null; + this.genesis = null; + $.checkState(this.node.network, 'Node is expected to have a "network" property'); this.network = this.node.network; this._setDataPath(); + this.cache = { + hashes: {}, // dictionary of hash -> prevHash + chainHashes: {} + }; + this.lastSavedMetadata = null; + this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance + this.levelupStore = leveldown; if (options.store) { this.levelupStore = options.store; @@ -69,14 +85,65 @@ DB.prototype._setDataPath = function() { }; DB.prototype.start = function(callback) { + var self = this; if (!fs.existsSync(this.dataPath)) { mkdirp.sync(this.dataPath); } + + this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer); this.store = levelup(this.dataPath, { db: this.levelupStore }); this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this)); - this.emit('ready'); - log.info('Bitcoin Database Ready'); - setImmediate(callback); + + this.once('ready', function() { + log.info('Bitcoin Database Ready'); + + // Notify that there is a new tip + self.node.modules.bitcoind.on('tip', function(height) { + if(!self.node.stopping) { + var percentage = self.node.modules.bitcoind.syncPercentage(); + log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); + self.sync(); + } + }); + }); + + // Does our database already have a tip? + self.getMetadata(function(err, metadata) { + if(err) { + return callback(err); + } else if(!metadata || !metadata.tip) { + self.tip = self.genesis; + self.tip.__height = 0; + self.connectBlock(self.genesis, function(err) { + if(err) { + return callback(err); + } + + self.emit('addblock', self.genesis); + self.saveMetadata(); + self.sync(); + self.emit('ready'); + setImmediate(callback); + + }); + } else { + metadata.tip = metadata.tip; + self.getBlock(metadata.tip, function(err, tip) { + if(err) { + return callback(err); + } + + self.tip = tip; + self.tip.__height = metadata.tipHeight; + self.cache = metadata.cache; + self.sync(); + self.emit('ready'); + setImmediate(callback); + + }); + } + }); + }; DB.prototype.stop = function(callback) { @@ -92,6 +159,14 @@ DB.prototype.getInfo = function(callback) { }); }; +/** + * Closes the underlying store database + * @param {Function} callback - A function that accepts: Error + */ +DB.prototype.close = function(callback) { + this.store.close(callback); +}; + DB.prototype.transactionHandler = function(txInfo) { var tx = Transaction().fromBuffer(txInfo.buffer); for (var i = 0; i < this.subscriptions.transaction.length; i++) { @@ -102,14 +177,6 @@ DB.prototype.transactionHandler = function(txInfo) { } }; -/** - * Closes the underlying store database - * @param {Function} callback - A function that accepts: Error - */ -DB.prototype.close = function(callback) { - this.store.close(callback); -}; - DB.prototype.getAPIMethods = function() { var methods = [ ['getBlock', this, this.getBlock, 1], @@ -231,6 +298,26 @@ DB.prototype.putMetadata = function(metadata, callback) { this.store.put('metadata', JSON.stringify(metadata), {}, callback); }; +DB.prototype.saveMetadata = function(callback) { + var self = this; + + callback = callback || function() {}; + + if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) { + return callback(); + } + + var metadata = { + tip: self.tip ? self.tip.hash : null, + tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, + cache: self.cache + }; + + self.lastSavedMetadata = new Date(); + + self.putMetadata(metadata, callback); +}; + /** * Retrieves metadata from the database * @param {Function} callback - A function that accepts: Error and Object @@ -317,4 +404,267 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) { ); }; +/** + * Will get an array of hashes all the way to the genesis block for + * the chain based on "block hash" as the tip. + * + * @param {String} block hash - a block hash + * @param {Function} callback - A function that accepts: Error and Array of hashes + */ +DB.prototype.getHashes = function getHashes(tipHash, callback) { + var self = this; + + $.checkArgument(utils.isHash(tipHash)); + + var hashes = []; + var depth = 0; + + function getHashAndContinue(err, hash) { + /* jshint maxstatements: 20 */ + + if (err) { + return callback(err); + } + + depth++; + + hashes.unshift(hash); + + if (hash === self.genesis.hash) { + // Stop at the genesis block + self.cache.chainHashes[tipHash] = hashes; + callback(null, hashes); + } else if(self.cache.chainHashes[hash]) { + hashes.shift(); + hashes = self.cache.chainHashes[hash].concat(hashes); + delete self.cache.chainHashes[hash]; + self.cache.chainHashes[tipHash] = hashes; + callback(null, hashes); + } else { + // Continue with the previous hash + // check cache first + var prevHash = self.cache.hashes[hash]; + if(prevHash) { + // Don't let the stack get too deep. Otherwise we will crash. + if(depth >= MAX_STACK_DEPTH) { + depth = 0; + return setImmediate(function() { + getHashAndContinue(null, prevHash); + }); + } else { + return getHashAndContinue(null, prevHash); + } + } else { + // do a db call if we don't have it + self.getPrevHash(hash, function(err, prevHash) { + if(err) { + return callback(err); + } + + return getHashAndContinue(null, prevHash); + }); + } + } + } + + getHashAndContinue(null, tipHash); + +}; + +/** + * This function will find the common ancestor between the current chain and a forked block, + * by moving backwards from the forked block until it meets the current chain. + * @param {Block} block - The new tip that forks the current chain. + * @param {Function} done - A callback function that is called when complete. + */ +DB.prototype.findCommonAncestor = function(block, done) { + + var self = this; + + // The current chain of hashes will likely already be available in a cache. + self.getHashes(self.tip.hash, function(err, currentHashes) { + if (err) { + done(err); + } + + // Create a hash map for faster lookups + var currentHashesMap = {}; + var length = currentHashes.length; + for (var i = 0; i < length; i++) { + currentHashesMap[currentHashes[i]] = true; + } + + // TODO: expose prevHash as a string from bitcore + var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); + + // We only need to go back until we meet the main chain for the forked block + // and thus don't need to find the entire chain of hashes. + + while(ancestorHash && !currentHashesMap[ancestorHash]) { + var blockIndex = self.node.modules.bitcoind.getBlockIndex(ancestorHash); + ancestorHash = blockIndex ? blockIndex.prevHash : null; + } + + // Hash map is no-longer needed, quickly let + // scavenging garbage collection know to cleanup + currentHashesMap = null; + + if (!ancestorHash) { + return done(new Error('Unknown common ancestor.')); + } + + done(null, ancestorHash); + + }); +}; + +/** + * This function will attempt to rewind the chain to the common ancestor + * between the current chain and a forked block. + * @param {Block} block - The new tip that forks the current chain. + * @param {Function} done - A callback function that is called when complete. + */ +DB.prototype.syncRewind = function(block, done) { + + var self = this; + + self.findCommonAncestor(block, function(err, ancestorHash) { + if (err) { + return done(err); + } + // Rewind the chain to the common ancestor + async.whilst( + function() { + // Wait until the tip equals the ancestor hash + return self.tip.hash !== ancestorHash; + }, + function(removeDone) { + + var tip = self.tip; + + // TODO: expose prevHash as a string from bitcore + var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex'); + + self.getBlock(prevHash, function(err, previousTip) { + if (err) { + removeDone(err); + } + + // Undo the related indexes for this block + self.disconnectBlock(tip, function(err) { + if (err) { + return removeDone(err); + } + + // Set the new tip + previousTip.__height = self.tip.__height - 1; + self.tip = previousTip; + self.saveMetadata(); + self.emit('removeblock', tip); + removeDone(); + }); + + }); + + }, done + ); + }); +}; + +/** + * This function will synchronize additional indexes for the chain based on + * the current active chain in the bitcoin daemon. In the event that there is + * a reorganization in the daemon, the chain will rewind to the last common + * ancestor and then resume syncing. + */ +DB.prototype.sync = function() { + var self = this; + + if (self.bitcoindSyncing) { + return; + } + + if (!self.tip) { + return; + } + + self.bitcoindSyncing = true; + self.lastSavedMetadataThreshold = 30000; + + var height; + + async.whilst(function() { + height = self.tip.__height; + return height < self.node.modules.bitcoind.height && !self.node.stopping; + }, function(done) { + self.node.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) { + if (err) { + return done(err); + } + + var block = Block.fromBuffer(blockBuffer); + + // TODO: expose prevHash as a string from bitcore + var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); + + if (prevHash === self.tip.hash) { + + // This block appends to the current chain tip and we can + // immediately add it to the chain and create indexes. + + // Populate height + block.__height = self.tip.__height + 1; + + // Update cache.hashes + self.cache.hashes[block.hash] = prevHash; + + // Update cache.chainHashes + self.getHashes(block.hash, function(err, hashes) { + if (err) { + return done(err); + } + // Create indexes + self.connectBlock(block, function(err) { + if (err) { + return done(err); + } + self.tip = block; + log.debug('Saving metadata'); + self.saveMetadata(); + log.debug('Chain added block to main chain'); + self.emit('addblock', block); + setImmediate(done); + }); + }); + + } else { + // This block doesn't progress the current tip, so we'll attempt + // to rewind the chain to the common ancestor of the block and + // then we can resume syncing. + self.syncRewind(block, done); + + } + }); + }, function(err) { + if (err) { + Error.captureStackTrace(err); + return self.node.emit('error', err); + } + + if(self.node.stopping) { + return; + } + + self.bitcoindSyncing = false; + self.lastSavedMetadataThreshold = 0; + + // If bitcoind is completely synced + if (self.node.modules.bitcoind.isSynced()) { + self.node.emit('synced'); + } + + }); + +}; + module.exports = DB; diff --git a/lib/node.js b/lib/node.js index e71cd39c..4c147632 100644 --- a/lib/node.js +++ b/lib/node.js @@ -4,12 +4,8 @@ var util = require('util'); var EventEmitter = require('events').EventEmitter; var async = require('async'); var bitcore = require('bitcore'); -var BufferUtil = bitcore.util.buffer; var Networks = bitcore.Networks; -var _ = bitcore.deps._; var $ = bitcore.util.preconditions; -var Block = bitcore.Block; -var Chain = require('./chain'); var index = require('./'); var log = index.log; var Bus = require('./bus'); @@ -20,9 +16,9 @@ function Node(config) { return new Node(config); } - this.chain = null; - this.network = null; + var self = this; + this.network = null; this.modules = {}; this._unloadedModules = []; @@ -35,45 +31,47 @@ function Node(config) { $.checkState(config.datadir, 'Node config expects "datadir"'); this.datadir = config.datadir; - this._loadConfiguration(config); - this._initialize(); + this._setNetwork(config); + + this.start(function(err) { + if(err) { + return self.emit('error', err); + } + self.emit('ready'); + }); + } util.inherits(Node, EventEmitter); -Node.prototype.openBus = function() { - return new Bus({node: this}); +util.inherits(Node, EventEmitter); + +Node.prototype._setNetwork = function(config) { + if (config.network === 'testnet') { + this.network = Networks.get('testnet'); + } else if (config.network === 'regtest') { + Networks.remove(Networks.testnet); + Networks.add({ + name: 'regtest', + alias: 'regtest', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0xfabfb5da, + port: 18444, + dnsSeeds: [ ] + }); + this.network = Networks.get('regtest'); + } else { + this.network = Networks.defaultNetwork; + } + $.checkState(this.network, 'Unrecognized network'); }; -Node.prototype.addModule = function(service) { - var self = this; - var mod = new service.module({ - node: this - }); - - $.checkState( - mod instanceof BaseModule, - 'Unexpected module instance type for module:' + service.name - ); - - // include in loaded modules - this.modules[service.name] = mod; - - // add API methods - var methodData = mod.getAPIMethods(); - methodData.forEach(function(data) { - var name = data[0]; - var instance = data[1]; - var method = data[2]; - - if (self[name]) { - throw new Error('Existing API method exists:' + name); - } else { - self[name] = function() { - return method.apply(instance, arguments); - }; - } - }); +Node.prototype.openBus = function() { + return new Bus({db: this.modules.db}); }; Node.prototype.getAllAPIMethods = function() { @@ -94,293 +92,9 @@ Node.prototype.getAllPublishEvents = function() { return events; }; -Node.prototype._loadConfiguration = function(config) { - this._loadNetwork(config); - this._loadConsensus(config); -}; - -/** - * This function will find the common ancestor between the current chain and a forked block, - * by moving backwards from the forked block until it meets the current chain. - * @param {Block} block - The new tip that forks the current chain. - * @param {Function} done - A callback function that is called when complete. - */ -Node.prototype._syncBitcoindAncestor = function(block, done) { - - var self = this; - - // The current chain of hashes will likely already be available in a cache. - self.chain.getHashes(self.chain.tip.hash, function(err, currentHashes) { - if (err) { - done(err); - } - - // Create a hash map for faster lookups - var currentHashesMap = {}; - var length = currentHashes.length; - for (var i = 0; i < length; i++) { - currentHashesMap[currentHashes[i]] = true; - } - - // TODO: expose prevHash as a string from bitcore - var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); - - // We only need to go back until we meet the main chain for the forked block - // and thus don't need to find the entire chain of hashes. - - while(ancestorHash && !currentHashesMap[ancestorHash]) { - var blockIndex = self.modules.bitcoind.getBlockIndex(ancestorHash); - ancestorHash = blockIndex ? blockIndex.prevHash : null; - } - - // Hash map is no-longer needed, quickly let - // scavenging garbage collection know to cleanup - currentHashesMap = null; - - if (!ancestorHash) { - return done(new Error('Unknown common ancestor.')); - } - - done(null, ancestorHash); - - }); -}; - -/** - * This function will attempt to rewind the chain to the common ancestor - * between the current chain and a forked block. - * @param {Block} block - The new tip that forks the current chain. - * @param {Function} done - A callback function that is called when complete. - */ -Node.prototype._syncBitcoindRewind = function(block, done) { - - var self = this; - - self._syncBitcoindAncestor(block, function(err, ancestorHash) { - if (err) { - return done(err); - } - // Rewind the chain to the common ancestor - async.whilst( - function() { - // Wait until the tip equals the ancestor hash - return self.chain.tip.hash !== ancestorHash; - }, - function(removeDone) { - - var tip = self.chain.tip; - - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex'); - - self.getBlock(prevHash, function(err, previousTip) { - if (err) { - removeDone(err); - } - - // Undo the related indexes for this block - self.modules.db.disconnectBlock(tip, function(err) { - if (err) { - return removeDone(err); - } - - // Set the new tip - previousTip.__height = self.chain.tip.__height - 1; - self.chain.tip = previousTip; - self.chain.saveMetadata(); - self.chain.emit('removeblock', tip); - removeDone(); - }); - - }); - - }, done - ); - }); -}; - -/** - * This function will synchronize additional indexes for the chain based on - * the current active chain in the bitcoin daemon. In the event that there is - * a reorganization in the daemon, the chain will rewind to the last common - * ancestor and then resume syncing. - */ -Node.prototype._syncBitcoind = function() { - var self = this; - - if (self.bitcoindSyncing) { - return; - } - - if (!self.chain.tip) { - return; - } - - self.bitcoindSyncing = true; - self.chain.lastSavedMetadataThreshold = 30000; - - var height; - - async.whilst(function() { - height = self.chain.tip.__height; - return height < self.modules.bitcoind.height && !self.stopping; - }, function(done) { - self.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) { - if (err) { - return done(err); - } - - var block = Block.fromBuffer(blockBuffer); - - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); - - if (prevHash === self.chain.tip.hash) { - - // This block appends to the current chain tip and we can - // immediately add it to the chain and create indexes. - - // Populate height - block.__height = self.chain.tip.__height + 1; - - // Update chain.cache.hashes - self.chain.cache.hashes[block.hash] = prevHash; - - // Update chain.cache.chainHashes - self.chain.getHashes(block.hash, function(err, hashes) { - if (err) { - return done(err); - } - // Create indexes - self.modules.db.connectBlock(block, function(err) { - if (err) { - return done(err); - } - self.chain.tip = block; - log.debug('Saving metadata'); - self.chain.saveMetadata(); - log.debug('Chain added block to main chain'); - self.chain.emit('addblock', block); - setImmediate(done); - }); - }); - - } else { - // This block doesn't progress the current tip, so we'll attempt - // to rewind the chain to the common ancestor of the block and - // then we can resume syncing. - self._syncBitcoindRewind(block, done); - - } - }); - }, function(err) { - if (err) { - Error.captureStackTrace(err); - return self.emit('error', err); - } - - if(self.stopping) { - return; - } - - self.bitcoindSyncing = false; - self.chain.lastSavedMetadataThreshold = 0; - - // If bitcoind is completely synced - if (self.modules.bitcoind.isSynced()) { - self.emit('synced'); - } - - }); - -}; - -Node.prototype._loadNetwork = function(config) { - if (config.network === 'testnet') { - this.network = Networks.get('testnet'); - } else if (config.network === 'regtest') { - Networks.remove(Networks.testnet); - Networks.add({ - name: 'regtest', - alias: 'regtest', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0xfabfb5da, - port: 18444, - dnsSeeds: [ ] - }); - this.network = Networks.get('regtest'); - } else { - this.network = Networks.get('livenet'); - } - $.checkState(this.network, 'Unrecognized network'); -}; - -Node.prototype._loadConsensus = function(config) { - var options; - if (!config) { - options = {}; - } else { - options = _.clone(config.consensus || {}); - } - options.node = this; - this.chain = new Chain(options); -}; - -Node.prototype._initialize = function() { - var self = this; - - this._initializeChain(); - - this.start(function(err) { - if(err) { - return self.emit('error', err); - } - self.emit('ready'); - }); -}; - -Node.prototype._initializeChain = function() { - - var self = this; - this.chain.on('ready', function() { - log.info('Bitcoin Chain Ready'); - - // Notify that there is a new tip - self.modules.bitcoind.on('tip', function(height) { - if(!self.stopping) { - var percentage = self.modules.bitcoind.syncPercentage(); - log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); - self._syncBitcoind(); - } - }); - - }); - this.chain.on('error', function(err) { - Error.captureStackTrace(err); - self.emit('error', err); - }); -}; - -Node.prototype.getServices = function() { - var services = [ - { - name: 'chain', - dependencies: ['db'] - } - ]; - - services = services.concat(this._unloadedModules); - - return services; -}; - Node.prototype.getServiceOrder = function() { - var services = this.getServices(); + var services = this._unloadedModules; // organize data for sorting var names = []; @@ -418,6 +132,37 @@ Node.prototype.getServiceOrder = function() { return stack; }; +Node.prototype._instantiateModule = function(service) { + var self = this; + var mod = new service.module({ + node: this + }); + + $.checkState( + mod instanceof BaseModule, + 'Unexpected module instance type for module:' + service.name + ); + + // include in loaded modules + this.modules[service.name] = mod; + + // add API methods + var methodData = mod.getAPIMethods(); + methodData.forEach(function(data) { + var name = data[0]; + var instance = data[1]; + var method = data[2]; + + if (self[name]) { + throw new Error('Existing API method exists: ' + name); + } else { + self[name] = function() { + return method.apply(instance, arguments); + }; + } + }); +}; + Node.prototype.start = function(callback) { var self = this; var servicesOrder = this.getServiceOrder(); @@ -426,14 +171,12 @@ Node.prototype.start = function(callback) { servicesOrder, function(service, next) { log.info('Starting ' + service.name); - - if (service.module) { - self.addModule(service); - self.modules[service.name].start(next); - } else { - // TODO: implement bitcoind, chain and db as modules - self[service.name].start(next); + try { + self._instantiateModule(service); + } catch(err) { + return callback(err); } + self.modules[service.name].start(next); }, callback ); @@ -451,11 +194,7 @@ Node.prototype.stop = function(callback) { services, function(service, next) { log.info('Stopping ' + service.name); - if (service.module) { - self.modules[service.name].stop(next); - } else { - self[service.name].stop(next); - } + self.modules[service.name].stop(next); }, callback ); diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 8ce78847..27bda159 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -68,8 +68,8 @@ function start(options) { function logSyncStatus() { log.info( - 'Sync Status: Tip:', node.chain.tip.hash, - 'Height:', node.chain.tip.__height, + 'Sync Status: Tip:', node.modules.db.tip.hash, + 'Height:', node.modules.db.tip.__height, 'Rate:', count/10, 'blocks per second' ); } @@ -184,15 +184,17 @@ function start(options) { log.error(err); }); - node.chain.on('addblock', function(block) { - count++; - // Initialize logging if not already instantiated - if (!interval) { - interval = setInterval(function() { - logSyncStatus(); - count = 0; - }, 10000); - } + node.on('ready', function() { + node.modules.db.on('addblock', function(block) { + count++; + // Initialize logging if not already instantiated + if (!interval) { + interval = setInterval(function() { + logSyncStatus(); + count = 0; + }, 10000); + } + }); }); node.on('stopping', function() { diff --git a/package.json b/package.json index 8e30d982..3a0b4253 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "package": "node bin/package.js", "upload": "node bin/upload.js", "start": "node bin/start.js", - "test": "NODE_ENV=test mocha --recursive", + "test": "NODE_ENV=test mocha -R spec --recursive", "coverage": "istanbul cover _mocha -- --recursive", "libbitcoind": "node bin/start-libbitcoind.js" }, diff --git a/test/chain.unit.js b/test/chain.unit.js deleted file mode 100644 index 3413c1c5..00000000 --- a/test/chain.unit.js +++ /dev/null @@ -1,249 +0,0 @@ -'use strict'; - -var chai = require('chai'); -var should = chai.should(); -var sinon = require('sinon'); -var memdown = require('memdown'); - -var index = require('../'); -var DB = index.DB; -var Chain = index.Chain; -var bitcore = require('bitcore'); -var BufferUtil = bitcore.util.buffer; -var Block = bitcore.Block; -var BN = bitcore.crypto.BN; - -var chainData = require('./data/testnet-blocks.json'); - -describe('Bitcoin Chain', function() { - - describe('@constructor', function() { - - it('can create a new instance with and without `new`', function() { - var chain = new Chain(); - chain = Chain(); - }); - - }); - - describe('#start', function() { - it('should call the callback when base chain is initialized', function(done) { - var chain = new Chain(); - chain.node = {}; - chain.node.modules = {}; - chain.node.modules.bitcoind = {}; - chain.node.modules.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); - chain.initialize = function() { - chain.emit('initialized'); - }; - - chain.start(done); - }); - }); - - describe('#initialize', function() { - - it('should initialize the chain with the genesis block if no metadata is found in the db', function(done) { - var db = {}; - db.getMetadata = sinon.stub().callsArgWith(0, null, {}); - db.putMetadata = sinon.stub().callsArg(1); - db.getTransactionsFromBlock = sinon.stub(); - db.connectBlock = sinon.stub().callsArg(1); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - - chain.on('ready', function() { - should.exist(chain.tip); - chain.tip.hash.should.equal('genesis'); - Number(chain.tip.__weight.toString(10)).should.equal(0); - done(); - }); - chain.on('error', function(err) { - should.not.exist(err); - done(); - }); - - chain.initialize(); - }); - - it('should initialize the chain with the metadata from the database if it exists', function(done) { - var db = {}; - db.getMetadata = sinon.stub().callsArgWith(0, null, {tip: 'block2', tipWeight: 2}); - db.putMetadata = sinon.stub().callsArg(1); - db.getBlock = sinon.stub().callsArgWith(1, null, {hash: 'block2', prevHash: 'block1'}); - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 10); - chain.getWeight = sinon.stub().callsArgWith(1, null, new BN(50)); - chain.on('ready', function() { - should.exist(chain.tip); - chain.tip.hash.should.equal('block2'); - done(); - }); - chain.on('error', function(err) { - should.not.exist(err); - done(); - }); - chain.initialize(); - }); - - it('emit error from getMetadata', function(done) { - var db = { - getMetadata: function(cb) { - cb(new Error('getMetadataError')); - } - }; - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.on('error', function(error) { - should.exist(error); - error.message.should.equal('getMetadataError'); - done(); - }); - chain.initialize(); - }); - - it('emit error from getBlock', function(done) { - var db = { - getMetadata: function(cb) { - cb(null, {tip: 'tip'}); - }, - getBlock: function(tip, cb) { - cb(new Error('getBlockError')); - } - }; - db.getTransactionsFromBlock = sinon.stub(); - db.mempool = { - on: sinon.spy() - }; - var node = { - modules: { - db: db - } - }; - var chain = new Chain({node: node, genesis: {hash: 'genesis'}}); - chain.on('error', function(error) { - should.exist(error); - error.message.should.equal('getBlockError'); - done(); - }); - chain.initialize(); - }); - }); - - describe('#stop', function() { - it('should call the callback', function(done) { - var chain = new Chain(); - chain.stop(done); - }); - }); - - describe('#_validateBlock', function() { - it('should call the callback', function(done) { - var chain = new Chain(); - chain._validateBlock('block', function(err) { - should.not.exist(err); - done(); - }); - }); - }); - - describe('#getWeight', function() { - var work = '000000000000000000000000000000000000000000005a7b3c42ea8b844374e9'; - var chain = new Chain(); - chain.node = {}; - chain.node.modules = {}; - chain.node.modules.db = {}; - chain.node.modules.bitcoind = { - getBlockIndex: sinon.stub().returns({ - chainWork: work - }) - }; - - it('should give the weight as a BN', function(done) { - chain.getWeight('hash', function(err, weight) { - should.not.exist(err); - weight.toString(16, 64).should.equal(work); - done(); - }); - }); - - it('should give an error if the weight is undefined', function(done) { - chain.node.modules.bitcoind.getBlockIndex = sinon.stub().returns(undefined); - chain.getWeight('hash2', function(err, weight) { - should.exist(err); - done(); - }); - }); - }); - - describe('#getHashes', function() { - - it('should get an array of chain hashes', function(done) { - - var blocks = {}; - var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex')); - var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex')); - var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex')); - blocks[genesisBlock.hash] = genesisBlock; - blocks[block1.hash] = block1; - blocks[block2.hash] = block2; - - var db = {}; - db.getPrevHash = function(blockHash, cb) { - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex'); - cb(null, prevHash); - }; - - var node = { - modules: { - db: db - } - }; - - var chain = new Chain({ - node: node, - genesis: genesisBlock - }); - - chain.tip = block2; - - delete chain.cache.hashes[block1.hash]; - - // the test - chain.getHashes(block2.hash, function(err, hashes) { - should.not.exist(err); - should.exist(hashes); - hashes.length.should.equal(3); - done(); - }); - - }); - }); - - -}); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index 5332afd3..69a1e34d 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -23,7 +23,7 @@ var mocknode = { } }; -describe('AddressModule', function() { +describe('Address Module', function() { describe('#getAPIMethods', function() { it('should return the correct methods', function() { @@ -424,13 +424,12 @@ describe('AddressModule', function() { describe('#getOutputs', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var db = {}; + var db = { + tip: { + __height: 1 + } + }; var testnode = { - chain: { - tip: { - __height: 1 - } - }, modules: { db: db, bitcoind: { @@ -819,6 +818,9 @@ describe('AddressModule', function() { ]; var db = { + tip: { + __height: 1 + }, getTransactionWithBlockInfo: function(txid, queryMempool, callback) { var transaction = { populateInputs: sinon.stub().callsArg(2) @@ -853,11 +855,6 @@ describe('AddressModule', function() { } }; var testnode = { - chain: { - tip: { - __height: 1 - } - }, modules: { db: db, bitcoind: { diff --git a/test/modules/db.unit.js b/test/modules/db.unit.js index 32bafcbc..1be47388 100644 --- a/test/modules/db.unit.js +++ b/test/modules/db.unit.js @@ -2,13 +2,18 @@ var should = require('chai').should(); var sinon = require('sinon'); +var EventEmitter = require('events').EventEmitter; +var proxyquire = require('proxyquire'); var index = require('../../'); var DB = index.modules.DBModule; var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var Networks = bitcore.Networks; var Block = bitcore.Block; +var BufferUtil = bitcore.util.buffer; var transactionData = require('../data/bitcoin-transactions.json'); +var chainHashes = require('../data/hashes.json'); +var chainData = require('../data/testnet-blocks.json'); var errors = index.errors; var memdown = require('memdown'); var bitcore = require('bitcore'); @@ -16,6 +21,14 @@ var Transaction = bitcore.Transaction; describe('DB Module', function() { + function hexlebuf(hexString){ + return BufferUtil.reverse(new Buffer(hexString, 'hex')); + } + + function lebufhex(buf) { + return BufferUtil.reverse(buf).toString('hex'); + } + var baseConfig = { node: { network: Networks.testnet, @@ -61,7 +74,7 @@ describe('DB Module', function() { }); it('should load the db with regtest', function() { // Switch to use regtest - Networks.remove(Networks.testnet); + // Networks.remove(Networks.testnet); Networks.add({ name: 'regtest', alias: 'regtest', @@ -85,36 +98,36 @@ describe('DB Module', function() { var db = new DB(config); db.dataPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db'); Networks.remove(regtest); - // Add testnet back - Networks.add({ - name: 'testnet', - alias: 'testnet', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0x0b110907, - port: 18333, - dnsSeeds: [ - 'testnet-seed.bitcoin.petertodd.org', - 'testnet-seed.bluematt.me', - 'testnet-seed.alexykot.me', - 'testnet-seed.bitcoin.schildbach.de' - ] - }); }); }); describe('#start', function() { + var TestDB; + var genesisBuffer; + + before(function() { + TestDB = proxyquire('../../lib/modules/db', { + fs: { + existsSync: sinon.stub().returns(true) + }, + levelup: sinon.stub() + }); + genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); + }); + it('should emit ready', function(done) { - var db = new DB(baseConfig); + var db = new TestDB(baseConfig); db.node = {}; db.node.modules = {}; db.node.modules.bitcoind = { - on: sinon.spy() + on: sinon.spy(), + genesisBuffer: genesisBuffer }; - db.addModule = sinon.spy(); + db._addModule = sinon.spy(); + db.getMetadata = sinon.stub().callsArg(0); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); var readyFired = false; db.on('ready', function() { readyFired = true; @@ -124,6 +137,143 @@ describe('DB Module', function() { done(); }); }); + + it('genesis block if no metadata is found in the db', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var db = new TestDB({node: node}); + db.getMetadata = sinon.stub().callsArgWith(0, null, null); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); + db.start(function() { + should.exist(db.tip); + db.tip.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'); + done(); + }); + }); + + it('metadata from the database if it exists', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var tip = Block.fromBuffer(genesisBuffer); + var db = new TestDB({node: node}); + var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; + db.getMetadata = sinon.stub().callsArgWith(0, null, { + tip: tipHash, + tipHeight: 0 + }); + db.getBlock = sinon.stub().callsArgWith(1, null, tip); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); + db.start(function() { + should.exist(db.tip); + db.tip.hash.should.equal(tipHash); + done(); + }); + }); + + it('emit error from getMetadata', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var db = new TestDB({node: node}); + db.getMetadata = sinon.stub().callsArgWith(0, new Error('test')); + db.start(function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + + it('emit error from getBlock', function(done) { + var node = { + network: Networks.testnet, + datadir: 'testdir', + modules: { + bitcoind: { + genesisBuffer: genesisBuffer, + on: sinon.stub() + } + } + }; + var db = new TestDB({node: node}); + var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; + db.getMetadata = sinon.stub().callsArgWith(0, null, { + tip: tipHash, + tipHeigt: 0 + }); + db.getBlock = sinon.stub().callsArgWith(1, new Error('test')); + db.start(function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + + it('will call sync when there is a new tip', function(done) { + var db = new TestDB(baseConfig); + db.node.modules = {}; + db.node.modules.bitcoind = new EventEmitter(); + db.node.modules.bitcoind.syncPercentage = sinon.spy(); + db.node.modules.bitcoind.genesisBuffer = genesisBuffer; + db.getMetadata = sinon.stub().callsArg(0); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.sync = sinon.stub(); + db.start(function() { + db.sync = function() { + db.node.modules.bitcoind.syncPercentage.callCount.should.equal(1); + done(); + }; + db.node.modules.bitcoind.emit('tip', 10); + }); + }); + + it('will not call sync when there is a new tip and shutting down', function(done) { + var db = new TestDB(baseConfig); + db.node.modules = {}; + db.node.modules.bitcoind = new EventEmitter(); + db.node.modules.bitcoind.syncPercentage = sinon.spy(); + db.node.modules.bitcoind.genesisBuffer = genesisBuffer; + db.getMetadata = sinon.stub().callsArg(0); + db.connectBlock = sinon.stub().callsArg(1); + db.saveMetadata = sinon.stub(); + db.node.stopping = true; + db.sync = sinon.stub(); + db.start(function() { + db.sync.callCount.should.equal(1); + db.node.modules.bitcoind.once('tip', function() { + db.sync.callCount.should.equal(1); + done(); + }); + db.node.modules.bitcoind.emit('tip', 10); + }); + }); + }); describe('#stop', function() { @@ -404,4 +554,237 @@ describe('DB Module', function() { methods.length.should.equal(5); }); }); + + describe('#getHashes', function() { + + it('should get an array of chain hashes', function(done) { + + var blocks = {}; + var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex')); + var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex')); + var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex')); + blocks[genesisBlock.hash] = genesisBlock; + blocks[block1.hash] = block1; + blocks[block2.hash] = block2; + + var db = new DB(baseConfig); + db.genesis = genesisBlock; + db.getPrevHash = function(blockHash, cb) { + // TODO: expose prevHash as a string from bitcore + var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex'); + cb(null, prevHash); + }; + + db.tip = block2; + + // the test + db.getHashes(block2.hash, function(err, hashes) { + should.not.exist(err); + should.exist(hashes); + hashes.length.should.equal(3); + done(); + }); + + }); + }); + + describe('#findCommonAncestor', function() { + it('will find an ancestor 6 deep', function() { + var db = new DB(baseConfig); + db.getHashes = function(tipHash, callback) { + callback(null, chainHashes); + }; + db.tip = { + hash: chainHashes[chainHashes.length] + }; + var expectedAncestor = chainHashes[chainHashes.length - 6]; + + var forkedBlocks = { + 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { + header: { + prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') + } + }, + '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { + header: { + prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') + } + }, + 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { + header: { + prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') + } + }, + '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { + header: { + prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') + } + }, + 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { + header: { + prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') + } + }, + '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { + header: { + prevHash: hexlebuf(expectedAncestor) + } + }, + }; + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlockIndex: function(hash) { + var block = forkedBlocks[hash]; + return { + prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex') + }; + } + }; + var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82']; + db.findCommonAncestor(block, function(err, ancestorHash) { + if (err) { + throw err; + } + ancestorHash.should.equal(expectedAncestor); + }); + }); + }); + + describe('#syncRewind', function() { + it('will undo blocks 6 deep', function() { + var db = new DB(baseConfig); + var ancestorHash = chainHashes[chainHashes.length - 6]; + db.tip = { + __height: 10, + hash: chainHashes[chainHashes.length], + header: { + prevHash: hexlebuf(chainHashes[chainHashes.length - 1]) + } + }; + db.saveMetadata = sinon.stub(); + db.emit = sinon.stub(); + db.getBlock = function(hash, callback) { + setImmediate(function() { + for(var i = chainHashes.length; i > 0; i--) { + var block = { + hash: chainHashes[i], + header: { + prevHash: hexlebuf(chainHashes[i - 1]) + } + }; + if (chainHashes[i] === hash) { + callback(null, block); + } + } + }); + }; + db.node.modules = {}; + db.disconnectBlock = function(block, callback) { + setImmediate(callback); + }; + db.findCommonAncestor = function(block, callback) { + setImmediate(function() { + callback(null, ancestorHash); + }); + }; + var forkedBlock = {}; + db.syncRewind(forkedBlock, function(err) { + if (err) { + throw err; + } + db.tip.__height.should.equal(4); + db.tip.hash.should.equal(ancestorHash); + }); + }); + }); + + describe('#sync', function() { + var node = new EventEmitter(); + var syncConfig = { + node: node, + store: memdown + }; + syncConfig.node.network = Networks.testnet; + syncConfig.node.datadir = 'testdir'; + it('will get and add block up to the tip height', function(done) { + var db = new DB(syncConfig); + var blockBuffer = new Buffer(blockData, 'hex'); + var block = Block.fromBuffer(blockBuffer); + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), + isSynced: sinon.stub().returns(true), + height: 1 + }; + db.tip = { + __height: 0, + hash: lebufhex(block.header.prevHash) + }; + db.getHashes = sinon.stub().callsArgWith(1, null); + db.saveMetadata = sinon.stub(); + db.emit = sinon.stub(); + db.cache = { + hashes: {} + }; + db.connectBlock = function(block, callback) { + db.tip.__height += 1; + callback(); + }; + db.node.once('synced', function() { + done(); + }); + db.sync(); + }); + it('will exit and emit error with error from bitcoind.getBlock', function(done) { + var db = new DB(syncConfig); + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlock: sinon.stub().callsArgWith(1, new Error('test error')), + height: 1 + }; + db.tip = { + __height: 0 + }; + db.node.on('error', function(err) { + err.message.should.equal('test error'); + done(); + }); + db.sync(); + }); + it('will stop syncing when the node is stopping', function(done) { + var db = new DB(syncConfig); + var blockBuffer = new Buffer(blockData, 'hex'); + var block = Block.fromBuffer(blockBuffer); + db.node.modules = {}; + db.node.modules.bitcoind = { + getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), + isSynced: sinon.stub().returns(true), + height: 1 + }; + db.tip = { + __height: 0, + hash: block.prevHash + }; + db.saveMetadata = sinon.stub(); + db.emit = sinon.stub(); + db.cache = { + hashes: {} + }; + db.connectBlock = function(block, callback) { + db.tip.__height += 1; + callback(); + }; + db.node.stopping = true; + var synced = false; + db.node.once('synced', function() { + synced = true; + }); + db.sync(); + setTimeout(function() { + synced.should.equal(false); + done(); + }, 10); + }); + }); + }); diff --git a/test/node.unit.js b/test/node.unit.js index 59313f80..195cb2ce 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -2,16 +2,9 @@ var should = require('chai').should(); var sinon = require('sinon'); -var EventEmitter = require('events').EventEmitter; var bitcore = require('bitcore'); var Networks = bitcore.Networks; -var BufferUtil = bitcore.util.buffer; -var Block = bitcore.Block; -var blockData = require('./data/livenet-345003.json'); var proxyquire = require('proxyquire'); -var index = require('..'); -var fs = require('fs'); -var chainHashes = require('./data/hashes.json'); var util = require('util'); var BaseModule = require('../lib/module'); @@ -23,30 +16,43 @@ describe('Bitcore Node', function() { var Node; - function hexlebuf(hexString){ - return BufferUtil.reverse(new Buffer(hexString, 'hex')); - } - - function lebufhex(buf) { - return BufferUtil.reverse(buf).toString('hex'); - } - before(function() { Node = proxyquire('../lib/node', {}); Node.prototype._loadConfiguration = sinon.spy(); Node.prototype._initialize = sinon.spy(); }); + after(function() { + var regtest = Networks.get('regtest'); + if (regtest) { + Networks.remove(regtest); + } + // restore testnet + Networks.add({ + name: 'testnet', + alias: 'testnet', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0x0b110907, + port: 18333, + dnsSeeds: [ + 'testnet-seed.bitcoin.petertodd.org', + 'testnet-seed.bluematt.me', + 'testnet-seed.alexykot.me', + 'testnet-seed.bitcoin.schildbach.de' + ], + }); + }); describe('@constructor', function() { - it('will set properties', function() { - function TestModule() {} + var TestModule; + before(function() { + TestModule = function TestModule() {}; util.inherits(TestModule, BaseModule); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { - return [ - ['getData', this, this.getData, 1] - ]; - }; + }); + it('will set properties', function() { var config = { datadir: 'testdir', modules: [ @@ -57,14 +63,70 @@ describe('Bitcore Node', function() { ], }; var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype._loadConfiguration = sinon.spy(); - TestNode.prototype._initialize = sinon.spy(); + TestNode.prototype.start = sinon.spy(); var node = new TestNode(config); - TestNode.prototype._loadConfiguration.callCount.should.equal(1); - TestNode.prototype._initialize.callCount.should.equal(1); + TestNode.prototype.start.callCount.should.equal(1); node._unloadedModules.length.should.equal(1); node._unloadedModules[0].name.should.equal('test1'); node._unloadedModules[0].module.should.equal(TestModule); + node.network.should.equal(Networks.defaultNetwork); + }); + it('will set network to testnet', function() { + var config = { + network: 'testnet', + datadir: 'testdir', + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype.start = sinon.spy(); + var node = new TestNode(config); + node.network.should.equal(Networks.testnet); + }); + it('will set network to regtest', function() { + var config = { + network: 'regtest', + datadir: 'testdir', + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype.start = sinon.spy(); + var node = new TestNode(config); + var regtest = Networks.get('regtest'); + should.exist(regtest); + node.network.should.equal(regtest); + }); + it('should emit error if an error occurred starting services', function(done) { + var config = { + datadir: 'testdir', + modules: [ + { + name: 'test1', + module: TestModule + } + ], + }; + var TestNode = proxyquire('../lib/node', {}); + TestNode.prototype.start = function(callback) { + setImmediate(function() { + callback(new Error('error')); + }); + }; + var node = new TestNode(config); + node.once('error', function(err) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); }); }); @@ -76,27 +138,6 @@ describe('Bitcore Node', function() { }); }); - describe('#addModule', function() { - it('will instantiate an instance and load api methods', function() { - var node = new Node(baseConfig); - function TestModule() {} - util.inherits(TestModule, BaseModule); - TestModule.prototype.getData = function() {}; - TestModule.prototype.getAPIMethods = function() { - return [ - ['getData', this, this.getData, 1] - ]; - }; - var service = { - name: 'testmodule', - module: TestModule - }; - node.addModule(service); - should.exist(node.modules.testmodule); - should.exist(node.getData); - }); - }); - describe('#getAllAPIMethods', function() { it('should return db methods and modules methods', function() { var node = new Node(baseConfig); @@ -116,6 +157,7 @@ describe('Bitcore Node', function() { methods.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); }); }); + describe('#getAllPublishEvents', function() { it('should return modules publish events', function() { var node = new Node(baseConfig); @@ -134,381 +176,28 @@ describe('Bitcore Node', function() { events.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); }); }); - describe('#_loadConfiguration', function() { - it('should call the necessary methods', function() { - var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype._initialize = sinon.spy(); - TestNode.prototype._loadConsensus = sinon.spy(); - var node = new TestNode(baseConfig); - node._loadConsensus.callCount.should.equal(1); - }); - }); - describe('#_syncBitcoindAncestor', function() { - it('will find an ancestor 6 deep', function() { - var node = new Node(baseConfig); - node.chain = { - getHashes: function(tipHash, callback) { - callback(null, chainHashes); - }, - tip: { - hash: chainHashes[chainHashes.length] - } - }; - var expectedAncestor = chainHashes[chainHashes.length - 6]; - - var forkedBlocks = { - 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { - header: { - prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') - } - }, - '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { - header: { - prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') - } - }, - 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { - header: { - prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') - } - }, - '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { - header: { - prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') - } - }, - 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { - header: { - prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') - } - }, - '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { - header: { - prevHash: hexlebuf(expectedAncestor) - } - }, - }; - node.modules = {}; - node.modules.bitcoind = { - getBlockIndex: function(hash) { - var block = forkedBlocks[hash]; - return { - prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex') - }; - } - }; - var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82']; - node._syncBitcoindAncestor(block, function(err, ancestorHash) { - if (err) { - throw err; - } - ancestorHash.should.equal(expectedAncestor); - }); - }); - }); - describe('#_syncBitcoindRewind', function() { - it('will undo blocks 6 deep', function() { - var node = new Node(baseConfig); - var ancestorHash = chainHashes[chainHashes.length - 6]; - node.chain = { - tip: { - __height: 10, - hash: chainHashes[chainHashes.length], - header: { - prevHash: hexlebuf(chainHashes[chainHashes.length - 1]) - } - }, - saveMetadata: sinon.stub(), - emit: sinon.stub() - }; - node.getBlock = function(hash, callback) { - setImmediate(function() { - for(var i = chainHashes.length; i > 0; i--) { - var block = { - hash: chainHashes[i], - header: { - prevHash: hexlebuf(chainHashes[i - 1]) - } - }; - if (chainHashes[i] === hash) { - callback(null, block); - } - } - }); - }; - node.modules = {}; - node.modules.db = { - disconnectBlock: function(block, callback) { - setImmediate(callback); - } - }; - node._syncBitcoindAncestor = function(block, callback) { - setImmediate(function() { - callback(null, ancestorHash); - }); - }; - var forkedBlock = {}; - node._syncBitcoindRewind(forkedBlock, function(err) { - if (err) { - throw err; - } - node.chain.tip.__height.should.equal(4); - node.chain.tip.hash.should.equal(ancestorHash); - }); - }); - }); - describe('#_syncBitcoind', function() { - it('will get and add block up to the tip height', function(done) { - var node = new Node(baseConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - node.modules = {}; - node.modules.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - node.chain = { - tip: { - __height: 0, - hash: lebufhex(block.header.prevHash) - }, - getHashes: sinon.stub().callsArgWith(1, null), - saveMetadata: sinon.stub(), - emit: sinon.stub(), - cache: { - hashes: {} - } - }; - node.modules.db = { - connectBlock: function(block, callback) { - node.chain.tip.__height += 1; - callback(); - } - }; - node.on('synced', function() { - done(); - }); - node._syncBitcoind(); - }); - it('will exit and emit error with error from bitcoind.getBlock', function(done) { - var node = new Node(baseConfig); - node.modules = {}; - node.modules.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, new Error('test error')), - height: 1 - }; - node.chain = { - tip: { - __height: 0 - } - }; - node.on('error', function(err) { - err.message.should.equal('test error'); - done(); - }); - node._syncBitcoind(); - }); - it('will stop syncing when the node is stopping', function(done) { - var node = new Node(baseConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - node.modules = {}; - node.modules.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - node.chain = { - tip: { - __height: 0, - hash: block.prevHash - }, - saveMetadata: sinon.stub(), - emit: sinon.stub(), - cache: { - hashes: {} - } - }; - node.modules.db = { - connectBlock: function(block, callback) { - node.chain.tip.__height += 1; - callback(); - } - }; - node.stopping = true; - - var synced = false; - - node.on('synced', function() { - synced = true; - }); - - node._syncBitcoind(); - - setTimeout(function() { - synced.should.equal(false); - done(); - }, 10); - }); - }); - - describe('#_loadNetwork', function() { - it('should use the testnet network if testnet is specified', function() { - var config = { - datadir: 'testdir', - network: 'testnet' - }; - var node = new Node(config); - node._loadNetwork(config); - node.network.name.should.equal('testnet'); - }); - it('should use the regtest network if regtest is specified', function() { - var config = { - datadir: 'testdir', - network: 'regtest' - }; - var node = new Node(config); - node._loadNetwork(config); - node.network.name.should.equal('regtest'); - }); - it('should use the livenet network if nothing is specified', function() { - var config = { - datadir: 'testdir' - }; - var node = new Node(config); - node._loadNetwork(config); - node.network.name.should.equal('livenet'); - }); - }); - describe('#_loadConsensus', function() { - var node; - before(function() { - node = new Node(baseConfig); - }); - it('will set properties', function() { - node._loadConsensus(); - should.exist(node.chain); - }); - }); - describe('#_initialize', function() { - var node; - before(function() { - var TestNode = proxyquire('../lib/node', {}); - TestNode.prototype._loadConfiguration = sinon.spy(); - TestNode.prototype._initializeChain = sinon.spy(); - - // mock the _initialize during construction - var _initialize = TestNode.prototype._initialize; - TestNode.prototype._initialize = sinon.spy(); - - node = new TestNode(baseConfig); - node.chain = { - on: sinon.spy() - }; - node.modules = {}; - node.modules.bitcoind = { - on: sinon.spy() - }; - node.modules.db = { - on: sinon.spy() - }; - // restore the original method - node._initialize = _initialize; - }); - - it('should initialize', function(done) { - node.once('ready', function() { - done(); - }); - node.start = sinon.stub().callsArg(0); - node._initialize(); - node._initializeChain.callCount.should.equal(1); - }); - - it('should emit an error if an error occurred starting services', function(done) { - node.once('error', function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - node.start = sinon.stub().callsArgWith(0, new Error('error')); - node._initialize(); - }); - - }); - - describe('#_initializeChain', function() { - - it('will call sync when there is a new tip', function(done) { - var node = new Node(baseConfig); - node.chain = new EventEmitter(); - node.modules = {}; - node.modules.bitcoind = new EventEmitter(); - node.modules.bitcoind.syncPercentage = sinon.spy(); - node._syncBitcoind = function() { - node.modules.bitcoind.syncPercentage.callCount.should.equal(1); - done(); - }; - node._initializeChain(); - node.chain.emit('ready'); - node.modules.bitcoind.emit('tip', 10); - }); - it('will not call sync when there is a new tip and shutting down', function(done) { - var node = new Node(baseConfig); - node.chain = new EventEmitter(); - node.modules = {}; - node.modules.bitcoind = new EventEmitter(); - node._syncBitcoind = sinon.spy(); - node.modules.bitcoind.syncPercentage = sinon.spy(); - node.stopping = true; - node.modules.bitcoind.on('tip', function() { - setImmediate(function() { - node.modules.bitcoind.syncPercentage.callCount.should.equal(0); - node._syncBitcoind.callCount.should.equal(0); - done(); - }); - }); - node._initializeChain(); - node.chain.emit('ready'); - node.modules.bitcoind.emit('tip', 10); - }); - it('will emit an error from the chain', function(done) { - var node = new Node(baseConfig); - node.chain = new EventEmitter(); - node.on('error', function(err) { - should.exist(err); - err.message.should.equal('test error'); - done(); - }); - node._initializeChain(); - node.chain.emit('error', new Error('test error')); - }); - }); describe('#getServiceOrder', function() { it('should return the services in the correct order', function() { var node = new Node(baseConfig); - node.getServices = function() { - return [ - { - name: 'chain', - dependencies: ['db'] - }, - { - name: 'db', + node._unloadedModules = [ + { + name: 'chain', + dependencies: ['db'] + }, + { + name: 'db', dependencies: ['daemon', 'p2p'] - }, - { - name:'daemon', - dependencies: [] - }, - { - name: 'p2p', - dependencies: [] - } - ]; - }; + }, + { + name:'daemon', + dependencies: [] + }, + { + name: 'p2p', + dependencies: [] + } + ]; var order = node.getServiceOrder(); order[0].name.should.equal('daemon'); order[1].name.should.equal('p2p'); @@ -517,9 +206,31 @@ describe('Bitcore Node', function() { }); }); + describe('#_instantiateModule', function() { + it('will instantiate an instance and load api methods', function() { + var node = new Node(baseConfig); + function TestModule() {} + util.inherits(TestModule, BaseModule); + TestModule.prototype.getData = function() {}; + TestModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + var service = { + name: 'testmodule', + module: TestModule + }; + node._instantiateModule(service); + should.exist(node.modules.testmodule); + should.exist(node.getData); + }); + }); + describe('#start', function() { it('will call start for each module', function(done) { var node = new Node(baseConfig); + function TestModule() {} util.inherits(TestModule, BaseModule); TestModule.prototype.start = sinon.stub().callsArg(0); @@ -529,23 +240,76 @@ describe('Bitcore Node', function() { ['getData', this, this.getData, 1] ]; }; - node.test2 = {}; - node.test2.start = sinon.stub().callsArg(0); + + function TestModule2() {} + util.inherits(TestModule2, BaseModule); + TestModule2.prototype.start = sinon.stub().callsArg(0); + TestModule2.prototype.getData2 = function() {}; + TestModule2.prototype.getAPIMethods = function() { + return [ + ['getData2', this, this.getData2, 1] + ]; + }; + node.getServiceOrder = sinon.stub().returns([ { name: 'test1', module: TestModule }, { - name: 'test2' + name: 'test2', + module: TestModule2 } ]); node.start(function() { - node.test2.start.callCount.should.equal(1); + TestModule2.prototype.start.callCount.should.equal(1); TestModule.prototype.start.callCount.should.equal(1); + should.exist(node.getData2); + should.exist(node.getData); done(); }); }); + it('will error if there are conflicting API methods', function(done) { + var node = new Node(baseConfig); + + function TestModule() {} + util.inherits(TestModule, BaseModule); + TestModule.prototype.start = sinon.stub().callsArg(0); + TestModule.prototype.getData = function() {}; + TestModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + + function ConflictModule() {} + util.inherits(ConflictModule, BaseModule); + ConflictModule.prototype.start = sinon.stub().callsArg(0); + ConflictModule.prototype.getData = function() {}; + ConflictModule.prototype.getAPIMethods = function() { + return [ + ['getData', this, this.getData, 1] + ]; + }; + + node.getServiceOrder = sinon.stub().returns([ + { + name: 'test', + module: TestModule + }, + { + name: 'conflict', + module: ConflictModule + } + ]); + + node.start(function(err) { + should.exist(err); + err.message.should.match(/^Existing API method exists/); + done(); + }); + + }); }); describe('#stop', function() { @@ -566,20 +330,15 @@ describe('Bitcore Node', function() { node.test2 = {}; node.test2.stop = sinon.stub().callsArg(0); node.getServiceOrder = sinon.stub().returns([ - { - name: 'test2' - }, { name: 'test1', module: TestModule } ]); node.stop(function() { - node.test2.stop.callCount.should.equal(1); TestModule.prototype.stop.callCount.should.equal(1); done(); }); }); }); - });