diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 51b9f816..1c882875 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -407,7 +407,7 @@ RPC.prototype.addnode = co(function* addnode(args) { break; case 'onetry': if (!this.pool.peers.get(addr.hostname)) { - peer = this.pool.createPeer(addr.port, addr.host); + peer = this.pool.createPeer(addr); this.pool.peers.add(peer); } break; @@ -499,6 +499,7 @@ RPC.prototype.getnettotals = co(function* getnettotals(args) { RPC.prototype.getpeerinfo = co(function* getpeerinfo(args) { var peers = []; + var id = 0; var peer; if (args.help || args.length !== 0) @@ -506,7 +507,7 @@ RPC.prototype.getpeerinfo = co(function* getpeerinfo(args) { for (peer = this.pool.peers.head(); peer; peer = peer.next) { peers.push({ - id: peer.id, + id: id++, addr: peer.hostname, addrlocal: peer.hostname, relaytxes: peer.outbound, @@ -560,7 +561,7 @@ RPC.prototype.setban = co(function* setban(args) { case 'add': peer = this.pool.peers.get(addr.hostname); if (peer) { - this.pool.ban(peer); + peer.ban(); break; } this.pool.hosts.ban(addr.host); @@ -588,7 +589,7 @@ RPC.prototype.listbanned = co(function* listbanned(args) { time = this.pool.hosts.banned[host]; banned.push({ address: host, - banned_until: time + constants.BAN_TIME, + banned_until: time + this.pool.hosts.banTime, ban_created: time, ban_reason: '' }); @@ -601,7 +602,7 @@ RPC.prototype.clearbanned = co(function* clearbanned(args) { if (args.help || args.length !== 0) throw new RPCError('clearbanned'); - this.pool.hosts.clear(); + this.pool.hosts.clearBanned(); return null; }); diff --git a/lib/net/peer.js b/lib/net/peer.js index 6a029bc3..9adf6e42 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -9,7 +9,6 @@ var assert = require('assert'); var EventEmitter = require('events').EventEmitter; -var tcp = require('./tcp'); var util = require('../utils/util'); var co = require('../utils/co'); var Parser = require('./parser'); @@ -26,7 +25,7 @@ var Block = require('../primitives/block'); var TX = require('../primitives/tx'); var errors = require('../btc/errors'); var List = require('../utils/list'); -var IP = require('../utils/ip'); +var NetAddress = require('../primitives/netaddress'); var invTypes = constants.inv; var packetTypes = packets.types; var VerifyResult = errors.VerifyResult; @@ -43,7 +42,6 @@ var VerifyResult = errors.VerifyResult; * @property {String} host * @property {Number} port * @property {String} hostname - * @property {Number} port * @property {Parser} parser * @property {Framer} framer * @property {Chain} chain @@ -68,7 +66,6 @@ var VerifyResult = errors.VerifyResult; * @property {Number} lastPing - Timestamp for last `ping` * sent (unix time). * @property {Number} minPing - Lowest ping time seen. - * @property {String} id - Peer's uid. * @property {Number} banScore * @emits Peer#ack */ @@ -86,16 +83,12 @@ function Peer(pool) { this.mempool = this.pool.mempool; this.network = this.chain.network; this.locker = new Locker(); - this.createSocket = this.options.createSocket; this.next = null; this.prev = null; - this.id = Peer.uid++; this.socket = null; this.outbound = false; - this.host = '0.0.0.0'; - this.port = 0; - this.hostname = '0.0.0.0:0'; + this.address = new NetAddress(); this.destroyed = false; this.ack = false; this.connected = false; @@ -123,6 +116,7 @@ function Peer(pool) { this.waitingTX = 0; this.syncSent = false; this.sentAddr = false; + this.sentGetAddr = false; this.challenge = null; this.lastPong = -1; this.lastPing = -1; @@ -164,13 +158,17 @@ function Peer(pool) { util.inherits(Peer, EventEmitter); -/** - * Globally incremented unique id. - * @private - * @type {Number} - */ +Peer.prototype.__defineGetter__('host', function() { + return this.address.host; +}); -Peer.uid = 0; +Peer.prototype.__defineGetter__('port', function() { + return this.address.port; +}); + +Peer.prototype.__defineGetter__('hostname', function() { + return this.address.hostname; +}); /** * Begin peer initialization. @@ -206,20 +204,6 @@ Peer.prototype._init = function init() { } }; -/** - * Set peer host, port, and hostname. - * @param {String} host - * @param {Number} port - */ - -Peer.prototype.setHost = function setHost(host, port) { - assert(typeof host === 'string'); - assert(typeof port === 'number'); - this.host = host; - this.port = port; - this.hostname = IP.hostname(host, port); -}; - /** * Bind to socket. * @param {net.Socket} socket @@ -229,25 +213,19 @@ Peer.prototype.bind = function bind(socket) { var self = this; assert(!this.socket); + this.socket = socket; + this.socket.once('connect', function() { + self.logger.info('Connected to %s.', self.hostname); + }); + this.socket.once('error', function(err) { + if (!self.connected) + return; + self.error(err); self.destroy(); - - switch (err.code) { - case 'ECONNREFUSED': - case 'EHOSTUNREACH': - case 'ENETUNREACH': - case 'ENOTFOUND': - case 'ECONNRESET': - self.ignore(); - break; - default: - if (!self.connected) - self.ignore(); - break; - } }); this.socket.once('close', function() { @@ -270,74 +248,48 @@ Peer.prototype.bind = function bind(socket) { /** * Accept an inbound socket. * @param {net.Socket} socket + * @returns {net.Socket} */ Peer.prototype.accept = function accept(socket) { - var host = IP.normalize(socket.remoteAddress); - var port = socket.remotePort; - assert(!this.socket); - this.bind(socket); - this.setHost(host, port); + this.address = NetAddress.fromSocket(socket, this.network); + this.address.services = 0; this.ts = util.now(); this.outbound = false; this.connected = true; + + this.bind(socket); }; /** * Create the socket and begin connecting. This method * will use `options.createSocket` if provided. - * @param {Number} port - * @param {String} host + * @param {NetAddress} addr * @returns {net.Socket} */ -Peer.prototype.connect = function connect(port, host) { - var self = this; +Peer.prototype.connect = function connect(addr) { var proxy = this.pool.proxyServer; var socket; assert(!this.socket); - if (this.createSocket) - socket = this.createSocket(port, host, proxy); - else - socket = tcp.connect(port, host, proxy); + socket = this.pool.createSocket(addr.port, addr.host, proxy); - this.bind(socket); - this.setHost(host, port); + this.address = addr; + this.ts = util.now(); this.outbound = true; this.connected = false; + this.bind(socket); + this.logger.debug('Connecting to %s.', this.hostname); - socket.once('connect', function() { - self.logger.info('Connected to %s.', self.hostname); - }); + return socket; }; -/** - * Open and perform initial handshake. - * @returns {Promise} - */ - -Peer.prototype.open = co(function* open() { - yield this.initConnect(); - yield this.initStall(); - yield this.initBIP151(); - yield this.initBIP150(); - yield this.initVerack(); - yield this.finalize(); - - if (this.destroyed) - throw new Error('Peer was destroyed before handshake.'); - - // Finally we can let the pool know - // that this peer is ready to go. - this.emit('open'); -}); - /** * Open and perform initial handshake (without rejection). * @returns {Promise} @@ -346,12 +298,58 @@ Peer.prototype.open = co(function* open() { Peer.prototype.tryOpen = co(function* tryOpen() { try { yield this.open(); + } catch (e) { + ; + } +}); + +/** + * Open and perform initial handshake. + * @returns {Promise} + */ + +Peer.prototype.open = co(function* open() { + try { + yield this._open(); } catch (e) { this.error(e); this.destroy(); + throw e; } }); +/** + * Open and perform initial handshake. + * @returns {Promise} + */ + +Peer.prototype._open = co(function* open() { + // Mark address attempt. + this.markAttempt(); + + // Connect to peer. + yield this.initConnect(); + yield this.initStall(); + + // Mark address success. + this.markSuccess(); + + // Handshake. + yield this.initBIP151(); + yield this.initBIP150(); + yield this.initVersion(); + yield this.finalize(); + + assert(!this.destroyed); + + // Mark address ack. + this.markAck(); + + // Finally we can let the pool know + // that this peer is ready to go. + this.emit('open'); +}); + /** * Wait for connection. * @private @@ -366,23 +364,33 @@ Peer.prototype.initConnect = function initConnect() { } return new Promise(function(resolve, reject) { + function cleanup() { + if (self.connectTimeout != null) { + clearTimeout(self.connectTimeout); + self.connectTimeout = null; + } + self.socket.removeListener('error', onError); + } + + function onError(err) { + cleanup(); + reject(err); + } + self.socket.once('connect', function() { self.ts = util.now(); self.connected = true; self.emit('connect'); - if (self.connectTimeout != null) { - clearTimeout(self.connectTimeout); - self.connectTimeout = null; - } - + cleanup(); resolve(); }); + self.socket.once('error', onError); + self.connectTimeout = setTimeout(function() { self.connectTimeout = null; reject(new Error('Connection timed out.')); - self.ignore(); }, 10000); }); }; @@ -424,6 +432,9 @@ Peer.prototype.initBIP151 = co(function* initBIP151() { this.error(err); } + if (this.destroyed) + throw new Error('Peer was destroyed during BIP151 handshake.'); + assert(this.bip151.completed); if (this.bip151.handshake) { @@ -456,6 +467,9 @@ Peer.prototype.initBIP150 = co(function* initBIP150() { yield this.bip150.wait(3000); + if (this.destroyed) + throw new Error('Peer was destroyed during BIP150 handshake.'); + assert(this.bip150.completed); if (this.bip150.auth) { @@ -470,12 +484,12 @@ Peer.prototype.initBIP150 = co(function* initBIP150() { * @private */ -Peer.prototype.initVerack = co(function* initVerack() { +Peer.prototype.initVersion = co(function* initVersion() { // Say hello. this.sendVersion(); // Advertise our address. - if (this.pool.address.host !== '0.0.0.0' + if (!this.pool.address.isNull() && !this.options.selfish && this.pool.server) { this.send(new packets.AddrPacket([this.pool.address])); @@ -483,6 +497,9 @@ Peer.prototype.initVerack = co(function* initVerack() { yield this.request('verack'); + if (this.destroyed) + throw new Error('Peer was destroyed during version handshake.'); + // Wait for _their_ version. if (!this.version) { this.logger.debug( @@ -491,9 +508,52 @@ Peer.prototype.initVerack = co(function* initVerack() { yield this.request('version'); + if (this.destroyed) + throw new Error('Peer was destroyed during version handshake.'); + assert(this.version); } + if (!this.network.selfConnect) { + if (util.equal(this.version.nonce, this.pool.localNonce)) + throw new Error('We connected to ourself. Oops.'); + } + + if (this.version.version < constants.MIN_VERSION) + throw new Error('Peer does not support required protocol version.'); + + if (this.options.witness) { + this.haveWitness = this.version.hasWitness(); + if (!this.haveWitness && this.network.oldWitness) { + try { + yield this.request('havewitness'); + this.haveWitness = true; + } catch (err) { + ; + } + } + } + + if (this.outbound) { + if (!this.version.hasNetwork()) + throw new Error('Peer does not support network services.'); + + if (this.options.headers) { + if (!this.version.hasHeaders()) + throw new Error('Peer does not support getheaders.'); + } + + if (this.options.spv) { + if (!this.version.hasBloom()) + throw new Error('Peer does not support BIP37.'); + } + + if (this.options.witness) { + if (!this.haveWitness) + throw new Error('Peer does not support segregated witness.'); + } + } + this.ack = true; this.logger.debug('Received verack (%s).', this.hostname); @@ -533,7 +593,8 @@ Peer.prototype.finalize = co(function* finalize() { } // Find some more peers. - this.send(new packets.GetAddrPacket()); + if (!this.pool.hosts.isFull()) + this.sendGetAddr(); // Relay our spv filter if we have one. this.updateWatch(); @@ -768,7 +829,7 @@ Peer.prototype.sendHeaders = function sendHeaders(items) { Peer.prototype.sendVersion = function sendVersion() { var packet = new packets.VersionPacket(); packet.version = constants.VERSION; - packet.services = this.pool.services; + packet.services = this.pool.address.services; packet.ts = this.network.now(); packet.from = this.pool.address; packet.nonce = this.pool.localNonce; @@ -778,6 +839,18 @@ Peer.prototype.sendVersion = function sendVersion() { this.send(packet); }; +/** + * Send a `getaddr` packet. + */ + +Peer.prototype.sendGetAddr = function sendGetAddr() { + if (this.sentGetAddr) + return; + + this.sentGetAddr = true; + this.send(new packets.GetAddrPacket()); +}; + /** * Send a `ping` packet. */ @@ -1556,64 +1629,12 @@ Peer.prototype.handleVersion = co(function* handleVersion(version) { if (this.version) throw new Error('Peer sent a duplicate version.'); - if (!this.network.selfConnect) { - if (util.equal(version.nonce, this.pool.localNonce)) { - this.ignore(); - throw new Error('We connected to ourself. Oops.'); - } - } - - if (version.version < constants.MIN_VERSION) { - this.ignore(); - throw new Error('Peer does not support required protocol version.'); - } - - if (this.options.witness) { - this.haveWitness = version.hasWitness(); - if (!this.haveWitness && this.network.oldWitness) { - try { - yield this.request('havewitness'); - this.haveWitness = true; - } catch (err) { - ; - } - } - } - - if (this.outbound) { - if (!version.hasNetwork()) { - this.ignore(); - throw new Error('Peer does not support network services.'); - } - - if (this.options.headers) { - if (!version.hasHeaders()) { - this.ignore(); - throw new Error('Peer does not support getheaders.'); - } - } - - if (this.options.spv) { - if (!version.hasBloom()) { - this.ignore(); - throw new Error('Peer does not support BIP37.'); - } - } - - if (this.options.witness) { - if (!this.haveWitness) { - this.ignore(); - throw new Error('Peer does not support segregated witness.'); - } - } - } - this.relay = version.relay; this.version = version; - this.fire('version', version); - this.send(new packets.VerackPacket()); + + this.fire('version', version); }); /** @@ -2030,7 +2051,7 @@ Peer.prototype.handlePong = co(function* handlePong(packet) { Peer.prototype.handleGetAddr = co(function* handleGetAddr(packet) { var items = []; - var addr; + var i, addrs, addr; if (this.options.selfish) return; @@ -2042,7 +2063,11 @@ Peer.prototype.handleGetAddr = co(function* handleGetAddr(packet) { this.sentAddr = true; - for (addr = this.pool.hosts.head(); addr; addr = addr.next) { + addrs = this.pool.hosts.toArray(); + + for (i = 0; i < addrs.length; i++) { + addr = addrs[i]; + if (!this.addrFilter.added(addr.hostname, 'ascii')) continue; @@ -2646,9 +2671,9 @@ Peer.prototype.sendCompact = function sendCompact() { Peer.prototype.increaseBan = function increaseBan(score) { this.banScore += score; - if (this.banScore >= constants.BAN_SCORE) { + if (this.banScore >= this.pool.banScore) { this.logger.debug('Ban threshold exceeded (%s).', this.hostname); - this.pool.ban(this); + this.ban(); return true; } @@ -2656,11 +2681,39 @@ Peer.prototype.increaseBan = function increaseBan(score) { }; /** - * Ignore peer. + * Ban peer. */ -Peer.prototype.ignore = function ignore() { - return this.pool.ignore(this); +Peer.prototype.ban = function ban() { + this.logger.debug('Banning peer (%s).', this.hostname); + this.pool.hosts.ban(this.host); + this.pool.hosts.remove(this.hostname); + this.destroy(); +}; + +/** + * Mark connection attempt. + */ + +Peer.prototype.markAttempt = function markAttempt() { + this.pool.hosts.markAttempt(this.hostname); +}; + +/** + * Mark connection success. + */ + +Peer.prototype.markSuccess = function markSuccess() { + this.pool.hosts.markSuccess(this.hostname); +}; + +/** + * Mark ack success. + */ + +Peer.prototype.markAck = function markAck() { + assert(this.version); + this.pool.hosts.markAck(this.hostname, this.version.services); }; /** @@ -2674,8 +2727,7 @@ Peer.prototype.ignore = function ignore() { Peer.prototype.reject = function reject(obj, code, reason, score) { this.sendReject(code, reason, obj); - if (score > 0) - this.increaseBan(score); + this.increaseBan(score); }; /** diff --git a/lib/net/pool.js b/lib/net/pool.js index 27babe6c..21c371ae 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -28,6 +28,8 @@ var request = require('../http/request'); var List = require('../utils/list'); var tcp = require('./tcp'); var dns = require('./dns'); +var murmur3 = require('../utils/murmur3'); +var StaticWriter = require('../utils/staticwriter'); var invTypes = constants.inv; var VerifyError = errors.VerifyError; var VerifyResult = errors.VerifyResult; @@ -104,20 +106,20 @@ function Pool(options) { this.mempool = options.mempool; this.network = this.chain.network; - this.services = constants.LOCAL_SERVICES; - this.port = this.network.port; - this.server = null; this.maxOutbound = 8; this.maxInbound = 8; this.connected = false; this.uid = 0; - this.createServer = null; + this.createSocket = tcp.createSocket; + this.createServer = tcp.createServer; + this.resolve = dns.resolve; this.locker = new Locker(); - this.dnsLock = new Locker(); this.proxyServer = null; this.auth = null; this.identityKey = null; + this.banTime = constants.BAN_TIME; + this.banScore = constants.BAN_SCORE; this.syncing = false; @@ -126,9 +128,12 @@ function Pool(options) { this.feeRate = -1; this.address = new NetAddress(); + this.address.ts = this.network.now(); + this.address.services = constants.LOCAL_SERVICES; + this.address.setPort(this.network.port); - this.peers = new PeerList(this); this.hosts = new HostList(this); + this.peers = new PeerList(this); this.localNonce = util.nonce(); @@ -174,26 +179,51 @@ Pool.prototype._initOptions = function _initOptions() { this.options.headers = this.options.spv; if (!this.options.witness) - this.services &= ~constants.services.WITNESS; + this.address.services &= ~constants.services.WITNESS; - if (this.options.port != null) - this.port = this.options.port; + if (this.options.host != null) { + assert(typeof this.options.host === 'string'); + this.address.setHost(this.options.host); + } - this.address.ts = util.now(); - this.address.services = this.services; - this.address.setPort(this.port); + if (this.options.port != null) { + assert(typeof this.options.port === 'number'); + this.address.setPort(this.options.port); + } - if (this.options.maxOutbound != null) + if (this.options.maxOutbound != null) { + assert(typeof this.options.maxOutbound === 'number'); this.maxOutbound = this.options.maxOutbound; + } - if (this.options.maxInbound != null) + if (this.options.maxInbound != null) { + assert(typeof this.options.maxInbound === 'number'); this.maxInbound = this.options.maxInbound; + } - this.createServer = this.options.createServer; - this.proxyServer = this.options.proxyServer; + if (this.options.createSocket) { + assert(typeof this.options.createSocket === 'function'); + this.createSocket = this.options.createSocket; + } + + if (this.options.createServer) { + assert(typeof this.options.createServer === 'function'); + this.createServer = this.options.createServer; + } + + if (this.options.resolve) { + assert(typeof this.options.resolve === 'function'); + this.resolve = this.options.resolve; + } + + if (this.options.proxyServer) { + assert(typeof this.options.proxyServer === 'string'); + this.proxyServer = this.options.proxyServer; + } if (this.options.bip150) { - this.options.bip151 = true; + assert(typeof this.options.bip151 === 'boolean'); + this.auth = new BIP150.AuthDB(); if (this.options.authPeers) @@ -209,11 +239,25 @@ Pool.prototype._initOptions = function _initOptions() { 'Invalid identity key.'); } - if (this.options.loadTimeout != null) - this.loadTimeout = this.options.loadTimeout; + if (this.options.banScore != null) { + assert(typeof this.options.banScore === 'number'); + this.banScore = this.options.banScore; + } - if (this.options.feeRate != null) + if (this.options.banTime != null) { + assert(typeof this.options.banTime === 'number'); + this.banTime = this.options.banTime; + } + + if (this.options.loadTimeout != null) { + assert(typeof this.options.loadTimeout === 'number'); + this.loadTimeout = this.options.loadTimeout; + } + + if (this.options.feeRate != null) { + assert(typeof this.options.feeRate === 'number'); this.feeRate = this.options.feeRate; + } if (this.options.seeds) this.hosts.setSeeds(this.options.seeds); @@ -227,11 +271,15 @@ Pool.prototype._initOptions = function _initOptions() { if (!this.options.mempool) this.txFilter = new Bloom.Rolling(50000, 0.000001); - if (this.options.requestTimeout != null) + if (this.options.requestTimeout != null) { + assert(typeof this.options.requestTimeout === 'number'); this.requestTimeout = this.options.requestTimeout; + } - if (this.options.invTimeout != null) + if (this.options.invTimeout != null) { + assert(typeof this.options.invTimeout === 'number'); this.invTimeout = this.options.invTimeout; + } }; /** @@ -342,10 +390,12 @@ Pool.prototype._close = co(function* close() { for (i = 0; i < hashes.length; i++) { hash = hashes[i]; - this.requestMap[hash].finish(new Error('Pool closed.')); + item = this.requestMap[hash]; + item.finish(new Error('Pool closed.')); } this.peers.destroy(); + this.hosts.reset(); this.stopInterval(); this.stopTimeout(); @@ -385,19 +435,19 @@ Pool.prototype._connect = co(function* connect() { if (this.connected) return; - try { - ip = yield this.getIP(); - } catch (e) { - this.logger.error(e); + if (this.address.isNull()) { + try { + ip = yield this.getIP(); + } catch (e) { + this.logger.error(e); + } + if (ip) { + this.address.setHost(ip); + this.logger.info('External IP found: %s.', ip); + } } - if (ip) { - this.address.setHost(ip); - this.logger.info('External IP found: %s.', ip); - } - - // Try to get 16 hosts for now. - yield this.hosts.discover(16); + yield this.hosts.discover(); if (this.hosts.size() === 0) throw new Error('No hosts available. Do you have an internet connection?'); @@ -420,13 +470,10 @@ Pool.prototype.listen = function listen() { if (this.server) return Promise.resolve(); - if (this.createServer) { - this.server = this.createServer(); - } else { - if (!tcp.Server) - return; - this.server = new tcp.Server(); - } + if (!this.createServer) + return; + + this.server = this.createServer(); this.server.on('connection', function(socket) { self.handleInbound(socket); @@ -440,7 +487,7 @@ Pool.prototype.listen = function listen() { }); return new Promise(function(resolve, reject) { - self.server.listen(self.port, '0.0.0.0', co.wrap(resolve, reject)); + self.server.listen(self.address.port, '0.0.0.0', co.wrap(resolve, reject)); }); }; @@ -495,7 +542,7 @@ Pool.prototype.handleInbound = function handleInbound(socket) { host = IP.hostname(host, socket.remotePort); - assert(!this.peers.get(host), 'Port collision.'); + assert(!this.peers.map[host], 'Port collision.'); this.addInbound(socket); }; @@ -593,7 +640,6 @@ Pool.prototype.onInterval = function onInterval() { */ Pool.prototype.addLoader = function addLoader() { - var self = this; var peer, addr; if (!this.loaded) @@ -604,32 +650,26 @@ Pool.prototype.addLoader = function addLoader() { return; } - addr = this.hosts.getHost(); + addr = this.getHost(false); + + if (!addr) + return; peer = this.peers.get(addr.hostname); if (peer) { + this.logger.info('Repurposing peer for loader (%s).', peer.hostname); this.setLoader(peer); return; } - if (this.syncing) { - this.startTimeout(); - this.startInterval(); - } + peer = this.createPeer(addr); - peer = this.createPeer(addr.port, addr.host); + this.logger.info('Setting loader peer (%s).', peer.hostname); - this.logger.info('Added loader peer (%s).', peer.hostname); - - assert(!this.peers.load); - this.peers.load = peer; this.peers.add(peer); - this.fillPeers(); - util.nextTick(function() { - self.emit('loader', peer); - }); + this.setLoader(peer); }; /** @@ -649,8 +689,9 @@ Pool.prototype.setLoader = function setLoader(peer) { this.startInterval(); } - this.logger.info('Repurposing peer for loader (%s).', peer.hostname); - this.peers.repurpose(peer); + assert(peer.outbound); + this.peers.load = peer; + this.fillPeers(); peer.sync(); @@ -747,6 +788,17 @@ Pool.prototype.sendMempool = function sendMempool() { peer.sendMempool(); }; +/** + * Send `getaddr` to all peers. + */ + +Pool.prototype.sendGetAddr = function sendGetAddr() { + var peer; + + for (peer = this.peers.head(); peer; peer = peer.next) + peer.sendGetAddr(); +}; + /** * Send `alert` to all peers. * @param {AlertPacket} alert @@ -767,12 +819,12 @@ Pool.prototype.sendAlert = function sendAlert(alert) { * @returns {Peer} */ -Pool.prototype.createPeer = function createPeer(port, host) { +Pool.prototype.createPeer = function createPeer(addr) { var peer = new Peer(this); this.bindPeer(peer); - peer.connect(port, host); + peer.connect(addr); peer.tryOpen(); return peer; @@ -870,8 +922,8 @@ Pool.prototype.bindPeer = function bindPeer(peer) { } })); - peer.on('txs', function(txs) { - self.handleTXInv(txs, peer); + peer.on('txs', function(hashes) { + self.handleTXInv(hashes, peer); }); peer.on('reject', function(reject) { @@ -897,9 +949,6 @@ Pool.prototype.handleOpen = function handleOpen(peer) { if (!peer.outbound) return; - // Attempt to promote from pending->outbound - this.peers.promote(peer); - // If we don't have an ack'd loader yet, use this peer. if (!this.peers.load || !this.peers.load.ack) this.setLoader(peer); @@ -919,6 +968,16 @@ Pool.prototype.handleClose = function handleClose(peer) { return; } + if (this.peers.outbound === 0) { + this.logger.warning('%s %s %s', + 'Could not connect to any peers.', + 'Do you have a network connection?', + 'Retrying in 5 seconds.'); + setTimeout(function() { + self.addLoader(); + }, 5000); + } + if (!peer.isLoader()) { this.removePeer(peer); this.fillPeers(); @@ -929,17 +988,6 @@ Pool.prototype.handleClose = function handleClose(peer) { this.stopInterval(); this.stopTimeout(); - if (this.peers.outbound === 0) { - this.logger.warning('%s %s %s', - 'Could not connect to any peers.', - 'Do you have a network connection?', - 'Retrying in 5 seconds.'); - setTimeout(function() { - self.addLoader(); - }, 5000); - return; - } - this.addLoader(); }; @@ -980,6 +1028,9 @@ Pool.prototype.handleAddr = function handleAddr(addrs, peer) { for (i = 0; i < addrs.length; i++) { addr = addrs[i]; + if (addr.isNull()) + continue; + if (!addr.hasNetwork()) continue; @@ -993,7 +1044,7 @@ Pool.prototype.handleAddr = function handleAddr(addrs, peer) { continue; } - if (this.hosts.add(addr)) + if (this.hosts.add(addr, peer.address)) this.emit('host', addr, peer); } @@ -1215,8 +1266,10 @@ Pool.prototype._handleHeaders = co(function* handleHeaders(headers, peer) { last = hash; - if (!(yield this.chain.has(hash))) - this.getBlock(peer, hash); + if (yield this.chain.has(hash)) + continue; + + this.getBlock(peer, hash); } // Schedule the getdata's we just added. @@ -1339,20 +1392,20 @@ Pool.prototype._handleBlockInv = co(function* handleBlockInv(hashes, peer) { /** * Handle peer inv packet (txs). * @private - * @param {Hash[]} txs + * @param {Hash[]} hashes * @param {Peer} peer */ -Pool.prototype.handleTXInv = function handleTXInv(txs, peer) { +Pool.prototype.handleTXInv = function handleTXInv(hashes, peer) { var i, hash; - this.emit('txs', txs, peer); + this.emit('txs', hashes, peer); if (this.syncing && !this.chain.synced) return; - for (i = 0; i < txs.length; i++) { - hash = txs[i]; + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; try { this.getTX(peer, hash); } catch (e) { @@ -1474,6 +1527,7 @@ Pool.prototype.hasReject = function hasReject(hash) { /** * Create an inbound peer from an existing socket. * @private + * @param {NetAddress} addr * @param {net.Socket} socket */ @@ -1495,6 +1549,45 @@ Pool.prototype.addInbound = function addInbound(socket) { }); }; +/** + * Allocate a host from the host list. + * @param {Boolean} unique + * @returns {NetAddress} + */ + +Pool.prototype.getHost = function getHost(unique) { + var now = this.network.now(); + var i, entry, addr; + + for (i = 0; i < 100; i++) { + entry = this.hosts.getHost(); + + if (!entry) + break; + + addr = entry.addr; + + if (unique) { + if (this.peers.has(addr.hostname)) + continue; + } + + if (addr.isNull()) + continue; + + if (!addr.hasNetwork()) + continue; + + if (now - entry.lastAttempt < 600 && i < 30) + continue; + + if (addr.port !== this.network.port && i < 50) + continue; + + return entry.addr; + } +}; + /** * Create an outbound non-loader peer. These primarily * exist for transaction relaying. @@ -1515,12 +1608,12 @@ Pool.prototype.addOutbound = function addOutbound() { if (!this.peers.load) return; - addr = this.hosts.getHost(); + addr = this.getHost(true); - if (this.peers.get(addr.hostname)) + if (!addr) return; - peer = this.createPeer(addr.port, addr.host); + peer = this.createPeer(addr); this.peers.add(peer); @@ -1529,26 +1622,12 @@ Pool.prototype.addOutbound = function addOutbound() { }); }; -/** - * Attempt to refill the pool with peers. - * @private - */ - -Pool.prototype.fillPeers = co(function* fillPeers() { - var unlock = yield this.dnsLock.lock(); - try { - return yield this._fillPeers(); - } finally { - unlock(); - } -}); - /** * Attempt to refill the pool with peers (no lock). * @private */ -Pool.prototype._fillPeers = co(function* fillPeers() { +Pool.prototype.fillPeers = function fillPeers() { var need = this.maxOutbound - this.peers.outbound; var i; @@ -1559,15 +1638,9 @@ Pool.prototype._fillPeers = co(function* fillPeers() { this.peers.outbound, this.maxOutbound); - if (this.hosts.size() < need && need < 10) { - this.logger.warning('Very few hosts available.'); - this.logger.warning('Hitting DNS seeds again.'); - yield this.hosts.discover(); - } - for (i = 0; i < need; i++) this.addOutbound(); -}); +}; /** * Remove a peer from any list. Drop all load requests. @@ -1923,29 +1996,6 @@ Pool.prototype.setFeeRate = function setFeeRate(rate) { peer.sendFeeRate(rate); }; -/** - * Ban a peer. - * @param {Peer} peer - */ - -Pool.prototype.ban = function ban(peer) { - this.logger.debug('Banning peer (%s).', peer.hostname); - this.hosts.ban(peer.host); - this.hosts.remove(peer.hostname); - peer.destroy(); -}; - -/** - * Ignore peer. - * @param {Peer} peer - */ - -Pool.prototype.ignore = function ignore(peer) { - this.logger.debug('Ignoring peer (%s).', peer.hostname); - this.hosts.ignore(peer.hostname); - peer.destroy(); -}; - /** * Attempt to retrieve external IP from icanhazip.com. * @returns {Promise} @@ -2010,51 +2060,70 @@ Pool.prototype.getIP2 = co(function* getIP2() { /** * Peer List * @constructor + * @param {Object} options */ -function PeerList(pool) { - this.logger = pool.logger; +function PeerList(options) { + this.logger = options.logger; this.map = {}; this.list = new List(); this.load = null; this.inbound = 0; this.outbound = 0; - this.pending = 0; } +/** + * Get the list head. + * @returns {Peer} + */ + PeerList.prototype.head = function head() { return this.list.head; }; +/** + * Get the list tail. + * @returns {Peer} + */ + PeerList.prototype.tail = function tail() { return this.list.tail; }; +/** + * Get list size. + * @returns {Number} + */ + PeerList.prototype.size = function size() { return this.list.size; }; -PeerList.prototype.promote = function promote(peer) { - assert(peer.outbound); - assert(peer.ack); - this.pending--; -}; +/** + * Add peer to list. + * @param {Peer} peer + */ PeerList.prototype.add = function add(peer) { assert(this.list.push(peer)); + assert(!this.map[peer.hostname]); this.map[peer.hostname] = peer; - if (peer.outbound) { + + if (peer.outbound) this.outbound++; - if (!peer.ack) - this.pending++; - } else { + else this.inbound++; - } }; +/** + * Remove peer from list. + * @param {Peer} peer + */ + PeerList.prototype.remove = function remove(peer) { assert(this.list.remove(peer)); + assert(this.map[peer.hostname]); delete this.map[peer.hostname]; @@ -2063,24 +2132,38 @@ PeerList.prototype.remove = function remove(peer) { this.load = null; } - if (peer.outbound) { + if (peer.outbound) this.outbound--; - if (!peer.ack) - this.pending--; - } else { + else this.inbound--; - } }; -PeerList.prototype.repurpose = function repurpose(peer) { - assert(peer.outbound); - this.load = peer; -}; +/** + * Get peer by hostname. + * @param {String} hostname + * @returns {Peer} + */ PeerList.prototype.get = function get(hostname) { return this.map[hostname]; }; +/** + * Test whether a peer exists. + * @param {String} hostname + * @returns {Boolean} + */ + +PeerList.prototype.has = function has(hostname) { + return this.map[hostname] != null; +}; + +/** + * Get peers by host. + * @param {String} host + * @returns {Peer[]} + */ + PeerList.prototype.getByHost = function getByHost(host) { var peers = []; var peer; @@ -2094,6 +2177,10 @@ PeerList.prototype.getByHost = function getByHost(host) { return peers; }; +/** + * Destroy peer list (kills peers). + */ + PeerList.prototype.destroy = function destroy() { var peer, next; @@ -2101,7 +2188,6 @@ PeerList.prototype.destroy = function destroy() { this.load = null; this.inbound = 0; this.outbound = 0; - this.pending = 0; for (peer = this.list.head; peer; peer = next) { next = peer.next; @@ -2112,35 +2198,55 @@ PeerList.prototype.destroy = function destroy() { /** * Host List * @constructor + * @param {Object} options */ -function HostList(pool) { - this.network = pool.network; - this.logger = pool.logger; - this.proxyServer = pool.proxyServer; - this.list = new List(); +function HostList(options) { + this.address = options.address; + this.network = options.network; + this.logger = options.logger; + this.proxyServer = options.proxyServer; + this.resolve = options.resolve; + this.banTime = options.banTime; + this.seeds = []; - this.map = {}; this.banned = {}; - this.setSeeds(this.network.seeds); + + this.map = {}; + this.fresh = []; + this.used = []; + + this.totalFresh = 0; + this.totalUsed = 0; + + this.maxBuckets = 20; + this.maxEntries = 50; + this.maxAddresses = this.maxBuckets * this.maxEntries; + + this.horizonDays = 30; + this.retries = 3; + this.minFailDays = 7; + this.maxFailures = 10; + this.maxRefs = 8; + + this._init(); } /** - * Get head element. - * @returns {NetAddress} + * Initialize list. + * @private */ -HostList.prototype.head = function head() { - return this.list.head; -}; +HostList.prototype._init = function init() { + var i; -/** - * Get tail element. - * @returns {NetAddress} - */ + for (i = 0; i < this.maxBuckets; i++) + this.fresh.push(new MapBucket()); -HostList.prototype.tail = function tail() { - return this.list.tail; + for (i = 0; i < this.maxBuckets; i++) + this.used.push(new List()); + + this.setSeeds(this.network.seeds); }; /** @@ -2149,7 +2255,16 @@ HostList.prototype.tail = function tail() { */ HostList.prototype.size = function size() { - return this.list.size; + return this.totalFresh + this.totalUsed; +}; + +/** + * Test whether the host list is full. + * @returns {Boolean} + */ + +HostList.prototype.isFull = function isFull() { + return this.size() >= this.maxAddresses; }; /** @@ -2157,65 +2272,22 @@ HostList.prototype.size = function size() { */ HostList.prototype.reset = function reset() { + var i, bucket; + this.map = {}; - this.list.reset(); -}; -/** - * Clear banned and ignored. - */ + for (i = 0; i < this.fresh.length; i++) { + bucket = this.fresh[i]; + bucket.reset(); + } -HostList.prototype.clear = function clear() { - this.banned = {}; -}; + for (i = 0; i < this.used.length; i++) { + bucket = this.used[i]; + bucket.reset(); + } -/** - * Allocate a new loader host. - * @returns {NetAddress} - */ - -HostList.prototype.getHost = function getHost() { - var addr = this.list.shift(); - assert(addr, 'No address available.'); - this.list.push(addr); - return addr; -}; - -/** - * Add host to host list. - * @param {NetAddress} addr - * @returns {Boolean} - */ - -HostList.prototype.add = function add(addr) { - if (this.list.size > 500) - return; - - if (this.map[addr.hostname]) - return; - - assert(this.list.unshift(addr)); - this.map[addr.hostname] = addr; - - return addr; -}; - -/** - * Remove host from host list. - * @param {String} hostname - * @returns {Boolean} - */ - -HostList.prototype.remove = function remove(hostname) { - var addr = this.map[hostname]; - - if (!addr) - return; - - assert(this.list.remove(addr)); - delete this.map[addr.hostname]; - - return addr; + this.totalFresh = 0; + this.totalUsed = 0; }; /** @@ -2236,6 +2308,14 @@ HostList.prototype.unban = function unban(host) { delete this.banned[host]; }; +/** + * Clear banned hosts. + */ + +HostList.prototype.clearBanned = function clearBanned() { + this.banned = {}; +}; + /** * Test whether the host is banned. * @param {String} host @@ -2248,7 +2328,7 @@ HostList.prototype.isBanned = function isBanned(host) { if (time == null) return false; - if (util.now() > time + constants.BAN_TIME) { + if (util.now() > time + this.banTime) { delete this.banned[host]; return false; } @@ -2257,18 +2337,442 @@ HostList.prototype.isBanned = function isBanned(host) { }; /** - * Ignore peer. + * Allocate a new loader host. + * @returns {HostEntry} + */ + +HostList.prototype.getHost = function getHost() { + var now = this.network.now(); + var buckets = this.fresh; + var factor = 1; + var index, key, bucket, entry, num; + + if (this.size() === 0) + return; + + if (this.totalUsed > 0) { + if (this.totalFresh === 0 || util.random(0, 2) === 0) + buckets = this.used; + } + + for (;;) { + index = util.random(0, buckets.length); + bucket = buckets[index]; + + if (bucket.size === 0) + continue; + + index = util.random(0, bucket.size); + + if (buckets === this.used) { + entry = bucket.head; + while (index--) + entry = entry.next; + } else { + key = bucket.keys()[index]; + entry = bucket.get(key); + } + + num = util.random(0, 1 << 30); + + if (num < factor * entry.chance(now) * (1 << 30)) + return entry; + + factor *= 1.2; + } +}; + +/** + * Get fresh bucket for host. + * @private + * @param {HostEntry} entry + * @returns {MapBucket} + */ + +HostList.prototype.freshBucket = function freshBucket(entry) { + var size = 0; + var bw, hash, index; + + size += entry.addr.host.length; + size += entry.src.host.length; + + bw = new StaticWriter(size); + bw.writeString(entry.addr.host, 'ascii'); + bw.writeString(entry.src.host, 'ascii'); + + hash = murmur3(bw.render(), 0xfba4c795); + index = hash % this.fresh.length; + + return this.fresh[index]; +}; + +/** + * Get used bucket for host. + * @private + * @param {HostEntry} entry + * @returns {List} + */ + +HostList.prototype.usedBucket = function usedBucket(entry) { + var data = new Buffer(entry.addr.host, 'ascii'); + var hash = murmur3(data, 0xfba4c795); + var index = hash % this.used.length; + return this.used[index]; +}; + +/** + * Add host to host list. + * @param {NetAddress} addr + * @param {NetAddress?} src + * @returns {Boolean} + */ + +HostList.prototype.add = function add(addr, src) { + var now = this.network.now(); + var penalty = 2 * 60 * 60; + var interval = 24 * 60 * 60; + var factor = 1; + var i, entry, bucket; + + if (this.isFull()) + return false; + + entry = this.map[addr.hostname]; + + if (entry) { + // No source means we're inserting + // this ourselves. No penalty. + if (!src) + penalty = 0; + + // Update services. + entry.addr.services |= addr.services; + + // Online? + if (now - addr.ts < 24 * 60 * 60) + interval = 60 * 60; + + // Periodically update time. + if (entry.addr.ts < addr.ts - interval - penalty) + entry.addr.ts = addr.ts; + + // Do not update if no new + // information is present. + if (entry.addr.ts && addr.ts <= entry.addr.ts) + return false; + + // Do not update if the entry was + // already in the "used" table. + if (entry.used) + return false; + + assert(entry.refCount > 0); + + // Do not update if the max + // reference count is reached. + if (entry.refCount === this.maxRefs) + return false; + + assert(entry.refCount < this.maxRefs); + + // Stochastic test: previous refCount + // N: 2^N times harder to increase it. + for (i = 0; i < entry.refCount; i++) + factor *= 2; + + if (util.random(0, factor) !== 0) + return false; + } else { + if (!src) + src = this.address; + + entry = new HostEntry(addr, src, this.network); + + this.totalFresh++; + } + + bucket = this.freshBucket(entry); + + if (bucket.has(entry.key())) + return false; + + if (bucket.size >= this.maxEntries) + this.evictFresh(bucket); + + bucket.set(entry.key(), entry); + entry.refCount++; + + this.map[entry.key()] = entry; + + return true; +}; + +/** + * Evict a host from fresh bucket. + * @param {MapBucket} bucket + */ + +HostList.prototype.evictFresh = function evictFresh(bucket) { + var keys = bucket.keys(); + var i, key, entry, old; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = bucket.get(key); + + if (this.isStale(entry)) { + bucket.remove(entry.key()); + + if (--entry.refCount === 0) { + delete this.map[entry.key()]; + this.totalFresh--; + } + + continue; + } + + if (!old) { + old = entry; + continue; + } + + if (entry.addr.ts < old.addr.ts) + old = entry; + } + + if (!old) + return; + + bucket.remove(old.key()); + + if (--old.refCount === 0) { + delete this.map[old.key()]; + this.totalFresh--; + } +}; + +/** + * Test whether a host is evictable. + * @param {HostEntry} entry + * @returns {Boolean} + */ + +HostList.prototype.isStale = function isStale(entry) { + var now = this.network.now(); + + if (entry.lastAttempt && entry.lastAttempt >= now - 60) + return false; + + if (entry.addr.ts > now + 10 * 60) + return true; + + if (entry.addr.ts === 0) + return true; + + if (now - entry.addr.ts > this.horizonDays * 24 * 60 * 60) + return true; + + if (entry.lastSuccess === 0 && entry.attempts >= this.retries) + return true; + + if (now - entry.lastSuccess > this.minFailDays * 24 * 60 * 60) { + if (entry.attempts >= this.maxFailures) + return true; + } + + return false; +}; + +/** + * Remove host from host list. + * @param {String} hostname + * @returns {NetAddress} + */ + +HostList.prototype.remove = function remove(hostname) { + var entry = this.map[hostname]; + var i, head, bucket; + + if (!entry) + return; + + if (entry.used) { + assert(entry.refCount === 0); + + head = entry; + while (head.prev) + head = head.prev; + + for (i = 0; i < this.used.length; i++) { + bucket = this.used[i]; + if (bucket.head === head) { + bucket.remove(entry); + this.totalUsed--; + break; + } + } + + assert(i < this.used.length); + } else { + for (i = 0; i < this.fresh.length; i++) { + bucket = this.fresh[i]; + if (bucket.remove(entry.key())) + entry.refCount--; + } + + this.totalFresh--; + assert(entry.refCount === 0); + } + + delete this.map[entry.key()]; + + return entry.addr; +}; + +/** + * Mark host as failed. * @param {String} hostname */ -HostList.prototype.ignore = function ignore(hostname) { - var addr = this.map[hostname]; +HostList.prototype.markAttempt = function markAttempt(hostname) { + var entry = this.map[hostname]; + var now = this.network.now(); - if (!addr) + if (!entry) return; - delete this.map[hostname]; - this.list.remove(addr); + entry.attempts++; + entry.lastAttempt = now; +}; + +/** + * Mark host as successfully connected. + * @param {String} hostname + */ + +HostList.prototype.markSuccess = function markSuccess(hostname) { + var entry = this.map[hostname]; + var now = this.network.now(); + + if (!entry) + return; + + if (now - entry.addr.ts > 20 * 60) + entry.addr.ts = now; +}; + +/** + * Mark host as successfully connected. + * @param {String} hostname + * @param {Number} services + */ + +HostList.prototype.markAck = function markAck(hostname, services) { + var entry = this.map[hostname]; + var now = this.network.now(); + var i, bucket, evicted, old, fresh; + + if (!entry) + return; + + entry.addr.services |= services; + entry.lastSuccess = now; + entry.lastAttempt = now; + entry.attempts = 0; + + if (entry.used) + return; + + assert(entry.refCount > 0); + + // Remove from fresh. + for (i = 0; i < this.fresh.length; i++) { + bucket = this.fresh[i]; + if (bucket.remove(entry.key())) { + entry.refCount--; + old = bucket; + } + } + + assert(old); + assert(entry.refCount === 0); + this.totalFresh--; + + // Find room in used bucket. + bucket = this.usedBucket(entry); + + if (bucket.size < this.maxEntries) { + entry.used = true; + bucket.push(entry); + this.totalUsed++; + return; + } + + // No room. Evict. + evicted = this.evictUsed(bucket); + fresh = this.freshBucket(evicted); + + // Move to entry's old bucket if no room. + if (fresh.size >= this.maxEntries) + fresh = old; + + // Swap to evicted's used bucket. + entry.used = true; + bucket.replace(evicted, entry); + + // Move evicted to fresh bucket. + evicted.used = false; + fresh.set(evicted.key(), evicted); + assert(evicted.refCount === 0); + evicted.refCount++; + this.totalFresh++; +}; + +/** + * Pick used for eviction. + * @param {List} bucket + */ + +HostList.prototype.evictUsed = function evictUsed(bucket) { + var old = bucket.head; + var entry; + + for (entry = bucket.head; entry; entry = entry.next) { + if (entry.addr.ts < old.addr.ts) + old = entry; + } + + return old; +}; + +/** + * Convert address list to array. + * @returns {NetAddress[]} + */ + +HostList.prototype.toArray = function toArray() { + var out = []; + var i, j, keys, key, bucket, entry; + + for (i = 0; i < this.fresh.length; i++) { + bucket = this.fresh[i]; + keys = bucket.keys(); + for (j = 0; j < keys.length; j++) { + key = keys[j]; + entry = bucket.get(key); + out.push(entry.addr); + } + } + + for (i = 0; i < this.used.length; i++) { + bucket = this.used[i]; + for (entry = bucket.head; entry; entry = entry.next) + out.push(entry.addr); + } + + assert.equal(out.length, this.size()); + + return out; }; /** @@ -2292,9 +2796,9 @@ HostList.prototype.setSeeds = function setSeeds(seeds) { * @param {String} hostname */ -HostList.prototype.addSeed = function addSeed(hostname) { - var addr = IP.parseHost(hostname, this.network.port); - this.seeds.push(addr); +HostList.prototype.addSeed = function addSeed(host) { + var addr = IP.parseHost(host, this.network.port); + return this.seeds.push(addr); }; /** @@ -2303,20 +2807,16 @@ HostList.prototype.addSeed = function addSeed(hostname) { * @returns {Promise} */ -HostList.prototype.discover = co(function* discover(max) { +HostList.prototype.discover = co(function* discover() { + var jobs = []; var i, seed; - if (!max) - max = Infinity; - for (i = 0; i < this.seeds.length; i++) { seed = this.seeds[i]; - - yield this.populate(seed); - - if (this.list.size >= max) - break; + jobs.push(this.populate(seed)); } + + yield Promise.all(jobs); }); /** @@ -2337,7 +2837,7 @@ HostList.prototype.populate = co(function* populate(seed) { this.logger.info('Resolving hosts from seed: %s.', seed.host); try { - hosts = yield dns.resolve(seed.host, this.proxyServer); + hosts = yield this.resolve(seed.host, this.proxyServer); } catch (e) { this.logger.error(e); return; @@ -2350,6 +2850,159 @@ HostList.prototype.populate = co(function* populate(seed) { } }); +/** + * MapBucket + * @constructor + */ + +function MapBucket() { + this.map = {}; + this.size = 0; +} + +/** + * Get map keys. + * @returns {String[]} + */ + +MapBucket.prototype.keys = function keys() { + return Object.keys(this.map); +}; + +/** + * Get item from map. + * @param {String} key + * @returns {Object|null} + */ + +MapBucket.prototype.get = function get(key) { + return this.map[key]; +}; + +/** + * Test whether map has an item. + * @param {String} key + * @returns {Boolean} + */ + +MapBucket.prototype.has = function has(key) { + return this.map[key] !== undefined; +}; + +/** + * Set a key to value in map. + * @param {String} key + * @param {Object} value + * @returns {Boolean} + */ + +MapBucket.prototype.set = function set(key, value) { + var item = this.map[key]; + + assert(value !== undefined); + + this.map[key] = value; + + if (item === undefined) { + this.size++; + return true; + } + + return false; +}; + +/** + * Remove an item from map. + * @param {String} key + * @returns {Object|null} + */ + +MapBucket.prototype.remove = function remove(key) { + var item = this.map[key]; + + if (item === undefined) + return; + + delete this.map[key]; + this.size--; + + return item; +}; + +/** + * Reset the map. + */ + +MapBucket.prototype.reset = function reset() { + this.map = {}; + this.size = 0; +}; + +/** + * HostEntry + * @constructor + * @param {NetAddress} addr + */ + +function HostEntry(addr, src, network) { + assert(addr instanceof NetAddress); + assert(src instanceof NetAddress); + + this.addr = addr; + this.src = src; + this.prev = null; + this.next = null; + this.used = false; + this.refCount = 0; + this.attempts = 0; + this.lastSuccess = 0; + this.lastAttempt = 0; +} + +/** + * Get key suitable for a hash table (hostname). + * @returns {String} + */ + +HostEntry.prototype.key = function key() { + return this.addr.hostname; +}; + +/** + * Get host priority. + * @param {Number} now + * @returns {Number} + */ + +HostEntry.prototype.chance = function _chance(now) { + var attempts = this.attempts; + var chance = 1; + + if (now - this.lastAttempt < 60 * 10) + chance *= 0.01; + + chance *= Math.pow(0.66, Math.min(attempts, 8)); + + return chance; +}; + +/** + * Inspect host address. + * @returns {Object} + */ + +HostEntry.prototype.inspect = function inspect() { + return { + addr: this.addr, + src: this.src, + used: this.used, + refCount: this.refCount, + attempts: this.attempts, + lastSuccess: util.date(this.lastSuccess), + lastAttempt: util.date(this.lastAttempt) + }; +}; + /** * Represents an in-flight block or transaction. * @exports LoadRequest diff --git a/lib/net/tcp-browser.js b/lib/net/tcp-browser.js index bf347d35..26da71fe 100644 --- a/lib/net/tcp-browser.js +++ b/lib/net/tcp-browser.js @@ -9,8 +9,8 @@ var ProxySocket = require('./proxysocket'); var tcp = exports; -tcp.connect = function connect(port, host, uri) { - return ProxySocket.connect(uri, port, host); +tcp.createSocket = function createSocket(port, host, proxy) { + return ProxySocket.connect(proxy, port, host); }; -tcp.Server = null; +tcp.createServer = null; diff --git a/lib/net/tcp.js b/lib/net/tcp.js index 003019f1..d624e486 100644 --- a/lib/net/tcp.js +++ b/lib/net/tcp.js @@ -9,8 +9,10 @@ var net = require('net'); var tcp = exports; -tcp.connect = function connect(port, host) { +tcp.createSocket = function createSocket(port, host, proxy) { return net.connect(port, host); }; -tcp.Server = net.Server; +tcp.createServer = function createServer() { + return new net.Server(); +}; diff --git a/lib/primitives/netaddress.js b/lib/primitives/netaddress.js index bc457222..5af7451b 100644 --- a/lib/primitives/netaddress.js +++ b/lib/primitives/netaddress.js @@ -6,11 +6,11 @@ 'use strict'; +var assert = require('assert'); var constants = require('../protocol/constants'); var Network = require('../protocol/network'); var util = require('../utils/util'); var IP = require('../utils/ip'); -var assert = require('assert'); var StaticWriter = require('../utils/staticwriter'); var BufferReader = require('../utils/reader'); @@ -39,9 +39,6 @@ function NetAddress(options) { this.ts = 0; this.hostname = '0.0.0.0:0'; - this.prev = null; - this.next = null; - if (options) this.fromOptions(options); } @@ -122,6 +119,15 @@ NetAddress.prototype.hasWitness = function hasWitness() { return (this.services & constants.services.WITNESS) !== 0; }; +/** + * Test whether the host is null. + * @returns {Boolean} + */ + +NetAddress.prototype.isNull = function isNull() { + return this.host === '0.0.0.0' || this.host === '::'; +}; + /** * Set host. * @param {String} host @@ -326,8 +332,7 @@ NetAddress.prototype.toRaw = function toRaw(full) { NetAddress.prototype.inspect = function inspect() { return ''; diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index 579fbf77..c596b358 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -49,12 +49,12 @@ main.type = 'main'; */ main.seeds = [ + 'seed.bitcoin.sipa.be', // Pieter Wuille + 'dnsseed.bluematt.me', // Matt Corallo 'dnsseed.bitcoin.dashjr.org', // Luke Dashjr 'seed.bitcoinstats.com', // Christian Decker 'bitseed.xf2.org', // Jeff Garzik - 'seed.bitcoin.jonasschnelli.ch', // Jonas Schnelli - 'seed.bitcoin.sipa.be', // Pieter Wuille - 'dnsseed.bluematt.me' // Matt Corallo + 'seed.bitcoin.jonasschnelli.ch' // Jonas Schnelli ]; /** diff --git a/lib/protocol/timedata.js b/lib/protocol/timedata.js index 2ebfc2ba..a656ab04 100644 --- a/lib/protocol/timedata.js +++ b/lib/protocol/timedata.js @@ -100,6 +100,26 @@ TimeData.prototype.now = function now() { return util.now() + this.offset; }; +/** + * Adjust a timestamp. + * @param {Number} time + * @returns {Number} Adjusted Time. + */ + +TimeData.prototype.adjust = function adjust(time) { + return time + this.offset; +}; + +/** + * Unadjust a timestamp. + * @param {Number} time + * @returns {Number} Local Time. + */ + +TimeData.prototype.local = function local(time) { + return time - this.offset; +}; + /* * Helpers */ diff --git a/lib/utils/list.js b/lib/utils/list.js index 1c843cbc..17421acb 100644 --- a/lib/utils/list.js +++ b/lib/utils/list.js @@ -175,6 +175,32 @@ List.prototype.remove = function remove(item) { return true; }; +/** + * Replace an item in-place. + * @param {ListItem} ref + * @param {ListItem} item + */ + +List.prototype.replace = function replace(ref, item) { + if (ref.prev) + ref.prev.next = item; + + if (ref.next) + ref.next.prev = item; + + item.prev = ref.prev; + item.next = ref.next; + + ref.next = null; + ref.prev = null; + + if (this.head === ref) + this.head = item; + + if (this.tail === ref) + this.tail = item; +}; + /** * Slice the list to an array of items. * Will remove the items sliced. diff --git a/lib/utils/util.js b/lib/utils/util.js index 9aaaee98..0b7b79cb 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -541,6 +541,17 @@ util.time = function time(date) { return new Date(date) / 1000 | 0; }; +/** + * Get random range. + * @param {Number} min + * @param {Number} max + * @returns {Number} + */ + +util.random = function random(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +}; + /** * Create a 64 bit nonce. * @returns {Buffer} @@ -548,8 +559,8 @@ util.time = function time(date) { util.nonce = function _nonce() { var nonce = new Buffer(8); - nonce.writeUInt32LE((Math.random() * 0x100000000) >>> 0, 0, true); - nonce.writeUInt32LE((Math.random() * 0x100000000) >>> 0, 4, true); + nonce.writeUInt32LE(util.random(0, 0x100000000), 0, true); + nonce.writeUInt32LE(util.random(0, 0x100000000), 4, true); return nonce; };