flocore-node/lib/services/bitcoind/index.js

473 lines
12 KiB
JavaScript

'use strict';
var util = require('util');
var bitcore = require('bitcore-lib');
var zmq = require('zmq');
var async = require('async');
var BitcoinRPC = require('bitcoind-rpc');
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var index = require('../../');
var errors = index.errors;
var log = index.log;
var Service = require('../../service');
function Bitcoin(options) {
if (!(this instanceof Bitcoin)) {
return new Bitcoin(options);
}
Service.call(this, options);
this.options = options;
this.subscriptions = {};
this.subscriptions.rawtransaction = [];
this.subscriptions.hashblock = [];
this._initClients();
this._process = options.process || process;
this.on('error', function(err) {
log.error(err.stack);
});
}
util.inherits(Bitcoin, Service);
Bitcoin.dependencies = [];
Bitcoin.prototype._initClients = function() {
var self = this;
this.nodes = [];
this.nodesIndex = 0;
Object.defineProperty(this, 'client', {
get: function() {
var client = self.nodes[self.nodesIndex].client;
self.nodesIndex = (self.nodesIndex + 1) % self.nodes.length;
return client;
},
enumerable: true,
configurable: false
});
};
Bitcoin.prototype.getAPIMethods = function() {
var methods = [
['getBlock', this, this.getBlock, 1]
];
return methods;
};
Bitcoin.prototype.getPublishEvents = function() {
return [
{
name: 'bitcoind/rawtransaction',
scope: this,
subscribe: this.subscribe.bind(this, 'rawtransaction'),
unsubscribe: this.unsubscribe.bind(this, 'rawtransaction')
},
{
name: 'bitcoind/hashblock',
scope: this,
subscribe: this.subscribe.bind(this, 'hashblock'),
unsubscribe: this.unsubscribe.bind(this, 'hashblock')
}
];
};
Bitcoin.prototype.subscribe = function(name, emitter) {
this.subscriptions[name].push(emitter);
log.info(emitter.remoteAddress, 'subscribe:', 'bitcoind/' + name, 'total:', this.subscriptions[name].length);
};
Bitcoin.prototype.unsubscribe = function(name, emitter) {
var index = this.subscriptions[name].indexOf(emitter);
if (index > -1) {
this.subscriptions[name].splice(index, 1);
}
log.info(emitter.remoteAddress, 'unsubscribe:', 'bitcoind/' + name, 'total:', this.subscriptions[name].length);
};
Bitcoin.prototype._tryAllClients = function(func, callback) {
var self = this;
var nodesIndex = this.nodesIndex;
var retry = function(done) {
var client = self.nodes[nodesIndex].client;
nodesIndex = (nodesIndex + 1) % self.nodes.length;
func(client, done);
};
async.retry({times: this.nodes.length, interval: this.tryAllInterval || 1000}, retry, callback);
};
Bitcoin.prototype._wrapRPCError = function(errObj) {
var err = new errors.RPCError(errObj.message);
err.code = errObj.code;
return err;
};
Bitcoin.prototype._initChain = function(callback) {
var self = this;
self.client.getBestBlockHash(function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
self.tiphash = response.result;
self.client.getBlock(response.result, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
self.height = response.result.height;
self.client.getBlockHash(0, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
var blockhash = response.result;
self.getRawBlock(blockhash, function(err, blockBuffer) {
if (err) {
return callback(err);
}
self.genesisBuffer = blockBuffer;
self.emit('ready');
log.info('Bitcoin Daemon Ready');
callback();
});
});
});
});
};
Bitcoin.prototype._zmqBlockHandler = function(node, message) {
var self = this;
self._rapidProtectedUpdateTip(node, message);
self.emit('block', message);
for (var i = 0; i < this.subscriptions.hashblock.length; i++) {
this.subscriptions.hashblock[i].emit('bitcoind/hashblock', message.toString('hex'));
}
};
Bitcoin.prototype._rapidProtectedUpdateTip = function(node, message) {
var self = this;
if (new Date() - self.lastTip > 1000) {
self.lastTip = new Date();
self._updateTip(node, message);
} else {
clearTimeout(self.lastTipTimeout);
self.lastTipTimeout = setTimeout(function() {
self._updateTip(node, message);
}, 1000);
}
};
Bitcoin.prototype._updateTip = function(node, message) {
var self = this;
var hex = message.toString('hex');
if (hex !== self.tiphash) {
self.tiphash = message.toString('hex');
node.client.getBlock(self.tiphash, function(err, response) {
if (err) {
var error = self._wrapRPCError(err);
self.emit('error', error);
} else {
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
}
});
}
};
Bitcoin.prototype._zmqTransactionHandler = function(node, message) {
var self = this;
self.emit('tx', message);
for (var i = 0; i < this.subscriptions.rawtransaction.length; i++) {
this.subscriptions.rawtransaction[i].emit('bitcoind/rawtransaction', message.toString('hex'));
}
};
Bitcoin.prototype._checkSyncedAndSubscribeZmqEvents = function(node) {
var self = this;
var interval;
function checkAndSubscribe(callback) {
node.client.getBestBlockHash(function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
var blockhash = new Buffer(response.result, 'hex');
self.emit('block', blockhash);
self._updateTip(node, blockhash);
node.client.getBlockchainInfo(function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
var progress = response.result.verificationprogress;
if (progress >= self.zmqSubscribeProgress) {
self._subscribeZmqEvents(node);
clearInterval(interval);
callback(null, true);
} else {
callback(null, false);
}
});
});
}
checkAndSubscribe(function(err, synced) {
if (err) {
log.error(err);
}
if (!synced) {
interval = setInterval(function() {
if (self.node.stopping) {
return clearInterval(interval);
}
checkAndSubscribe(function(err) {
if (err) {
log.error(err);
}
});
}, node._tipUpdateInterval || Bitcoin.DEFAULT_TIP_UPDATE_INTERVAL);
}
});
};
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) {
node.zmqSubSocket = zmq.socket('sub');
node.zmqSubSocket.on('connect', function(fd, endPoint) {
log.info('ZMQ connected to:', endPoint);
});
node.zmqSubSocket.on('connect_delay', function(fd, endPoint) {
if (this.zmqDelayWarningMultiplierCouunt++ >= this.zmqDelayWarningMultiplier) {
log.warn('ZMQ connection delay:', endPoint);
this.zmqDelayWarningMultiplierCouunt = 0;
}
});
node.zmqSubSocket.on('disconnect', function(fd, endPoint) {
log.warn('ZMQ disconnect:', endPoint);
});
node.zmqSubSocket.on('monitor_error', function(err) {
log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err);
setTimeout(function() {
node.zmqSubSocket.monitor(500, 0);
}, 5000);
});
node.zmqSubSocket.monitor(100, 0);
node.zmqSubSocket.connect(zmqUrl);
};
Bitcoin.prototype._loadTipFromNode = function(node, callback) {
var self = this;
node.client.getBestBlockHash(function(err, response) {
if (err && err.code === -28) {
log.warn(err.message);
return callback(self._wrapRPCError(err));
} else if (err) {
return callback(self._wrapRPCError(err));
}
node.client.getBlock(response.result, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
callback();
});
});
};
Bitcoin.prototype._connectProcess = function(config, callback) {
var self = this;
var node = {};
var exitShutdown = false;
async.retry({times: 60, interval: self.startRetryInterval}, function(done) {
if (self.node.stopping) {
exitShutdown = true;
return done();
}
node.client = new BitcoinRPC({
protocol: config.rpcprotocol || 'http',
host: config.rpchost || '127.0.0.1',
port: config.rpcport,
user: config.rpcuser,
pass: config.rpcpassword,
rejectUnauthorized: _.isUndefined(config.rpcstrict) ? true : config.rpcstrict
});
self._loadTipFromNode(node, done);
}, function(err) {
if (err) {
return callback(err);
}
if (exitShutdown) {
return callback(new Error('Stopping while trying to connect to bitcoind.'));
}
self._initZmqSubSocket(node, config.zmqpubrawtx);
self._subscribeZmqEvents(node);
callback(null, node);
});
};
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);
}
if (self.nodes.length === 0) {
return callback(new Error('Bitcoin configuration options "spawn" or "connect" are expected'));
}
self._initChain(callback);
});
};
Bitcoin.prototype._getAddressDetailsForOutput = function(output, outputIndex, result, addressStrings) {
if (!output.address) {
return;
}
var address = output.address;
if (addressStrings.indexOf(address) >= 0) {
if (!result.addresses[address]) {
result.addresses[address] = {
inputIndexes: [],
outputIndexes: [outputIndex]
};
} else {
result.addresses[address].outputIndexes.push(outputIndex);
}
result.satoshis += output.satoshis;
}
};
Bitcoin.prototype._maybeGetBlockHash = function(blockArg, callback) {
var self = this;
if (_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) {
self._tryAllClients(function(client, done) {
client.getBlockHash(blockArg, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
done(null, response.result);
});
}, callback);
} else {
callback(null, blockArg);
}
};
Bitcoin.prototype.getRawBlock = function(blockArg, callback) {
var self = this;
function queryBlock(err, blockhash) {
if (err) {
return callback(err);
}
self._tryAllClients(function(client, done) {
self.client.getBlock(blockhash, false, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var buffer = new Buffer(response.result, 'hex');
done(null, buffer);
});
}, callback);
}
self._maybeGetBlockHash(blockArg, queryBlock);
};
Bitcoin.prototype.getBlock = function(blockArg, callback) {
var self = this;
function queryBlock(err, blockhash) {
if (err) {
return callback(err);
}
self._tryAllClients(function(client, done) {
client.getBlock(blockhash, false, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var blockObj = bitcore.Block.fromString(response.result);
done(null, blockObj);
});
}, callback);
}
self._maybeGetBlockHash(blockArg, queryBlock);
};
Bitcoin.prototype.stop = function(callback) {
callback();
};
module.exports = Bitcoin;