diff --git a/lib/node.js b/lib/node.js index 737cd307..be4ec189 100644 --- a/lib/node.js +++ b/lib/node.js @@ -28,7 +28,6 @@ var errors = require('./errors'); * * @param {Object} config - The configuration of the node * @param {Array} config.services - The array of services - * @param {String} config.datadir - The directory for data (e.g. bitcoind datadir) * @param {Number} config.port - The HTTP port for services * @param {Boolean} config.https - Enable https * @param {Object} config.httpsOptions - Options for https @@ -52,8 +51,6 @@ function Node(config) { $.checkArgument(Array.isArray(config.services)); this._unloadedServices = config.services; } - $.checkState(config.datadir, 'Node config expects "datadir"'); - this.datadir = config.datadir; this.port = config.port; this.https = config.https; this.httpsOptions = config.httpsOptions; diff --git a/lib/scaffold/default-base-config.js b/lib/scaffold/default-base-config.js index 6bb0babc..6cc5b316 100644 --- a/lib/scaffold/default-base-config.js +++ b/lib/scaffold/default-base-config.js @@ -15,10 +15,15 @@ function getDefaultBaseConfig(options) { return { path: process.cwd(), config: { - datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'), network: options.network || 'livenet', port: 3001, - services: ['bitcoind', 'db', 'address', 'web'] + services: ['bitcoind', 'web'], + servicesConfig: { + bitcoind: { + datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'), + exec: path.resolve(__dirname, '../../dist/bitcoind') + } + } } }; } diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index 83d36f7a..145b6a5c 100644 --- a/lib/scaffold/default-config.js +++ b/lib/scaffold/default-config.js @@ -24,17 +24,22 @@ function getDefaultConfig(options) { mkdirp.sync(defaultPath); } - var defaultServices = ['bitcoind', 'db', 'address', 'web']; + var defaultServices = ['bitcoind', 'web']; if (options.additionalServices) { defaultServices = defaultServices.concat(options.additionalServices); } if (!fs.existsSync(defaultConfigFile)) { var defaultConfig = { - datadir: path.resolve(defaultPath, './data'), network: 'livenet', port: 3001, - services: defaultServices + services: defaultServices, + servicesConfig: { + bitcoind: { + datadir: path.resolve(defaultPath, './data'), + exec: path.resolve(__dirname, '../../dist/bitcoind') + } + } }; fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2)); } diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index f9ac430a..2f2bcfad 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -201,11 +201,6 @@ function start(options) { } fullConfig.services = start.setupServices(require, servicesPath, options.config); - fullConfig.datadir = path.resolve(options.path, options.config.datadir); - - if (fullConfig.daemon) { - start.spawnChildProcess(fullConfig.datadir, process); - } var node = new BitcoreNode(fullConfig); @@ -237,46 +232,9 @@ function start(options) { } -/** - * This function will fork the passed in process and exit the parent process - * in order to daemonize the process. If there is already a daemon for this pid (process), - * then the function just returns. Stdout and stderr both append to one file, 'bitcore-node.log' - * located in the datadir. - * @param {String} datadir - The data directory where the bitcoin blockchain and config live. - * @param {Object} _process - The process that needs to fork a child and then, itself, exit. - */ -function spawnChildProcess(datadir, _process) { - - if (_process.env.__bitcore_node) { - return _process.pid; - } - - var args = [].concat(_process.argv); - args.shift(); - var script = args.shift(); - var env = _process.env; - var cwd = _process.cwd(); - env.__bitcore_node = true; - - var stderr = fs.openSync(datadir + '/bitcore-node.log', 'a+'); - var stdout = stderr; - - var cp_opt = { - stdio: ['ignore', stdout, stderr], - env: env, - cwd: cwd, - detached: true - }; - - var child = child_process.spawn(_process.execPath, [script].concat(args), cp_opt); - child.unref(); - return _process.exit(); -} - module.exports = start; module.exports.registerExitHandlers = registerExitHandlers; module.exports.exitHandler = exitHandler; module.exports.registerSyncHandlers = registerSyncHandlers; module.exports.setupServices = setupServices; -module.exports.spawnChildProcess = spawnChildProcess; module.exports.cleanShutdown = cleanShutdown; diff --git a/lib/services/bitcoind.js b/lib/services/bitcoind.js index 765a8550..a0298cd6 100644 --- a/lib/services/bitcoind.js +++ b/lib/services/bitcoind.js @@ -6,7 +6,6 @@ var spawn = require('child_process').spawn; var util = require('util'); var mkdirp = require('mkdirp'); var bitcore = require('bitcore-lib'); -var Address = bitcore.Address; var zmq = require('zmq'); var async = require('async'); var LRU = require('lru-cache'); @@ -27,12 +26,11 @@ var Transaction = require('../transaction'); * @param {Node} options.node - A reference to the node */ function Bitcoin(options) { + /* jshint maxstatements: 20 */ if (!(this instanceof Bitcoin)) { return new Bitcoin(options); } - this._reindex = false; - this._reindexWait = 1000; Service.call(this, options); // caches valid until there is a new block @@ -41,15 +39,32 @@ function Bitcoin(options) { this.balanceCache = LRU(50000); this.summaryCache = LRU(50000); - // caches valid indefinetly + // caches valid indefinitely this.transactionCache = LRU(100000); this.transactionInfoCache = LRU(100000); this.blockCache = LRU(144); this.blockHeaderCache = LRU(288); + this.zmqKnownTransactions = LRU(50); + + this.options = options; + + // bitcoind child process + this.spawn = false; + + // available bitcoind nodes + this.nodes = []; + this.nodesIndex = 0; + Object.defineProperty(this, 'client', { + get: function() { + var client = this.nodes[this.nodesIndex].client; + this.nodesIndex = (this.nodesIndex + 1) % this.nodes.length; + return client; + }, + enumerable: true, + configurable: false + }); - $.checkState(this.node.datadir, 'Node is missing datadir property'); } - util.inherits(Bitcoin, Service); Bitcoin.dependencies = []; @@ -77,15 +92,24 @@ Bitcoin.prototype.getAPIMethods = function() { return methods; }; -Bitcoin.prototype._loadConfiguration = function() { +Bitcoin.prototype._loadSpawnConfiguration = function(node) { /* jshint maxstatements: 25 */ - $.checkArgument(this.node.datadir, 'Please specify "datadir" in configuration options'); - var configPath = this.node.datadir + '/bitcoin.conf'; - this.configuration = {}; + $.checkArgument(this.options.spawn, 'Please specify "spawn" in bitcoind config options'); + $.checkArgument(this.options.spawn.datadir, 'Please specify "spawn.datadir" in bitcoind config options'); + $.checkArgument(this.options.spawn.exec, 'Please specify "spawn.exec" in bitcoind config options'); - if (!fs.existsSync(this.node.datadir)) { - mkdirp.sync(this.node.datadir); + var spawnOptions = this.options.spawn; + var configPath = spawnOptions.datadir + '/bitcoin.conf'; + + this.spawn = {}; + this.spawn.datadir = this.options.spawn.datadir; + this.spawn.exec = this.options.spawn.exec; + this.spawn.configPath = configPath; + this.spawn.config = {}; + + if (!fs.existsSync(spawnOptions.datadir)) { + mkdirp.sync(spawnOptions.datadir); } var file = fs.readFileSync(configPath); @@ -100,48 +124,50 @@ Bitcoin.prototype._loadConfiguration = function() { } else { value = option[1]; } - this.configuration[option[0]] = value; + this.spawn.config[option[0]] = value; } } + var spawnConfig = this.spawn.config; + $.checkState( - this.configuration.txindex && this.configuration.txindex === 1, + spawnConfig.txindex && spawnConfig.txindex === 1, '"txindex" option is required in order to use transaction query features of bitcore-node. ' + 'Please add "txindex=1" to your configuration and reindex an existing database if ' + 'necessary with reindex=1' ); $.checkState( - this.configuration.addressindex && this.configuration.addressindex === 1, + spawnConfig.addressindex && spawnConfig.addressindex === 1, '"addressindex" option is required in order to use address query features of bitcore-node. ' + 'Please add "addressindex=1" to your configuration and reindex an existing database if ' + 'necessary with reindex=1' ); $.checkState( - this.configuration.server && this.configuration.server === 1, + spawnConfig.server && spawnConfig.server === 1, '"server" option is required to communicate to bitcoind from bitcore. ' + 'Please add "server=1" to your configuration and restart' ); $.checkState( - this.configuration.zmqpubrawtx, + spawnConfig.zmqpubrawtx, '"zmqpubrawtx" option is required to get event updates from bitcoind. ' + 'Please add "zmqpubrawtx=tcp://127.0.0.1:" to your configuration and restart' ); $.checkState( - this.configuration.zmqpubhashblock, + spawnConfig.zmqpubhashblock, '"zmqpubhashblock" option is required to get event updates from bitcoind. ' + 'Please add "zmqpubhashblock=tcp://127.0.0.1:" to your configuration and restart' ); - if (this.configuration.reindex && this.configuration.reindex === 1) { + if (spawnConfig.reindex && spawnConfig.reindex === 1) { log.warn('Reindex option is currently enabled. This means that bitcoind is undergoing a reindex. ' + - 'The reindex flag will start the index from beginning every time the node is started, so it ' + - 'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' + - 'of bitcore-node services will start.'); - this._reindex = true; + 'The reindex flag will start the index from beginning every time the node is started, so it ' + + 'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' + + 'of bitcore-node services will start.'); + node._reindex = true; } }; @@ -153,67 +179,37 @@ Bitcoin.prototype._resetCaches = function() { this.summaryCache.reset(); }; -Bitcoin.prototype._registerEventHandlers = function() { +Bitcoin.prototype._initChain = function(callback) { var self = this; - this.zmqSubSocket.subscribe('hashblock'); - this.zmqSubSocket.subscribe('rawtx'); - - this.zmqSubSocket.on('message', function(topic, message) { - var topicString = topic.toString('utf8'); - if (topicString === 'rawtx') { - self.emit('tx', message); - } else if (topicString === 'hashblock') { - self._resetCaches(); - self.tiphash = message.toString('hex'); - self.client.getBlock(self.tiphash, function(err, response) { - if (err) { - return log.error(err); - } - self.height = response.result.height; - $.checkState(self.height >= 0); - self.emit('tip', self.height); - }); - - if(!self.node.stopping) { - self.syncPercentage(function(err, percentage) { - if (err) { - return log.error(err); - } - log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2)); - }); - } - } - }); - -}; - -Bitcoin.prototype._onReady = function(result, callback) { - var self = this; - - self._registerEventHandlers(); - - self.client.getInfo(function(err, response) { + self.client.getBestBlockHash(function(err, response) { if (err) { return callback(err); } - self.height = response.result.blocks; - self.client.getBlockHash(0, function(err, response) { + self.client.getBlock(response.result, function(err, response) { if (err) { return callback(err); } - var blockhash = response.result; - self.getBlock(blockhash, function(err, block) { + + self.height = response.result.height; + + self.client.getBlockHash(0, function(err, response) { if (err) { return callback(err); } - self.tiphash = block.hash; - self.genesisBuffer = block.toBuffer(); - self.emit('ready', result); - log.info('Bitcoin Daemon Ready'); - callback(); + var blockhash = response.result; + self.getBlock(blockhash, function(err, block) { + if (err) { + return callback(err); + } + self.genesisBuffer = block.toBuffer(); + self.emit('ready'); + log.info('Bitcoin Daemon Ready'); + callback(); + }); }); + }); }); }; @@ -229,27 +225,139 @@ Bitcoin.prototype._getNetworkOption = function() { return networkOption; }; -/** - * Called by Node to start the service - * @param {Function} callback - */ -Bitcoin.prototype.start = function(callback) { +Bitcoin.prototype._zmqBlockHandler = function(node, message) { + var self = this; + var hex = message.toString('hex'); + if (hex !== self.tiphash) { + self._resetCaches(); + self.tiphash = message.toString('hex'); + node.client.getBlock(self.tiphash, function(err, response) { + if (err) { + return log.error(err); + } + self.height = response.result.height; + $.checkState(self.height >= 0); + self.emit('tip', self.height); + }); + + if(!self.node.stopping) { + self.syncPercentage(function(err, percentage) { + if (err) { + return log.error(err); + } + log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2)); + }); + } + } +}; + +Bitcoin.prototype._zmqTransactionHandler = function(node, message) { + var self = this; + var id = message.toString('binary'); + if (!self.zmqKnownTransactions[id]) { + self.zmqKnownTransactions[id] = true; + self.emit('tx', message); + } +}; + +Bitcoin.prototype._subscribeZmqEvents = function(node) { + var self = this; + node.zmqSubSocket.subscribe('hashblock'); + node.zmqSubSocket.subscribe('rawtx'); + node.zmqSubSocket.on('message', function(topic, message) { + var topicString = topic.toString('utf8'); + if (topicString === 'rawtx') { + self._zmqTransactionHandler(node, message); + } else if (topicString === 'hashblock') { + self._zmqBlockHandler(node, message); + } + }); +}; + +Bitcoin.prototype._initZmqSubSocket = function(node, zmqUrl) { + var self = this; + node.zmqSubSocket = zmq.socket('sub'); + + node.zmqSubSocket.on('monitor_error', function(err) { + log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err); + setTimeout(function() { + self.zmqSubSocket.monitor(500, 0); + }, 5000); + }); + + node.zmqSubSocket.monitor(500, 0); + node.zmqSubSocket.connect(zmqUrl); +}; + +Bitcoin.prototype._checkReindex = function(node, callback) { + var self = this; + if (node._reindex) { + var interval = setInterval(function() { + node.client.syncPercentage(function(err, percentSynced) { + if (err) { + return log.error(err); + } + log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2)); + if (Math.round(percentSynced) >= 100) { + node._reindex = false; + self._subscribeZmqEvents(node); + callback(); + clearInterval(interval); + } + }); + }, self._reindexWait); + + } else { + self._subscribeZmqEvents(node); + callback(); + } +}; + +Bitcoin.prototype._loadTipFromNode = function(node, callback) { + var self = this; + node.client.getBestBlockHash(function(err, response) { + if (err) { + if (!(err instanceof Error)) { + log.warn(err.message); + } + return callback(new Error('Could not connect to bitcoind RPC')); + } + node.client.getBlock(response.result, function(err, response) { + if (err) { + return done(err); + } + self.height = response.result.height; + $.checkState(self.height >= 0); + self.emit('tip', self.height); + callback(); + }); + }); +}; + +Bitcoin.prototype._spawnChildProcess = function(callback) { var self = this; - self._loadConfiguration(); + var node = {}; + node._reindex = false; + node._reindexWait = 1000; + + try { + self._loadSpawnConfiguration(node); + } catch(e) { + return callback(e); + } var options = [ - '--conf=' + path.resolve(this.node.datadir, './bitcoin.conf'), - '--datadir=' + this.node.datadir, + '--conf=' + path.resolve(this.spawn.configPath), + '--datadir=' + this.spawn.datadir, ]; if (self._getNetworkOption()) { options.push(self._getNetworkOption()); } + self.spawn.process = spawn(this.spawn.exec, options, {stdio: 'inherit'}); - self.process = spawn('bitcoind', options, {stdio: 'inherit'}); - - self.process.on('error', function(err) { + self.spawn.process.on('error', function(err) { log.error(err); }); @@ -258,67 +366,104 @@ Bitcoin.prototype.start = function(callback) { return done(new Error('Stopping while trying to connect to bitcoind.')); } - self.client = new BitcoinRPC({ + node.client = new BitcoinRPC({ protocol: 'http', host: '127.0.0.1', - port: self.configuration.rpcport, - user: self.configuration.rpcuser, - pass: self.configuration.rpcpassword + port: self.spawn.config.rpcport, + user: self.spawn.config.rpcuser, + pass: self.spawn.config.rpcpassword }); - self.client.getBestBlockHash(function(err, response) { - if (err) { - if (!(err instanceof Error)) { - log.warn(err.message); - } - return done(new Error('Could not connect to bitcoind RPC')); - } - self.client.getBlock(response.result, function(err, response) { - if (err) { - return done(err); - } - self.height = response.result.height; - $.checkState(self.height >= 0); - self.emit('tip', self.height); - done(); - }); - }); + self._loadTipFromNode(node, done); - }, function ready(err, result) { + }, function(err) { if (err) { return callback(err); } - self.zmqSubSocket = zmq.socket('sub'); + self._initZmqSubSocket(node, self.spawn.config.zmqpubrawtx); - self.zmqSubSocket.on('monitor_error', function(err) { - log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err); - setTimeout(function() { - self.zmqSubSocket.monitor(500, 0); - }, 5000); + self._checkReindex(node, function() { + if (err) { + return callback(err); + } + callback(null, node); }); - self.zmqSubSocket.monitor(500, 0); - self.zmqSubSocket.connect(self.configuration.zmqpubrawtx); + }); +}; - if (self._reindex) { - var interval = setInterval(function() { - self.syncPercentage(function(err, percentSynced) { - if (err) { - return log.error(err); - } - log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2)); - if (Math.round(percentSynced) >= 100) { - self._reindex = false; - self._onReady(result, callback); - clearInterval(interval); - } - }); - }, self._reindexWait); +Bitcoin.prototype._connectProcess = function(config, callback) { + var self = this; + var node = {}; - } else { - self._onReady(result, callback); + async.retry({times: 60, interval: 5000}, function(done) { + if (self.node.stopping) { + return done(new Error('Stopping while trying to connect to bitcoind.')); } + + node.client = new BitcoinRPC({ + protocol: config.rpcprotocol || 'http', + host: config.rpchost || '127.0.0.1', + port: config.rpcport, + user: config.rpcuser, + pass: config.rpcpassword + }); + + self._loadTipFromNode(node, done); + + }, function(err) { + if (err) { + return callback(err); + } + + self._initZmqSubSocket(node, config.zmqpubrawtx); + + callback(null, node); + }); +}; + +/** + * Called by Node to start the service + * @param {Function} callback + */ +Bitcoin.prototype.start = function(callback) { + var self = this; + + async.series([ + function(next) { + if (self.options.spawn) { + self._spawnChildProcess(function(err, node) { + if (err) { + return next(err); + } + self.nodes.push(node); + next(); + }); + } else { + next(); + } + }, + function(next) { + if (self.options.connect) { + async.map(self.options.connect, self._connectProcess.bind(self), function(err, nodes) { + if (err) { + return callback(err); + } + for(var i = 0; i < nodes.length; i++) { + self.nodes.push(nodes[i]); + } + next(); + }); + } else { + next(); + } + } + ], function(err) { + if (err) { + return callback(err); + } + self._initChain(callback); }); }; @@ -950,15 +1095,15 @@ Bitcoin.prototype.getInfo = function(callback) { * @param {Function} callback */ Bitcoin.prototype.stop = function(callback) { - if (this.process) { - this.process.once('exit', function(err, status) { + if (this.spawn && this.spawn.process) { + this.spawn.process.once('exit', function(err, status) { if (err) { return callback(err); } else { return callback(); } }); - this.process.kill('SIGHUP'); + this.spawn.process.kill('SIGHUP'); } else { callback(); } diff --git a/package.json b/package.json index 63dc3a15..56627575 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ ], "dependencies": { "async": "^1.3.0", - "bindings": "^1.2.1", "bitcoind-rpc": "^0.3.0", "bitcore-lib": "^0.13.13", "body-parser": "^1.13.3", @@ -50,7 +49,6 @@ "express": "^4.13.3", "liftoff": "^2.2.0", "lru-cache": "^4.0.1", - "memdown": "^1.0.0", "mkdirp": "0.5.0", "npm": "^2.14.1", "semver": "^5.0.1",