bitcore-p2p
Version:
Interface to the bitcoin P2P network for bitcore
386 lines (341 loc) • 10.4 kB
JavaScript
'use strict';
var dns = require('dns');
var EventEmitter = require('events').EventEmitter;
var bitcore = require('bitcore-lib');
var sha256 = bitcore.crypto.Hash.sha256;
var Peer = require('./peer');
var Networks = bitcore.Networks;
var util = require('util');
var net = require('net');
function now() {
return Math.floor(new Date().getTime() / 1000);
}
/**
* A pool is a collection of Peers. A pool will discover peers from DNS seeds, and
* collect information about new peers in the network. When a peer disconnects the pool
* will connect to others that are available to maintain a max number of
* ongoing peer connections. Peer events are relayed to the pool.
*
* @example
* ```javascript
*
* var pool = new Pool({network: Networks.livenet});
* pool.on('peerinv', function(peer, message) {
* // do something with the inventory announcement
* });
* pool.connect();
* ```
*
* @param {Object=} options
* @param {Network=} options.network - The network configuration
* @param {Boolean=} options.listenAddr - Prevent new peers being added from addr messages
* @param {Boolean=} options.dnsSeed - Prevent seeding with DNS discovered known peers
* @param {Boolean=} options.relay - Prevent inventory announcements until a filter is loaded
* @param {Number=} options.maxSize - The max number of peers
* @returns {Pool}
* @constructor
*/
function Pool(options) {
/* jshint maxcomplexity: 10 */
/* jshint maxstatements: 20 */
var self = this;
options = options || {};
this.keepalive = false;
this._connectedPeers = {};
this._addrs = [];
if (options.network && options.network.constructor.name !== 'Network') {
options.network = Networks.get(options.network);
}
this.listenAddr = options.listenAddr !== false;
this.dnsSeed = options.dnsSeed !== false;
this.maxSize = options.maxSize || Pool.MaxConnectedPeers;
this.messages = options.messages;
this.network = options.network || Networks.defaultNetwork;
this.relay = options.relay === false ? false : true;
if (options.addrs) {
for(var i = 0; i < options.addrs.length; i++) {
this._addAddr(options.addrs[i]);
}
}
if (this.listenAddr) {
this.on('peeraddr', function peerAddrEvent(peer, message) {
var addrs = message.addresses;
var length = addrs.length;
for (var i = 0; i < length; i++) {
var addr = addrs[i];
var future = new Date().getTime() + (10 * 60 * 1000);
if (addr.time.getTime() <= 100000000000 || addr.time.getTime() > future) {
// In case of an invalid time, assume "5 days ago"
var past = new Date(new Date().getTime() - 5 * 24 * 60 * 60 * 1000);
addr.time = past;
}
this._addAddr(addr);
}
});
}
this.on('seed', function seedEvent(ips) {
ips.forEach(function(ip) {
self._addAddr({
ip: {
v4: ip
}
});
});
if (self.keepalive) {
self._fillConnections();
}
});
this.on('peerdisconnect', function peerDisconnectEvent(peer, addr) {
self._deprioritizeAddr(addr);
self._removeConnectedPeer(addr);
if (self.keepalive) {
self._fillConnections();
}
});
return this;
}
util.inherits(Pool, EventEmitter);
Pool.MaxConnectedPeers = 8;
Pool.RetrySeconds = 30;
Pool.PeerEvents = ['version', 'inv', 'getdata', 'ping', 'pong', 'addr',
'getaddr', 'verack', 'reject', 'alert', 'headers', 'block', 'merkleblock',
'tx', 'getblocks', 'getheaders', 'error', 'filterload', 'filteradd',
'filterclear'
];
/**
* Will initiate connection to peers, if available peers have been added to
* the pool, it will connect to those, otherwise will use DNS seeds to find
* peers to connect. When a peer disconnects it will add another.
*/
Pool.prototype.connect = function connect() {
this.keepalive = true;
var self = this;
if (this.dnsSeed) {
self._addAddrsFromSeeds();
} else {
self._fillConnections();
}
return this;
};
/**
* Will disconnect all peers that are connected.
*/
Pool.prototype.disconnect = function disconnect() {
this.keepalive = false;
for (var i in this._connectedPeers) {
this._connectedPeers[i].disconnect();
}
return this;
};
/**
* @returns {Number} The number of peers currently connected.
*/
Pool.prototype.numberConnected = function numberConnected() {
return Object.keys(this._connectedPeers).length;
};
/**
* Will fill the connected peers to the maximum amount.
*/
Pool.prototype._fillConnections = function _fillConnections() {
var length = this._addrs.length;
for (var i = 0; i < length; i++) {
if (this.numberConnected() >= this.maxSize) {
break;
}
var addr = this._addrs[i];
if (!addr.retryTime || now() > addr.retryTime) {
this._connectPeer(addr);
}
}
return this;
};
/**
* Will remove a peer from the list of connected peers.
* @param {Object} addr - An addr from the list of addrs
*/
Pool.prototype._removeConnectedPeer = function _removeConnectedPeer(addr) {
if (this._connectedPeers[addr.hash].status !== Peer.STATUS.DISCONNECTED) {
this._connectedPeers[addr.hash].disconnect();
} else {
delete this._connectedPeers[addr.hash];
}
return this;
};
/**
* Will connect a peer and add to the list of connected peers.
* @param {Object} addr - An addr from the list of addrs
*/
Pool.prototype._connectPeer = function _connectPeer(addr) {
var self = this;
if (!this._connectedPeers[addr.hash]) {
var port = addr.port || self.network.port;
var ip = addr.ip.v4 || addr.ip.v6;
var peer = new Peer({
host: ip,
port: port,
messages: self.messages,
network: this.network,
relay: self.relay
});
peer.on('connect', function peerConnect() {
self.emit('peerconnect', peer, addr);
});
self._addPeerEventHandlers(peer, addr);
peer.connect();
self._connectedPeers[addr.hash] = peer;
}
return this;
};
/**
* Adds a peer with a connected socket to the _connectedPeers object, and
* initializes the associated event handlers.
* @param {Socket} - socket - A new connected socket
* @param {Object} - addr - The associated addr object for the peer
*/
Pool.prototype._addConnectedPeer = function _addConnectedPeer(socket, addr) {
var self = this;
if (!this._connectedPeers[addr.hash]) {
var peer = new Peer({
socket: socket,
network: this.network,
messages: self.messages
});
self._addPeerEventHandlers(peer, addr);
self._connectedPeers[addr.hash] = peer;
self.emit('peerconnect', peer, addr);
}
return this;
};
/**
* Will add disconnect and ready events for a peer and intialize
* handlers for relay peer message events.
*/
Pool.prototype._addPeerEventHandlers = function(peer, addr) {
var self = this;
peer.on('disconnect', function peerDisconnect() {
self.emit('peerdisconnect', peer, addr);
});
peer.on('ready', function peerReady() {
self.emit('peerready', peer, addr);
});
Pool.PeerEvents.forEach(function addPeerEvents(event) {
peer.on(event, function peerEvent(message) {
self.emit('peer' + event, peer, message);
});
});
};
/**
* Will deprioritize an addr in the list of addrs by moving it to the end
* of the array, and setting a retryTime
* @param {Object} addr - An addr from the list of addrs
*/
Pool.prototype._deprioritizeAddr = function _deprioritizeAddr(addr) {
for (var i = 0; i < this._addrs.length; i++) {
if (this._addrs[i].hash === addr.hash) {
var middle = this._addrs[i];
middle.retryTime = now() + Pool.RetrySeconds;
var beginning = this._addrs.splice(0, i);
var end = this._addrs.splice(i + 1, this._addrs.length);
var combined = beginning.concat(end);
this._addrs = combined.concat([middle]);
}
}
return this;
};
/**
* Will add an addr to the beginning of the addrs array
* @param {Object}
*/
Pool.prototype._addAddr = function _addAddr(addr) {
// Use default port if not specified
addr.port = addr.port || this.network.port;
// make a unique key
addr.hash = sha256(new Buffer(addr.ip.v6 + addr.ip.v4 + addr.port)).toString('hex');
var length = this._addrs.length;
var exists = false;
for (var i = 0; i < length; i++) {
if (this._addrs[i].hash === addr.hash) {
exists = true;
}
}
if (!exists) {
this._addrs.unshift(addr);
}
return addr;
};
/**
* Will add addrs to the list of addrs from a DNS seed
* @param {String} seed - A domain name to resolve known peers
* @param {Function} done
*/
Pool.prototype._addAddrsFromSeed = function _addAddrsFromSeed(seed) {
var self = this;
dns.resolve(seed, function(err, ips) {
if (err) {
self.emit('seederror', err);
return;
}
if (!ips || !ips.length) {
self.emit('seederror', new Error('No IPs found from seed lookup.'));
return;
}
// announce to pool
self.emit('seed', ips);
});
return this;
};
/**
* Will add addrs to the list of addrs from network DNS seeds
* @param {Function} done
*/
Pool.prototype._addAddrsFromSeeds = function _addAddrsFromSeeds() {
var self = this;
var seeds = this.network.dnsSeeds;
seeds.forEach(function(seed) {
self._addAddrsFromSeed(seed);
});
return this;
};
/**
* @returns {String} A string formatted for the console
*/
Pool.prototype.inspect = function inspect() {
return '<Pool network: ' +
this.network + ', connected: ' +
this.numberConnected() + ', available: ' +
this._addrs.length + '>';
};
/**
* Will send a message to all of the peers in the pool.
* @param {Message} message - An instance of the message to send
*/
Pool.prototype.sendMessage = function(message) {
// broadcast to peers
for(var key in this._connectedPeers) {
var peer = this._connectedPeers[key];
peer.sendMessage(message);
}
};
/**
* Will enable a listener for peer connections, when a peer connects
* it will be added to the pool.
*/
Pool.prototype.listen = function() {
var self = this;
// Create server
this.server = net.createServer(function(socket) {
var addr = {
ip: {}
};
if(net.isIPv6(socket.remoteAddress)) {
addr.ip.v6 = socket.remoteAddress;
} else {
addr.ip.v4 = socket.remoteAddress;
}
addr.port = socket.remotePort;
addr = self._addAddr(addr);
self._addConnectedPeer(socket, addr);
});
this.server.listen(this.network.port);
};
module.exports = Pool;