diff --git a/.travis.yml b/.travis.yml index 86d0f69a..52844c32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ before_install: - git config --global user.name "BitPay, Inc." script: - _mocha -R spec integration/regtest.js + - _mocha -R spec integration/regtest-node.js - _mocha -R spec --recursive cache: directories: diff --git a/integration/data/bitcoin.conf b/integration/data/bitcoin.conf index 2ebd94ba..840c95a3 100644 --- a/integration/data/bitcoin.conf +++ b/integration/data/bitcoin.conf @@ -4,5 +4,3 @@ txindex=1 rpcallowip=127.0.0.1 rpcuser=bitcoin rpcpassword=local321 - - diff --git a/integration/regtest-node.js b/integration/regtest-node.js new file mode 100644 index 00000000..4bfbecb5 --- /dev/null +++ b/integration/regtest-node.js @@ -0,0 +1,184 @@ +'use strict'; + +// These tests require bitcoind.js Bitcoin Core bindings to be compiled with +// the environment variable BITCOINDJS_ENV=test. This enables the use of regtest +// functionality by including the wallet in the build. +// To run the tests: $ mocha -R spec integration/regtest-node.js + +var chainlib = require('chainlib'); +var async = require('async'); +var log = chainlib.log; +log.debug = function() {}; + +if (process.env.BITCORENODE_ENV !== 'test') { + log.info('Please set the environment variable BITCORENODE_ENV=test and make sure bindings are compiled for testing'); + process.exit(); +} + +var chai = require('chai'); +var bitcore = require('bitcore'); +var rimraf = require('rimraf'); +var node; + +var should = chai.should(); + +var BitcoinRPC = require('bitcoind-rpc'); +var BitcoreNode = require('..').Node; +var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG'; +var testKey; +var client; + +describe('Node Functionality', function() { + + before(function(done) { + this.timeout(30000); + + // Add the regtest network + bitcore.Networks.remove(bitcore.Networks.testnet); + bitcore.Networks.add({ + name: 'regtest', + alias: 'regtest', + pubkeyhash: 0x6f, + privatekey: 0xef, + scripthash: 0xc4, + xpubkey: 0x043587cf, + xprivkey: 0x04358394, + networkMagic: 0xfabfb5da, + port: 18444, + dnsSeeds: [ ] + }); + + var datadir = __dirname + '/data'; + + testKey = bitcore.PrivateKey(testWIF); + + rimraf(datadir + '/regtest', function(err) { + + if (err) { + throw err; + } + + var configuration = { + datadir: datadir, + network: 'regtest' + }; + + node = new BitcoreNode(configuration); + + node.on('error', function(err) { + log.error(err); + }); + + node.on('ready', function() { + + client = new BitcoinRPC({ + protocol: 'http', + host: '127.0.0.1', + port: 18332, + user: 'bitcoin', + pass: 'local321' + }); + + var syncedHandler = function() { + if (node.chain.tip.__height === 150) { + node.removeListener('synced', syncedHandler); + done(); + } + }; + + node.on('synced', syncedHandler); + + client.generate(150, function(err, response) { + if (err) { + throw err; + } + }); + }); + + + }); + }); + + after(function(done) { + this.timeout(20000); + node.db.bitcoind.stop(function(err, result) { + done(); + }); + }); + + it('will handle a reorganization', function(done) { + + var count; + var blockHash; + + async.series([ + function(next) { + client.getBlockCount(function(err, response) { + if (err) { + return next(err); + } + count = response.result; + next(); + }); + }, + function(next) { + client.getBlockHash(count, function(err, response) { + if (err) { + return next(err); + } + blockHash = response.result; + next(); + }); + }, + function(next) { + client.invalidateBlock(blockHash, next); + }, + function(next) { + client.getBlockCount(function(err, response) { + if (err) { + return next(err); + } + response.result.should.equal(count - 1); + next(); + }); + } + ], function(err) { + if (err) { + throw err; + } + var blocksRemoved = 0; + var blocksAdded = 0; + + var removeBlock = function() { + blocksRemoved++; + }; + + node.chain.on('removeblock', removeBlock); + + var addBlock = function() { + blocksAdded++; + if (blocksAdded === 2 && blocksRemoved === 1) { + node.chain.removeListener('addblock', addBlock); + node.chain.removeListener('removeblock', removeBlock); + done(); + } + }; + + node.chain.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. + client.sendToAddress(testKey.toAddress().toString(), 10, function(err) { + if (err) { + throw err; + } + client.generate(2, function(err, response) { + if (err) { + throw err; + } + }); + }); + }); + + }); +}); diff --git a/integration/regtest.js b/integration/regtest.js index 97df6d4f..468d5517 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -66,21 +66,14 @@ describe('Daemon Binding Functionality', function() { network: 'regtest' }); - bitcoind.start(function() { - log.info('Bitcoind started'); - }); - bitcoind.on('error', function(err) { log.error('error="%s"', err.message); }); - bitcoind.on('open', function(status) { - log.info('status="%s"', status); - }); - log.info('Waiting for Bitcoin Core to initialize...'); - bitcoind.on('ready', function() { + bitcoind.start(function() { + log.info('Bitcoind started'); client = new BitcoinRPC({ protocol: 'http', @@ -96,6 +89,7 @@ describe('Daemon Binding Functionality', function() { // can be spent. client.generate(150, function(err, response) { + if (err) { throw err; } @@ -139,7 +133,9 @@ describe('Daemon Binding Functionality', function() { }); }); }); + }); + }); }); diff --git a/lib/chain.js b/lib/chain.js index 7e93d38b..e6c17665 100644 --- a/lib/chain.js +++ b/lib/chain.js @@ -45,12 +45,15 @@ function Chain(options) { this.targetTimespan = options.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN; this.targetSpacing = options.targetSpacing || Chain.DEFAULTS.TARGET_SPACING; + this.node = options.node; + return this; } util.inherits(Chain, BaseChain); Chain.prototype.start = function(callback) { + this.genesis = Block.fromBuffer(this.node.bitcoind.genesisBuffer); this.on('initialized', callback); this.initialize(); }; diff --git a/lib/daemon.js b/lib/daemon.js index 54717ad3..24508922 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -27,6 +27,8 @@ function Daemon(options) { this.options.datadir = this.options.datadir.replace(/^~/, process.env.HOME); this.datadir = this.options.datadir; + this.node = options.node; + this.config = this.datadir + '/bitcoin.conf'; Object.keys(exports).forEach(function(key) { @@ -68,6 +70,7 @@ Daemon.prototype.start = function(callback) { function onTipUpdateListener(result) { if (result) { // Emit and event that the tip was updated + self.height = result; self.emit('tip', result); // Recursively wait until the next update bitcoind.onTipUpdate(onTipUpdateListener); @@ -82,10 +85,17 @@ Daemon.prototype.start = function(callback) { } }); - setImmediate(function() { + // Set the current chain height + var info = self.getInfo(); + self.height = info.blocks; + + // Get the genesis block + self.getBlock(0, function(err, block) { + self.genesisBuffer = block; self.emit('ready', result); - callback(); + setImmediate(callback); }); + }); }); }; diff --git a/lib/db.js b/lib/db.js index ea287a5b..cfab74f3 100644 --- a/lib/db.js +++ b/lib/db.js @@ -26,6 +26,8 @@ function DB(options) { this.network = bitcore.Networks.get(options.network) || bitcore.Networks.testnet; + this.node = options.node; + // Modules to be loaded when ready this._modules = options.modules || []; this._modules.push(AddressModule); @@ -49,7 +51,7 @@ DB.prototype.start = function(callback) { } this.bitcoind.on('tx', this.transactionHandler.bind(this)); this.emit('ready'); - callback(); + setImmediate(callback); }; DB.prototype.stop = function(callback) { @@ -132,7 +134,7 @@ DB.prototype.estimateFee = function(blocks, callback) { setImmediate(function() { callback(null, self.bitcoind.estimateFee(blocks)); }); -} +}; DB.prototype.validateBlockData = function(block, callback) { // bitcoind does the validation diff --git a/lib/genesis.json b/lib/genesis.json deleted file mode 100644 index 2c06930b..00000000 --- a/lib/genesis.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "livenet": "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", - "testnet": "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000" -} diff --git a/lib/node.js b/lib/node.js index 04fce0bd..18712823 100644 --- a/lib/node.js +++ b/lib/node.js @@ -14,7 +14,6 @@ var bitcore = require('bitcore'); var Networks = bitcore.Networks; var _ = bitcore.deps._; var $ = bitcore.util.preconditions; -var genesis = require('./genesis.json'); var daemon = require('./daemon'); var Bus = require('./bus'); @@ -25,12 +24,6 @@ function Node(config) { util.inherits(Node, BaseNode); -var defaultServices = { - 'bitcoind': [], - 'db': ['bitcoind'], - 'chain': ['db'], -}; - Node.prototype.openBus = function() { return new Bus({db: this.db}); }; @@ -92,7 +85,7 @@ Node.prototype._loadBitcoinConf = function(config) { } } - $.checkState((this.bitcoinConfiguration.txindex && this.bitcoinConfiguration.txindex == 1), + $.checkState((this.bitcoinConfiguration.txindex && this.bitcoinConfiguration.txindex == 1), 'Txindex option is required in order to use most of the features of bitcore-node. \ Please add "txindex=1" to your configuration and reindex an existing database if necessary with reindex=1'); }; @@ -101,6 +94,7 @@ Node.prototype._loadBitcoind = function(config) { var bitcoindConfig = {}; bitcoindConfig.datadir = config.datadir; bitcoindConfig.network = config.network; + bitcoindConfig.node = this; // start the bitcoind daemon this.bitcoind = daemon(bitcoindConfig); @@ -135,31 +129,21 @@ Node.prototype._syncBitcoindAncestor = function(block, done) { // 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. - async.whilst(function() { - // Wait until the previous hash is in the current chain - return ancestorHash && !currentHashesMap[ancestorHash]; - }, function(next) { - self.bitcoind.getBlockIndex(ancestorHash, function(err, blockIndex) { - if (err) { - return next(err); - } - ancestorHash = blockIndex.prevHash; - next(); - }); - }, function(err) { + while(ancestorHash && !currentHashesMap[ancestorHash]) { + var blockIndex = self.bitcoind.getBlockIndex(ancestorHash); + ancestorHash = blockIndex ? blockIndex.prevHash : null; + } - // Hash map is no-longer needed, quickly let - // scavenging garbage collection know to cleanup - currentHashesMap = null; + // Hash map is no-longer needed, quickly let + // scavenging garbage collection know to cleanup + currentHashesMap = null; - if (err) { - return done(err); - } else if (!ancestorHash) { - return done(new Error('Unknown common ancestor.')); - } + if (!ancestorHash) { + return done(new Error('Unknown common ancestor.')); + } + + done(null, ancestorHash); - done(null, ancestorHash); - }); }); }; @@ -174,7 +158,9 @@ 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() { @@ -236,7 +222,7 @@ Node.prototype._syncBitcoind = function() { async.whilst(function() { height = self.chain.tip.__height; - return height < self.bitcoindHeight && !self.stopping; + return height < self.bitcoind.height && !self.stopping; }, function(done) { self.bitcoind.getBlock(height + 1, function(err, blockBuffer) { if (err) { @@ -244,6 +230,7 @@ Node.prototype._syncBitcoind = function() { } var block = self.Block.fromBuffer(blockBuffer); + if (block.prevHash === self.chain.tip.hash) { // This block appends to the current chain tip and we can @@ -252,22 +239,27 @@ Node.prototype._syncBitcoind = function() { // Populate height block.__height = self.chain.tip.__height + 1; - // Update chain hashes + // Update chain.cache.hashes self.chain.cache.hashes[block.hash] = block.prevHash; - // Create indexes - self.db._onChainAddBlock(block, function(err) { + // Update chain.cache.chainHashes + self.chain.getHashes(block.hash, function(err, hashes) { if (err) { return done(err); } - - delete self.chain.tip.__transactions; - 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); + // Create indexes + self.db._onChainAddBlock(block, function(err) { + if (err) { + return done(err); + } + delete self.chain.tip.__transactions; + 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 { @@ -325,15 +317,13 @@ Node.prototype._loadNetwork = function(config) { }; Node.prototype._loadDB = function(config) { + var options = _.clone(config.db || {}); + if (config.DB) { // Other modules can inherit from our DB and replace it with their own DB = config.DB; } - if(!config.db) { - config.db = {}; - } - // Store the additional indexes in a new directory // based on the network configuration and the datadir $.checkArgument(config.datadir, 'Please specify "datadir" in configuration options'); @@ -341,46 +331,35 @@ Node.prototype._loadDB = function(config) { var regtest = Networks.get('regtest'); var datadir = config.datadir.replace(/^~/, process.env.HOME); if (this.network === Networks.livenet) { - config.db.path = datadir + '/bitcore-node.db'; + options.path = datadir + '/bitcore-node.db'; } else if (this.network === Networks.testnet) { - config.db.path = datadir + '/testnet3/bitcore-node.db'; + options.path = datadir + '/testnet3/bitcore-node.db'; } else if (this.network === regtest) { - config.db.path = datadir + '/regtest/bitcore-node.db'; + options.path = datadir + '/regtest/bitcore-node.db'; } else { throw new Error('Unknown network: ' + this.network); } - config.db.network = this.network; + options.network = this.network; - if (!fs.existsSync(config.db.path)) { - mkdirp.sync(config.db.path); + if (!fs.existsSync(options.path)) { + mkdirp.sync(options.path); } - this.db = new DB(config.db); + options.node = this; + + this.db = new DB(options); }; Node.prototype._loadConsensus = function(config) { - if (!config.consensus) { - config.consensus = {}; - } - - this.Block = Block; - - var genesisBlock; - if (config.genesis) { - genesisBlock = config.genesis; - } else if (config.network === 'testnet') { - genesisBlock = genesis.testnet; + var options; + if (!config) { + options = {}; } else { - genesisBlock = genesis.livenet; + options = _.clone(config.consensus || {}); } - - if (_.isString(genesisBlock)) { - genesisBlock = this.Block.fromBuffer(new Buffer(genesisBlock, 'hex')); - } - - // pass genesis to chain - config.consensus.genesis = genesisBlock; - this.chain = new Chain(config.consensus); + options.node = this; + this.Block = Block; + this.chain = new Chain(options); }; Node.prototype._initialize = function() { @@ -394,12 +373,24 @@ Node.prototype._initialize = function() { // Chain References this.chain.db = this.db; - // Bitcoind - this.bitcoind.on('ready', function(status) { + this._initializeBitcoind(); + this._initializeDatabase(); + this._initializeChain(); + + this.start(function(err) { + if(err) { + return self.emit('error', err); + } + self.emit('ready'); + }); +}; + +Node.prototype._initializeBitcoind = function() { + var self = this; + + // Notify that there is a new tip + this.bitcoind.on('ready', function() { log.info('Bitcoin Daemon Ready'); - // Set the current chain height - var info = self.bitcoind.getInfo(); - self.bitcoindHeight = info.blocks; }); // Notify that there is a new tip @@ -407,7 +398,6 @@ Node.prototype._initialize = function() { if(!self.stopping) { var percentage = self.bitcoind.syncPercentage(); log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage); - self.bitcoindHeight = height; self._syncBitcoind(); } }); @@ -417,18 +407,10 @@ Node.prototype._initialize = function() { self.emit('error', err); }); - // Chain - this.chain.on('ready', function() { - log.info('Bitcoin Chain Ready'); - self._syncBitcoind(); - }); +}; - this.chain.on('error', function(err) { - Error.captureStackTrace(err); - self.emit('error', err); - }); - - // Database +Node.prototype._initializeDatabase = function() { + var self = this; this.db.on('ready', function() { log.info('Bitcoin Database Ready'); }); @@ -437,20 +419,32 @@ Node.prototype._initialize = function() { Error.captureStackTrace(err); self.emit('error', err); }); +}; - 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'); + self._syncBitcoind(); + }); + this.chain.on('error', function(err) { + Error.captureStackTrace(err); + self.emit('error', err); }); }; -Node.prototype.getServiceOrder = function(services, keys, stack) { - if(!services) { - services = defaultServices; - } +Node.prototype.getServices = function() { + var defaultServices = { + 'bitcoind': [], + 'db': ['bitcoind'], + 'chain': ['db'] + }; + return defaultServices; +}; + +Node.prototype.getServiceOrder = function(keys, stack) { + + var services = this.getServices(); if(!keys) { keys = Object.keys(services); @@ -461,21 +455,20 @@ Node.prototype.getServiceOrder = function(services, keys, stack) { } for(var i = 0; i < keys.length; i++) { - this.getServiceOrder(services, services[keys[i]], stack); + this.getServiceOrder(services[keys[i]], stack); if(stack.indexOf(keys[i]) === -1) { stack.push(keys[i]); } } - return stack; }; Node.prototype.start = function(callback) { var self = this; - var services = this.getServiceOrder(); + var servicesOrder = this.getServiceOrder(); async.eachSeries( - services, + servicesOrder, function(service, next) { log.info('Starting ' + service); self[service].start(next); diff --git a/test/chain.unit.js b/test/chain.unit.js index ff432419..615c7ee6 100644 --- a/test/chain.unit.js +++ b/test/chain.unit.js @@ -28,6 +28,9 @@ describe('Bitcoin Chain', function() { describe('#start', function() { it('should call the callback when base chain is initialized', function(done) { var chain = new Chain(); + chain.node = {}; + chain.node.bitcoind = {}; + chain.node.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); chain.initialize = function() { chain.emit('initialized'); }; diff --git a/test/node.unit.js b/test/node.unit.js index deb81863..552b3e40 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -227,18 +227,19 @@ describe('Bitcoind Node', function() { it('will get and add block up to the tip height', function(done) { var node = new Node({}); node.Block = Block; - node.bitcoindHeight = 1; var blockBuffer = new Buffer(blockData); var block = Block.fromBuffer(blockBuffer); node.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true) + isSynced: sinon.stub().returns(true), + height: 1 }; node.chain = { tip: { __height: 0, hash: block.prevHash }, + getHashes: sinon.stub().callsArgWith(1, null), saveMetadata: sinon.stub(), emit: sinon.stub(), cache: { @@ -258,9 +259,9 @@ describe('Bitcoind Node', function() { }); it('will exit and emit error with error from bitcoind.getBlock', function(done) { var node = new Node({}); - node.bitcoindHeight = 1; node.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, new Error('test error')) + getBlock: sinon.stub().callsArgWith(1, new Error('test error')), + height: 1 }; node.chain = { tip: { @@ -276,12 +277,12 @@ describe('Bitcoind Node', function() { it('will stop syncing when the node is stopping', function(done) { var node = new Node({}); node.Block = Block; - node.bitcoindHeight = 1; var blockBuffer = new Buffer(blockData); var block = Block.fromBuffer(blockBuffer); node.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true) + isSynced: sinon.stub().returns(true), + height: 1 }; node.chain = { tip: { @@ -433,42 +434,34 @@ describe('Bitcoind Node', function() { describe('#_loadConsensus', function() { var node = new Node({}); - it('should use the genesis specified in the config', function() { - var config = { - genesis: '0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000' - }; - node._loadConsensus(config); + it('will set properties', function() { + node._loadConsensus(); + should.exist(node.Block); should.exist(node.chain); - node.chain.genesis.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'); - }); - it('should use the testnet genesis if testnet is specified', function() { - var config = { - network: 'testnet' - }; - node._loadConsensus(config); - should.exist(node.chain); - node.chain.genesis.hash.should.equal('000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943'); - }); - it('should use the livenet genesis if nothing is specified', function() { - var config = {}; - node._loadConsensus(config); - should.exist(node.chain); - node.chain.genesis.hash.should.equal('000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'); }); + }); describe('#_initialize', function() { - var node = new Node({}); - node.chain = { - on: sinon.spy() - }; - node.Block = 'Block'; - node.bitcoind = { - on: sinon.spy() - }; - node.db = { - on: sinon.spy() - }; + + var node; + + before(function() { + node = new Node({}); + node.chain = { + on: sinon.spy() + }; + node.Block = 'Block'; + node.bitcoind = { + on: sinon.spy() + }; + node._initializeBitcoind = sinon.spy(); + node._initializeDatabase = sinon.spy(); + node._initializeChain = sinon.spy(); + node.db = { + on: sinon.spy() + }; + }); it('should initialize', function(done) { node.once('ready', function() { @@ -486,8 +479,10 @@ describe('Bitcoind Node', function() { node.chain.db.should.equal(node.db); node.chain.db.should.equal(node.db); - // start syncing - node.setSyncStrategy = sinon.spy(); + // event handlers + node._initializeBitcoind.callCount.should.equal(1); + node._initializeDatabase.callCount.should.equal(1); + node._initializeChain.callCount.should.equal(1); }); @@ -497,26 +492,121 @@ describe('Bitcoind Node', function() { err.message.should.equal('error'); done(); }); - node.start = sinon.stub().callsArgWith(0, new Error('error')); - node._initialize(); }); }); - describe('#getServiceOrder', function() { - var services = { - 'chain': ['db'], - 'db': ['daemon', 'p2p'], - 'daemon': [], - 'p2p': [] - }; + describe('#_initalizeBitcoind', function() { + it('will call emit an error from libbitcoind', function(done) { + var node = new Node({}); + node.bitcoind = new EventEmitter(); + node.on('error', function(err) { + should.exist(err); + err.message.should.equal('test error'); + done(); + }); + node._initializeBitcoind(); + node.bitcoind.emit('error', new Error('test error')); + }); + it('will call sync when there is a new tip', function(done) { + var node = new Node({}); + node.bitcoind = new EventEmitter(); + node.bitcoind.syncPercentage = sinon.spy(); + node._syncBitcoind = function() { + node.bitcoind.syncPercentage.callCount.should.equal(1); + done(); + }; + node._initializeBitcoind(); + node.bitcoind.emit('tip', 10); + }); + it('will not call sync when there is a new tip and shutting down', function(done) { + var node = new Node({}); + node.bitcoind = new EventEmitter(); + node._syncBitcoind = sinon.spy(); + node.bitcoind.syncPercentage = sinon.spy(); + node.stopping = true; + node.bitcoind.on('tip', function() { + setImmediate(function() { + node.bitcoind.syncPercentage.callCount.should.equal(0); + node._syncBitcoind.callCount.should.equal(0); + done(); + }); + }); + node._initializeBitcoind(); + node.bitcoind.emit('tip', 10); + }); + }); + + describe('#_initializeDatabase', function() { + it('will log on ready event', function(done) { + var node = new Node({}); + node.db = new EventEmitter(); + sinon.stub(chainlib.log, 'info'); + node.db.on('ready', function() { + setImmediate(function() { + chainlib.log.info.callCount.should.equal(1); + chainlib.log.info.restore(); + done(); + }); + }); + node._initializeDatabase(); + node.db.emit('ready'); + }); + it('will call emit an error from db', function(done) { + var node = new Node({}); + node.db = new EventEmitter(); + node.on('error', function(err) { + should.exist(err); + err.message.should.equal('test error'); + done(); + }); + node._initializeDatabase(); + node.db.emit('error', new Error('test error')); + }); + }); + + describe('#_initializeChain', function() { + it('will call _syncBitcoind on ready', function(done) { + var node = new Node({}); + node._syncBitcoind = sinon.spy(); + node.chain = new EventEmitter(); + node.chain.on('ready', function(err) { + setImmediate(function() { + node._syncBitcoind.callCount.should.equal(1); + done(); + }); + }); + node._initializeChain(); + node.chain.emit('ready'); + }); + it('will emit an error from the chain', function(done) { + var node = new Node({}); + 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({}); - var order = node.getServiceOrder(services); - + node.getServices = function() { + return { + 'chain': ['db'], + 'db': ['daemon', 'p2p'], + 'daemon': [], + 'p2p': [] + }; + }; + var order = node.getServiceOrder(); order.should.deep.equal(['daemon', 'p2p', 'db', 'chain']); }); });