diff --git a/lib/net/bip150.js b/lib/net/bip150.js index d4de7a52..583079ba 100644 --- a/lib/net/bip150.js +++ b/lib/net/bip150.js @@ -26,14 +26,10 @@ const packets = require('./packets'); const {encoding} = bio; /** + * BIP150 * Represents a BIP150 input/output stream. * @alias module:net.BIP150 - * @constructor - * @param {BIP151} bip151 - * @param {String} host - * @param {Boolean} outbound - * @param {AuthDB} db - * @param {Buffer} key - Identity key. + * @extends EventEmitter * @property {BIP151} bip151 * @property {BIP151Stream} input * @property {BIP151Stream} output @@ -51,755 +47,765 @@ const {encoding} = bio; * @property {Boolean} completed */ -function BIP150(bip151, host, outbound, db, key) { - if (!(this instanceof BIP150)) - return new BIP150(bip151, host, outbound, db, key); +class BIP150 extends EventEmitter { + /** + * Create a BIP150 input/output stream. + * @constructor + * @param {BIP151} bip151 + * @param {String} host + * @param {Boolean} outbound + * @param {AuthDB} db + * @param {Buffer} key - Identity key. + */ - EventEmitter.call(this); + constructor(bip151, host, outbound, db, key) { + super(); - assert(bip151, 'BIP150 requires BIP151.'); - assert(typeof host === 'string', 'Hostname required.'); - assert(typeof outbound === 'boolean', 'Outbound flag required.'); - assert(db instanceof AuthDB, 'Auth DB required.'); - assert(Buffer.isBuffer(key), 'Identity key required.'); + assert(bip151, 'BIP150 requires BIP151.'); + assert(typeof host === 'string', 'Hostname required.'); + assert(typeof outbound === 'boolean', 'Outbound flag required.'); + assert(db instanceof AuthDB, 'Auth DB required.'); + assert(Buffer.isBuffer(key), 'Identity key required.'); - this.bip151 = bip151; - this.input = bip151.input; - this.output = bip151.output; - this.hostname = host; - this.outbound = outbound; - this.db = db; - this.privateKey = key; - this.publicKey = secp256k1.publicKeyCreate(key, true); + this.bip151 = bip151; + this.input = bip151.input; + this.output = bip151.output; + this.hostname = host; + this.outbound = outbound; + this.db = db; + this.privateKey = key; + this.publicKey = secp256k1.publicKeyCreate(key, true); - this.peerIdentity = null; - this.challengeReceived = false; - this.replyReceived = false; - this.proposeReceived = false; - this.challengeSent = false; - this.auth = false; - this.completed = false; - this.job = null; - this.timeout = null; - this.onAuth = null; + this.peerIdentity = null; + this.challengeReceived = false; + this.replyReceived = false; + this.proposeReceived = false; + this.challengeSent = false; + this.auth = false; + this.completed = false; + this.job = null; + this.timeout = null; + this.onAuth = null; - this.init(); -} - -Object.setPrototypeOf(BIP150.prototype, EventEmitter.prototype); - -/** - * Initialize BIP150. - * @private - */ - -BIP150.prototype.init = function init() { - if (this.outbound) - this.peerIdentity = this.db.getKnown(this.hostname); -}; - -/** - * Test whether the state should be - * considered authed. This differs - * for inbound vs. outbound. - * @returns {Boolean} - */ - -BIP150.prototype.isAuthed = function isAuthed() { - if (this.outbound) - return this.challengeSent && this.challengeReceived; - return this.challengeReceived && this.replyReceived; -}; - -/** - * Handle a received challenge hash. - * Returns an authreply signature. - * @param {Buffer} hash - * @returns {Buffer} - * @throws on auth failure - */ - -BIP150.prototype.challenge = function challenge(hash) { - const type = this.outbound ? 'r' : 'i'; - - assert(this.bip151.handshake, 'No BIP151 handshake before challenge.'); - assert(!this.challengeReceived, 'Peer challenged twice.'); - this.challengeReceived = true; - - if (hash.equals(encoding.ZERO_HASH)) - throw new Error('Auth failure.'); - - const msg = this.hash(this.input.sid, type, this.publicKey); - - if (!ccmp(hash, msg)) - return encoding.ZERO_SIG64; - - if (this.isAuthed()) { - this.auth = true; - this.emit('auth'); + this.init(); } - const sig = secp256k1.sign(msg, this.privateKey); + /** + * Initialize BIP150. + * @private + */ - // authreply - return secp256k1.fromDER(sig); -}; + init() { + if (this.outbound) + this.peerIdentity = this.db.getKnown(this.hostname); + } -/** - * Handle a received reply signature. - * Returns an authpropose hash. - * @param {Buffer} data - * @returns {Buffer} - * @throws on auth failure - */ + /** + * Test whether the state should be + * considered authed. This differs + * for inbound vs. outbound. + * @returns {Boolean} + */ -BIP150.prototype.reply = function reply(data) { - const type = this.outbound ? 'i' : 'r'; + isAuthed() { + if (this.outbound) + return this.challengeSent && this.challengeReceived; + return this.challengeReceived && this.replyReceived; + } - assert(this.challengeSent, 'Unsolicited reply.'); - assert(!this.replyReceived, 'Peer replied twice.'); - this.replyReceived = true; + /** + * Handle a received challenge hash. + * Returns an authreply signature. + * @param {Buffer} hash + * @returns {Buffer} + * @throws on auth failure + */ - if (data.equals(encoding.ZERO_SIG64)) - throw new Error('Auth failure.'); + challenge(hash) { + const type = this.outbound ? 'r' : 'i'; - if (!this.peerIdentity) - return random.randomBytes(32); + assert(this.bip151.handshake, 'No BIP151 handshake before challenge.'); + assert(!this.challengeReceived, 'Peer challenged twice.'); + this.challengeReceived = true; - const sig = secp256k1.toDER(data); - const msg = this.hash(this.output.sid, type, this.peerIdentity); + if (hash.equals(encoding.ZERO_HASH)) + throw new Error('Auth failure.'); - const result = secp256k1.verify(msg, sig, this.peerIdentity); + const msg = this.hash(this.input.sid, type, this.publicKey); - if (!result) - return random.randomBytes(32); + if (!ccmp(hash, msg)) + return encoding.ZERO_SIG64; + + if (this.isAuthed()) { + this.auth = true; + this.emit('auth'); + } + + const sig = secp256k1.sign(msg, this.privateKey); + + // authreply + return secp256k1.fromDER(sig); + } + + /** + * Handle a received reply signature. + * Returns an authpropose hash. + * @param {Buffer} data + * @returns {Buffer} + * @throws on auth failure + */ + + reply(data) { + const type = this.outbound ? 'i' : 'r'; + + assert(this.challengeSent, 'Unsolicited reply.'); + assert(!this.replyReceived, 'Peer replied twice.'); + this.replyReceived = true; + + if (data.equals(encoding.ZERO_SIG64)) + throw new Error('Auth failure.'); + + if (!this.peerIdentity) + return random.randomBytes(32); + + const sig = secp256k1.toDER(data); + const msg = this.hash(this.output.sid, type, this.peerIdentity); + + const result = secp256k1.verify(msg, sig, this.peerIdentity); + + if (!result) + return random.randomBytes(32); + + if (this.isAuthed()) { + this.auth = true; + this.emit('auth'); + return null; + } + + assert(this.outbound, 'No challenge received before reply on inbound.'); + + // authpropose + return this.hash(this.input.sid, 'p', this.publicKey); + } + + /** + * Handle a received propose hash. + * Returns an authchallenge hash. + * @param {Buffer} hash + * @returns {Buffer} + */ + + propose(hash) { + assert(!this.outbound, 'Outbound peer tried to propose.'); + assert(!this.challengeSent, 'Unsolicited propose.'); + assert(!this.proposeReceived, 'Peer proposed twice.'); + this.proposeReceived = true; + + const match = this.findAuthorized(hash); + + if (!match) + return encoding.ZERO_HASH; + + this.peerIdentity = match; + + // Add them in case we ever connect to them. + this.db.addKnown(this.hostname, this.peerIdentity); + + this.challengeSent = true; + + // authchallenge + return this.hash(this.output.sid, 'r', this.peerIdentity); + } + + /** + * Create initial authchallenge hash + * for the peer. The peer's identity + * key must be known. + * @returns {AuthChallengePacket} + */ + + toChallenge() { + assert(this.bip151.handshake, 'No BIP151 handshake before challenge.'); + assert(this.outbound, 'Cannot challenge an inbound connection.'); + assert(this.peerIdentity, 'Cannot challenge without a peer identity.'); + + const msg = this.hash(this.output.sid, 'i', this.peerIdentity); + + assert(!this.challengeSent, 'Cannot initiate challenge twice.'); + this.challengeSent = true; + + return new packets.AuthChallengePacket(msg); + } + + /** + * Derive new cipher keys based on + * BIP150 data. This differs from + * the regular key derivation of BIP151. + * @param {Buffer} sid - Sesson ID + * @param {Buffer} key - `k1` or `k2` + * @param {Buffer} req - Requesting Identity Key + * @param {Buffer} res - Response Identity Key + * @returns {Buffer} + */ + + rekey(sid, key, req, res) { + const seed = Buffer.allocUnsafe(130); + sid.copy(seed, 0); + key.copy(seed, 32); + req.copy(seed, 64); + res.copy(seed, 97); + return hash256.digest(seed); + } + + /** + * Rekey the BIP151 input stream + * using BIP150-style derivation. + */ + + rekeyInput() { + const stream = this.input; + const req = this.peerIdentity; + const res = this.publicKey; + const k1 = this.rekey(stream.sid, stream.k1, req, res); + const k2 = this.rekey(stream.sid, stream.k2, req, res); + stream.rekey(k1, k2); + } + + /** + * Rekey the BIP151 output stream + * using BIP150-style derivation. + */ + + rekeyOutput() { + const stream = this.output; + const req = this.publicKey; + const res = this.peerIdentity; + const k1 = this.rekey(stream.sid, stream.k1, req, res); + const k2 = this.rekey(stream.sid, stream.k2, req, res); + stream.rekey(k1, k2); + } + + /** + * Create a hash using the session ID. + * @param {Buffer} sid + * @param {String} ch + * @param {Buffer} key + * @returns {Buffer} + */ + + hash(sid, ch, key) { + const data = Buffer.allocUnsafe(66); + sid.copy(data, 0); + data[32] = ch.charCodeAt(0); + key.copy(data, 33); + return hash256.digest(data); + } + + /** + * Find an authorized peer in the Auth + * DB based on a proposal hash. Note + * that the hash to find is specific + * to the state of BIP151. This results + * in an O(n) search. + * @param {Buffer} hash + * @returns {Buffer|null} + */ + + findAuthorized(hash) { + // Scary O(n) stuff. + for (const key of this.db.authorized) { + const msg = this.hash(this.output.sid, 'p', key); + + // XXX Do we really need a constant + // time compare here? Do it just to + // be safe I guess. + if (ccmp(msg, hash)) + return key; + } - if (this.isAuthed()) { - this.auth = true; - this.emit('auth'); return null; } - assert(this.outbound, 'No challenge received before reply on inbound.'); + /** + * Destroy the BIP150 stream and + * any current running wait job. + */ - // authpropose - return this.hash(this.input.sid, 'p', this.publicKey); -}; + destroy() { + if (!this.job) + return; -/** - * Handle a received propose hash. - * Returns an authchallenge hash. - * @param {Buffer} hash - * @returns {Buffer} - */ - -BIP150.prototype.propose = function propose(hash) { - assert(!this.outbound, 'Outbound peer tried to propose.'); - assert(!this.challengeSent, 'Unsolicited propose.'); - assert(!this.proposeReceived, 'Peer proposed twice.'); - this.proposeReceived = true; - - const match = this.findAuthorized(hash); - - if (!match) - return encoding.ZERO_HASH; - - this.peerIdentity = match; - - // Add them in case we ever connect to them. - this.db.addKnown(this.hostname, this.peerIdentity); - - this.challengeSent = true; - - // authchallenge - return this.hash(this.output.sid, 'r', this.peerIdentity); -}; - -/** - * Create initial authchallenge hash - * for the peer. The peer's identity - * key must be known. - * @returns {AuthChallengePacket} - */ - -BIP150.prototype.toChallenge = function toChallenge() { - assert(this.bip151.handshake, 'No BIP151 handshake before challenge.'); - assert(this.outbound, 'Cannot challenge an inbound connection.'); - assert(this.peerIdentity, 'Cannot challenge without a peer identity.'); - - const msg = this.hash(this.output.sid, 'i', this.peerIdentity); - - assert(!this.challengeSent, 'Cannot initiate challenge twice.'); - this.challengeSent = true; - - return new packets.AuthChallengePacket(msg); -}; - -/** - * Derive new cipher keys based on - * BIP150 data. This differs from - * the regular key derivation of BIP151. - * @param {Buffer} sid - Sesson ID - * @param {Buffer} key - `k1` or `k2` - * @param {Buffer} req - Requesting Identity Key - * @param {Buffer} res - Response Identity Key - * @returns {Buffer} - */ - -BIP150.prototype.rekey = function rekey(sid, key, req, res) { - const seed = Buffer.allocUnsafe(130); - sid.copy(seed, 0); - key.copy(seed, 32); - req.copy(seed, 64); - res.copy(seed, 97); - return hash256.digest(seed); -}; - -/** - * Rekey the BIP151 input stream - * using BIP150-style derivation. - */ - -BIP150.prototype.rekeyInput = function rekeyInput() { - const stream = this.input; - const req = this.peerIdentity; - const res = this.publicKey; - const k1 = this.rekey(stream.sid, stream.k1, req, res); - const k2 = this.rekey(stream.sid, stream.k2, req, res); - stream.rekey(k1, k2); -}; - -/** - * Rekey the BIP151 output stream - * using BIP150-style derivation. - */ - -BIP150.prototype.rekeyOutput = function rekeyOutput() { - const stream = this.output; - const req = this.publicKey; - const res = this.peerIdentity; - const k1 = this.rekey(stream.sid, stream.k1, req, res); - const k2 = this.rekey(stream.sid, stream.k2, req, res); - stream.rekey(k1, k2); -}; - -/** - * Create a hash using the session ID. - * @param {Buffer} sid - * @param {String} ch - * @param {Buffer} key - * @returns {Buffer} - */ - -BIP150.prototype.hash = function hash(sid, ch, key) { - const data = Buffer.allocUnsafe(66); - sid.copy(data, 0); - data[32] = ch.charCodeAt(0); - key.copy(data, 33); - return hash256.digest(data); -}; - -/** - * Find an authorized peer in the Auth - * DB based on a proposal hash. Note - * that the hash to find is specific - * to the state of BIP151. This results - * in an O(n) search. - * @param {Buffer} hash - * @returns {Buffer|null} - */ - -BIP150.prototype.findAuthorized = function findAuthorized(hash) { - // Scary O(n) stuff. - for (const key of this.db.authorized) { - const msg = this.hash(this.output.sid, 'p', key); - - // XXX Do we really need a constant - // time compare here? Do it just to - // be safe I guess. - if (ccmp(msg, hash)) - return key; + this.reject(new Error('BIP150 stream was destroyed.')); } - return null; -}; + /** + * Cleanup wait job. + * @private + * @returns {Job} + */ -/** - * Destroy the BIP150 stream and - * any current running wait job. - */ + cleanup() { + const job = this.job; -BIP150.prototype.destroy = function destroy() { - if (!this.job) - return; + assert(!this.completed, 'Already completed.'); + assert(job, 'No completion job.'); - this.reject(new Error('BIP150 stream was destroyed.')); -}; + this.completed = true; + this.job = null; -/** - * Cleanup wait job. - * @private - * @returns {Job} - */ + if (this.timeout != null) { + clearTimeout(this.timeout); + this.timeout = null; + } -BIP150.prototype.cleanup = function cleanup() { - const job = this.job; + if (this.onAuth) { + this.removeListener('auth', this.onAuth); + this.onAuth = null; + } - assert(!this.completed, 'Already completed.'); - assert(job, 'No completion job.'); - - this.completed = true; - this.job = null; - - if (this.timeout != null) { - clearTimeout(this.timeout); - this.timeout = null; + return job; } - if (this.onAuth) { - this.removeListener('auth', this.onAuth); - this.onAuth = null; + /** + * Resolve the current wait job. + * @private + * @param {Object} result + */ + + resolve(result) { + const job = this.cleanup(); + job.resolve(result); } - return job; -}; + /** + * Reject the current wait job. + * @private + * @param {Error} err + */ -/** - * Resolve the current wait job. - * @private - * @param {Object} result - */ - -BIP150.prototype.resolve = function resolve(result) { - const job = this.cleanup(); - job.resolve(result); -}; - -/** - * Reject the current wait job. - * @private - * @param {Error} err - */ - -BIP150.prototype.reject = function reject(err) { - const job = this.cleanup(); - job.reject(err); -}; - -/** - * Wait for handshake to complete. - * @param {Number} timeout - * @returns {Promise} - */ - -BIP150.prototype.wait = function wait(timeout) { - return new Promise((resolve, reject) => { - this._wait(timeout, resolve, reject); - }); -}; - -/** - * Wait for handshake to complete. - * @private - * @param {Number} timeout - * @param {Function} resolve - * @param {Function} reject - */ - -BIP150.prototype._wait = function _wait(timeout, resolve, reject) { - assert(!this.auth, 'Cannot wait for init after handshake.'); - - this.job = { resolve, reject }; - - if (this.outbound && !this.peerIdentity) { - this.reject(new Error(`No identity for ${this.hostname}.`)); - return; + reject(err) { + const job = this.cleanup(); + job.reject(err); } - this.timeout = setTimeout(() => { - this.reject(new Error('BIP150 handshake timed out.')); - }, timeout); + /** + * Wait for handshake to complete. + * @param {Number} timeout + * @returns {Promise} + */ - this.onAuth = this.resolve.bind(this); - this.once('auth', this.onAuth); -}; + wait(timeout) { + return new Promise((resolve, reject) => { + this._wait(timeout, resolve, reject); + }); + } -/** - * Serialize the peer's identity - * key as a BIP150 "address". - * @returns {Base58String} - */ + /** + * Wait for handshake to complete. + * @private + * @param {Number} timeout + * @param {Function} resolve + * @param {Function} reject + */ -BIP150.prototype.getAddress = function getAddress() { - assert(this.peerIdentity, 'Cannot serialize address.'); - return BIP150.address(this.peerIdentity); -}; + _wait(timeout, resolve, reject) { + assert(!this.auth, 'Cannot wait for init after handshake.'); -/** - * Serialize an identity key as a - * BIP150 "address". - * @returns {Base58String} - */ + this.job = { resolve, reject }; -BIP150.address = function address(key) { - const bw = bio.write(27); - bw.writeU8(0x0f); - bw.writeU16BE(0xff01); - bw.writeBytes(hash160.digest(key)); - bw.writeChecksum(hash256.digest); - return base58.encode(bw.render()); -}; + if (this.outbound && !this.peerIdentity) { + this.reject(new Error(`No identity for ${this.hostname}.`)); + return; + } + + this.timeout = setTimeout(() => { + this.reject(new Error('BIP150 handshake timed out.')); + }, timeout); + + this.onAuth = this.resolve.bind(this); + this.once('auth', this.onAuth); + } + + /** + * Serialize the peer's identity + * key as a BIP150 "address". + * @returns {Base58String} + */ + + getAddress() { + assert(this.peerIdentity, 'Cannot serialize address.'); + return BIP150.address(this.peerIdentity); + } + + /** + * Serialize an identity key as a + * BIP150 "address". + * @returns {Base58String} + */ + + static address(key) { + const bw = bio.write(27); + bw.writeU8(0x0f); + bw.writeU16BE(0xff01); + bw.writeBytes(hash160.digest(key)); + bw.writeChecksum(hash256.digest); + return base58.encode(bw.render()); + } +} /** * AuthDB * @alias module:net.AuthDB - * @constructor */ -function AuthDB(options) { - if (!(this instanceof AuthDB)) - return new AuthDB(options); +class AuthDB { + /** + * Create an auth DB. + * @constructor + */ - this.logger = Logger.global; - this.resolve = dns.lookup; - this.prefix = null; - this.dnsKnown = []; + constructor(options) { + this.logger = Logger.global; + this.resolve = dns.lookup; + this.prefix = null; + this.dnsKnown = []; - this.known = new Map(); - this.authorized = []; + this.known = new Map(); + this.authorized = []; - this.init(options); -} - -/** - * Initialize authdb with options. - * @param {Object} options - */ - -AuthDB.prototype.init = function init(options) { - if (!options) - return; - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger.context('authdb'); + this.init(options); } - if (options.resolve != null) { - assert(typeof options.resolve === 'function'); - this.resolve = options.resolve; + /** + * Initialize authdb with options. + * @param {Object} options + */ + + init(options) { + if (!options) + return; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger.context('authdb'); + } + + if (options.resolve != null) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.knownPeers != null) { + assert(typeof options.knownPeers === 'object'); + this.setKnown(options.knownPeers); + } + + if (options.authPeers != null) { + assert(Array.isArray(options.authPeers)); + this.setAuthorized(options.authPeers); + } + + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + } } - if (options.knownPeers != null) { - assert(typeof options.knownPeers === 'object'); - this.setKnown(options.knownPeers); + /** + * Open auth database (lookup known peers). + * @method + * @returns {Promise} + */ + + async open() { + await this.readKnown(); + await this.readAuth(); + await this.lookup(); } - if (options.authPeers != null) { - assert(Array.isArray(options.authPeers)); - this.setAuthorized(options.authPeers); + /** + * Close auth database. + * @method + * @returns {Promise} + */ + + async close() { + ; } - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - } -}; + /** + * Add a known peer. + * @param {String} host - Peer Hostname + * @param {Buffer} key - Identity Key + */ -/** - * Open auth database (lookup known peers). - * @method - * @returns {Promise} - */ + addKnown(host, key) { + assert(typeof host === 'string', + 'Known host must be a string.'); -AuthDB.prototype.open = async function open() { - await this.readKnown(); - await this.readAuth(); - await this.lookup(); -}; + assert(Buffer.isBuffer(key) && key.length === 33, + 'Invalid public key for known peer.'); -/** - * Close auth database. - * @method - * @returns {Promise} - */ + const addr = IP.fromHostname(host); -AuthDB.prototype.close = async function close() { - ; -}; - -/** - * Add a known peer. - * @param {String} host - Peer Hostname - * @param {Buffer} key - Identity Key - */ - -AuthDB.prototype.addKnown = function addKnown(host, key) { - assert(typeof host === 'string', - 'Known host must be a string.'); - - assert(Buffer.isBuffer(key) && key.length === 33, - 'Invalid public key for known peer.'); - - const addr = IP.fromHostname(host); - - if (addr.type === IP.types.DNS) { - // Defer this for resolution. - this.dnsKnown.push([addr, key]); - return; - } - - this.known.set(host, key); -}; - -/** - * Add an authorized peer. - * @param {Buffer} key - Identity Key - */ - -AuthDB.prototype.addAuthorized = function addAuthorized(key) { - assert(Buffer.isBuffer(key) && key.length === 33, - 'Invalid public key for authorized peer.'); - this.authorized.push(key); -}; - -/** - * Initialize known peers with a host->key map. - * @param {Object} map - */ - -AuthDB.prototype.setKnown = function setKnown(map) { - this.known.clear(); - - for (const host of Object.keys(map)) { - const key = map[host]; - this.addKnown(host, key); - } -}; - -/** - * Initialize authorized peers with a list of keys. - * @param {Buffer[]} keys - */ - -AuthDB.prototype.setAuthorized = function setAuthorized(keys) { - this.authorized.length = 0; - - for (const key of keys) - this.addAuthorized(key); -}; - -/** - * Get a known peer key by hostname. - * @param {String} hostname - * @returns {Buffer|null} - */ - -AuthDB.prototype.getKnown = function getKnown(hostname) { - const known = this.known.get(hostname); - - if (known) - return known; - - const addr = IP.fromHostname(hostname); - - return this.known.get(addr.host); -}; - -/** - * Lookup known peers. - * @method - * @returns {Promise} - */ - -AuthDB.prototype.lookup = async function lookup() { - const jobs = []; - - for (const [addr, key] of this.dnsKnown) - jobs.push(this.populate(addr, key)); - - await Promise.all(jobs); -}; - -/** - * Populate known peers with hosts. - * @method - * @private - * @param {Object} addr - * @param {Buffer} key - * @returns {Promise} - */ - -AuthDB.prototype.populate = async function populate(addr, key) { - assert(addr.type === IP.types.DNS, 'Resolved host passed.'); - - this.logger.info('Resolving authorized hosts from: %s.', addr.host); - - let hosts; - try { - hosts = await this.resolve(addr.host); - } catch (e) { - this.logger.error(e); - return; - } - - for (let host of hosts) { - if (addr.port !== 0) - host = IP.toHostname(host, addr.port); + if (addr.type === IP.types.DNS) { + // Defer this for resolution. + this.dnsKnown.push([addr, key]); + return; + } this.known.set(host, key); } -}; -/** - * Parse known peers. - * @param {String} text - * @returns {Object} - */ + /** + * Add an authorized peer. + * @param {Buffer} key - Identity Key + */ -AuthDB.prototype.readKnown = async function readKnown() { - if (fs.unsupported) - return; - - if (!this.prefix) - return; - - const file = path.join(this.prefix, 'known-peers'); - - let text; - try { - text = await fs.readFile(file, 'utf8'); - } catch (e) { - if (e.code === 'ENOENT') - return; - throw e; + addAuthorized(key) { + assert(Buffer.isBuffer(key) && key.length === 33, + 'Invalid public key for authorized peer.'); + this.authorized.push(key); } - this.parseKnown(text); -}; + /** + * Initialize known peers with a host->key map. + * @param {Object} map + */ -/** - * Parse known peers. - * @param {String} text - * @returns {Object} - */ + setKnown(map) { + this.known.clear(); -AuthDB.prototype.parseKnown = function parseKnown(text) { - assert(typeof text === 'string'); + for (const host of Object.keys(map)) { + const key = map[host]; + this.addKnown(host, key); + } + } - if (text.charCodeAt(0) === 0xfeff) - text = text.substring(1); + /** + * Initialize authorized peers with a list of keys. + * @param {Buffer[]} keys + */ - text = text.replace(/\r\n/g, '\n'); - text = text.replace(/\r/g, '\n'); + setAuthorized(keys) { + this.authorized.length = 0; - let num = 0; + for (const key of keys) + this.addAuthorized(key); + } - for (const chunk of text.split('\n')) { - const line = chunk.trim(); + /** + * Get a known peer key by hostname. + * @param {String} hostname + * @returns {Buffer|null} + */ - num += 1; + getKnown(hostname) { + const known = this.known.get(hostname); - if (line.length === 0) - continue; + if (known) + return known; - if (line[0] === '#') - continue; + const addr = IP.fromHostname(hostname); - const parts = line.split(/\s+/); + return this.known.get(addr.host); + } - if (parts.length < 2) - throw new Error(`No key present on line ${num}: "${line}".`); + /** + * Lookup known peers. + * @method + * @returns {Promise} + */ - const hosts = parts[0].split(','); + async lookup() { + const jobs = []; - let host, addr; - if (hosts.length >= 2) { - host = hosts[0]; - addr = hosts[1]; - } else { - host = null; - addr = hosts[0]; + for (const [addr, key] of this.dnsKnown) + jobs.push(this.populate(addr, key)); + + await Promise.all(jobs); + } + + /** + * Populate known peers with hosts. + * @method + * @private + * @param {Object} addr + * @param {Buffer} key + * @returns {Promise} + */ + + async populate(addr, key) { + assert(addr.type === IP.types.DNS, 'Resolved host passed.'); + + this.logger.info('Resolving authorized hosts from: %s.', addr.host); + + let hosts; + try { + hosts = await this.resolve(addr.host); + } catch (e) { + this.logger.error(e); + return; } - const key = Buffer.from(parts[1], 'hex'); + for (let host of hosts) { + if (addr.port !== 0) + host = IP.toHostname(host, addr.port); - if (key.length !== 33) - throw new Error(`Invalid key on line ${num}: "${parts[1]}".`); - - if (host && host.length > 0) - this.addKnown(host, key); - - if (addr.length === 0) - continue; - - this.addKnown(addr, key); + this.known.set(host, key); + } } -}; -/** - * Parse known peers. - * @param {String} text - * @returns {Object} - */ + /** + * Parse known peers. + * @param {String} text + * @returns {Object} + */ -AuthDB.prototype.readAuth = async function readAuth() { - if (fs.unsupported) - return; - - if (!this.prefix) - return; - - const file = path.join(this.prefix, 'authorized-peers'); - - let text; - try { - text = await fs.readFile(file, 'utf8'); - } catch (e) { - if (e.code === 'ENOENT') + async readKnown() { + if (fs.unsupported) return; - throw e; + + if (!this.prefix) + return; + + const file = path.join(this.prefix, 'known-peers'); + + let text; + try { + text = await fs.readFile(file, 'utf8'); + } catch (e) { + if (e.code === 'ENOENT') + return; + throw e; + } + + this.parseKnown(text); } - this.parseAuth(text); -}; + /** + * Parse known peers. + * @param {String} text + * @returns {Object} + */ -/** - * Parse authorized peers. - * @param {String} text - * @returns {Buffer[]} keys - */ + parseKnown(text) { + assert(typeof text === 'string'); -AuthDB.prototype.parseAuth = function parseAuth(text) { - assert(typeof text === 'string'); + if (text.charCodeAt(0) === 0xfeff) + text = text.substring(1); - if (text.charCodeAt(0) === 0xfeff) - text = text.substring(1); + text = text.replace(/\r\n/g, '\n'); + text = text.replace(/\r/g, '\n'); - text = text.replace(/\r\n/g, '\n'); - text = text.replace(/\r/g, '\n'); + let num = 0; - let num = 0; + for (const chunk of text.split('\n')) { + const line = chunk.trim(); - for (const chunk of text.split('\n')) { - const line = chunk.trim(); + num += 1; - num += 1; + if (line.length === 0) + continue; - if (line.length === 0) - continue; + if (line[0] === '#') + continue; - if (line[0] === '#') - continue; + const parts = line.split(/\s+/); - const key = Buffer.from(line, 'hex'); + if (parts.length < 2) + throw new Error(`No key present on line ${num}: "${line}".`); - if (key.length !== 33) - throw new Error(`Invalid key on line ${num}: "${line}".`); + const hosts = parts[0].split(','); - this.addAuthorized(key); + let host, addr; + if (hosts.length >= 2) { + host = hosts[0]; + addr = hosts[1]; + } else { + host = null; + addr = hosts[0]; + } + + const key = Buffer.from(parts[1], 'hex'); + + if (key.length !== 33) + throw new Error(`Invalid key on line ${num}: "${parts[1]}".`); + + if (host && host.length > 0) + this.addKnown(host, key); + + if (addr.length === 0) + continue; + + this.addKnown(addr, key); + } } -}; + + /** + * Parse known peers. + * @param {String} text + * @returns {Object} + */ + + async readAuth() { + if (fs.unsupported) + return; + + if (!this.prefix) + return; + + const file = path.join(this.prefix, 'authorized-peers'); + + let text; + try { + text = await fs.readFile(file, 'utf8'); + } catch (e) { + if (e.code === 'ENOENT') + return; + throw e; + } + + this.parseAuth(text); + } + + /** + * Parse authorized peers. + * @param {String} text + * @returns {Buffer[]} keys + */ + + parseAuth(text) { + assert(typeof text === 'string'); + + if (text.charCodeAt(0) === 0xfeff) + text = text.substring(1); + + text = text.replace(/\r\n/g, '\n'); + text = text.replace(/\r/g, '\n'); + + let num = 0; + + for (const chunk of text.split('\n')) { + const line = chunk.trim(); + + num += 1; + + if (line.length === 0) + continue; + + if (line[0] === '#') + continue; + + const key = Buffer.from(line, 'hex'); + + if (key.length !== 33) + throw new Error(`Invalid key on line ${num}: "${line}".`); + + this.addAuthorized(key); + } + } +} /* * Expose diff --git a/lib/net/bip151.js b/lib/net/bip151.js index cb6f7dff..1d8f68e0 100644 --- a/lib/net/bip151.js +++ b/lib/net/bip151.js @@ -39,10 +39,9 @@ const INFO_SID = Buffer.from('BitcoinSessionID', 'ascii'); const HIGH_WATERMARK = 1024 * (1 << 20); /** + * BIP151 Stream * Represents a BIP151 input or output stream. * @alias module:net.BIP151Stream - * @constructor - * @param {Number} cipher * @property {Buffer} publicKey * @property {Buffer} privateKey * @property {Number} cipher @@ -57,232 +56,237 @@ const HIGH_WATERMARK = 1024 * (1 << 20); * @property {Number} lastKey */ -function BIP151Stream(cipher) { - if (!(this instanceof BIP151Stream)) - return new BIP151Stream(cipher); +class BIP151Stream { + /** + * Create a BIP151 input or output stream. + * @constructor + * @param {Number} cipher + */ - this.cipher = BIP151.ciphers.CHACHAPOLY; - this.privateKey = secp256k1.generatePrivateKey(); - this.publicKey = null; - this.k1 = null; - this.k2 = null; - this.sid = null; + constructor(cipher) { + this.cipher = BIP151.ciphers.CHACHAPOLY; + this.privateKey = secp256k1.generatePrivateKey(); + this.publicKey = null; + this.k1 = null; + this.k2 = null; + this.sid = null; - if (cipher != null) { - assert(cipher === BIP151.ciphers.CHACHAPOLY, 'Unknown cipher type.'); - this.cipher = cipher; + if (cipher != null) { + assert(cipher === BIP151.ciphers.CHACHAPOLY, 'Unknown cipher type.'); + this.cipher = cipher; + } + + this.chacha = new ChaCha20(); + this.aead = new AEAD(); + this.tag = null; + this.seq = 0; + this.iv = Buffer.allocUnsafe(8); + this.iv.fill(0); + + this.processed = 0; + this.lastRekey = 0; } - this.chacha = new ChaCha20(); - this.aead = new AEAD(); - this.tag = null; - this.seq = 0; - this.iv = Buffer.allocUnsafe(8); - this.iv.fill(0); + /** + * Initialize the stream with peer's public key. + * Computes ecdh secret and chacha keys. + * @param {Buffer} publicKey + */ - this.processed = 0; - this.lastRekey = 0; + init(publicKey) { + assert(Buffer.isBuffer(publicKey)); + + this.publicKey = publicKey; + + const secret = secp256k1.ecdh(this.publicKey, this.privateKey); + + const bw = bio.pool(33); + + bw.writeBytes(secret); + bw.writeU8(this.cipher); + + const data = bw.render(); + const prk = hkdf.extract(sha256, data, HKDF_SALT); + + this.k1 = hkdf.expand(sha256, prk, INFO_KEY1, 32); + this.k2 = hkdf.expand(sha256, prk, INFO_KEY2, 32); + this.sid = hkdf.expand(sha256, prk, INFO_SID, 32); + + this.seq = 0; + + this.update(); + + this.chacha.init(this.k1, this.iv); + this.aead.init(this.k2, this.iv); + + this.lastRekey = util.now(); + } + + /** + * Add buffer size to `processed`, + * check whether we need to rekey. + * @param {Buffer} packet + * @returns {Boolean} + */ + + shouldRekey(packet) { + const now = util.now(); + + this.processed += packet.length; + + if (now >= this.lastRekey + 10 + || this.processed >= HIGH_WATERMARK) { + this.lastRekey = now; + this.processed = 0; + return true; + } + + return false; + } + + /** + * Generate new chacha keys with `key = HASH256(sid | key)`. + * This will reinitialize the state of both ciphers. + */ + + rekey(k1, k2) { + assert(this.sid, 'Cannot rekey before initialization.'); + + if (!k1) { + this.k1 = hash256.root(this.sid, this.k1); + this.k2 = hash256.root(this.sid, this.k2); + } else { + this.k1 = k1; + this.k2 = k2; + } + + assert(this.k1); + assert(this.k2); + + // All state is reinitialized + // aside from the sequence number. + this.chacha.init(this.k1, this.iv); + this.aead.init(this.k2, this.iv); + } + + /** + * Increment packet sequence number and update IVs + * (note, sequence number overflows after 2^64-1). + * The IV will be updated without reinitializing + * cipher state. + */ + + sequence() { + // Wrap sequence number a la openssh. + if (++this.seq === 0x100000000) + this.seq = 0; + + this.update(); + + // State of the ciphers is + // unaltered aside from the iv. + this.chacha.init(null, this.iv); + this.aead.init(null, this.iv); + } + + /** + * Render the IV necessary for cipher streams. + * @returns {Buffer} + */ + + update() { + this.iv.writeUInt32LE(this.seq, 0, true); + return this.iv; + } + + /** + * Get public key tied to private key + * (not the same as BIP151Stream#publicKey). + * @returns {Buffer} + */ + + getPublicKey() { + return secp256k1.publicKeyCreate(this.privateKey, true); + } + + /** + * Encrypt a payload size with k1. + * @param {Buffer} data + * @returns {Buffer} + */ + + encryptSize(data) { + return this.chacha.encrypt(data.slice(0, 4)); + } + + /** + * Decrypt payload size with k1. + * @param {Buffer} data + * @returns {Number} + */ + + decryptSize(data) { + this.chacha.encrypt(data); + return data.readUInt32LE(0, true); + } + + /** + * Encrypt payload with AEAD (update cipher and mac). + * @param {Buffer} data + * @returns {Buffer} data + */ + + encrypt(data) { + return this.aead.encrypt(data); + } + + /** + * Decrypt payload with AEAD (update cipher only). + * @param {Buffer} data + * @returns {Buffer} data + */ + + decrypt(data) { + return this.aead.chacha20.encrypt(data); + } + + /** + * Authenticate payload with AEAD (update mac only). + * @param {Buffer} data + * @returns {Buffer} data + */ + + auth(data) { + return this.aead.auth(data); + } + + /** + * Finalize AEAD and compute MAC. + * @returns {Buffer} + */ + + final() { + this.tag = this.aead.final(); + return this.tag; + } + + /** + * Verify tag against mac in constant time. + * @param {Buffer} tag + * @returns {Boolean} + */ + + verify(tag) { + return Poly1305.verify(this.tag, tag); + } } /** - * Initialize the stream with peer's public key. - * Computes ecdh secret and chacha keys. - * @param {Buffer} publicKey - */ - -BIP151Stream.prototype.init = function init(publicKey) { - assert(Buffer.isBuffer(publicKey)); - - this.publicKey = publicKey; - - const secret = secp256k1.ecdh(this.publicKey, this.privateKey); - - const bw = bio.pool(33); - - bw.writeBytes(secret); - bw.writeU8(this.cipher); - - const data = bw.render(); - const prk = hkdf.extract(sha256, data, HKDF_SALT); - - this.k1 = hkdf.expand(sha256, prk, INFO_KEY1, 32); - this.k2 = hkdf.expand(sha256, prk, INFO_KEY2, 32); - this.sid = hkdf.expand(sha256, prk, INFO_SID, 32); - - this.seq = 0; - - this.update(); - - this.chacha.init(this.k1, this.iv); - this.aead.init(this.k2, this.iv); - - this.lastRekey = util.now(); -}; - -/** - * Add buffer size to `processed`, - * check whether we need to rekey. - * @param {Buffer} packet - * @returns {Boolean} - */ - -BIP151Stream.prototype.shouldRekey = function shouldRekey(packet) { - const now = util.now(); - - this.processed += packet.length; - - if (now >= this.lastRekey + 10 - || this.processed >= HIGH_WATERMARK) { - this.lastRekey = now; - this.processed = 0; - return true; - } - - return false; -}; - -/** - * Generate new chacha keys with `key = HASH256(sid | key)`. - * This will reinitialize the state of both ciphers. - */ - -BIP151Stream.prototype.rekey = function rekey(k1, k2) { - assert(this.sid, 'Cannot rekey before initialization.'); - - if (!k1) { - this.k1 = hash256.root(this.sid, this.k1); - this.k2 = hash256.root(this.sid, this.k2); - } else { - this.k1 = k1; - this.k2 = k2; - } - - assert(this.k1); - assert(this.k2); - - // All state is reinitialized - // aside from the sequence number. - this.chacha.init(this.k1, this.iv); - this.aead.init(this.k2, this.iv); -}; - -/** - * Increment packet sequence number and update IVs - * (note, sequence number overflows after 2^64-1). - * The IV will be updated without reinitializing - * cipher state. - */ - -BIP151Stream.prototype.sequence = function sequence() { - // Wrap sequence number a la openssh. - if (++this.seq === 0x100000000) - this.seq = 0; - - this.update(); - - // State of the ciphers is - // unaltered aside from the iv. - this.chacha.init(null, this.iv); - this.aead.init(null, this.iv); -}; - -/** - * Render the IV necessary for cipher streams. - * @returns {Buffer} - */ - -BIP151Stream.prototype.update = function update() { - this.iv.writeUInt32LE(this.seq, 0, true); - return this.iv; -}; - -/** - * Get public key tied to private key - * (not the same as BIP151Stream#publicKey). - * @returns {Buffer} - */ - -BIP151Stream.prototype.getPublicKey = function getPublicKey() { - return secp256k1.publicKeyCreate(this.privateKey, true); -}; - -/** - * Encrypt a payload size with k1. - * @param {Buffer} data - * @returns {Buffer} - */ - -BIP151Stream.prototype.encryptSize = function encryptSize(data) { - return this.chacha.encrypt(data.slice(0, 4)); -}; - -/** - * Decrypt payload size with k1. - * @param {Buffer} data - * @returns {Number} - */ - -BIP151Stream.prototype.decryptSize = function decryptSize(data) { - this.chacha.encrypt(data); - return data.readUInt32LE(0, true); -}; - -/** - * Encrypt payload with AEAD (update cipher and mac). - * @param {Buffer} data - * @returns {Buffer} data - */ - -BIP151Stream.prototype.encrypt = function encrypt(data) { - return this.aead.encrypt(data); -}; - -/** - * Decrypt payload with AEAD (update cipher only). - * @param {Buffer} data - * @returns {Buffer} data - */ - -BIP151Stream.prototype.decrypt = function decrypt(data) { - return this.aead.chacha20.encrypt(data); -}; - -/** - * Authenticate payload with AEAD (update mac only). - * @param {Buffer} data - * @returns {Buffer} data - */ - -BIP151Stream.prototype.auth = function auth(data) { - return this.aead.auth(data); -}; - -/** - * Finalize AEAD and compute MAC. - * @returns {Buffer} - */ - -BIP151Stream.prototype.final = function final() { - this.tag = this.aead.final(); - return this.tag; -}; - -/** - * Verify tag against mac in constant time. - * @param {Buffer} tag - * @returns {Boolean} - */ - -BIP151Stream.prototype.verify = function verify(tag) { - return Poly1305.verify(this.tag, tag); -}; - -/** + * BIP151 * Represents a BIP151 input and output stream. * Holds state for peer communication. * @alias module:net.BIP151 - * @constructor - * @param {Number} cipher + * @extends EventEmitter * @property {BIP151Stream} input * @property {BIP151Stream} output * @property {Boolean} initReceived @@ -295,36 +299,443 @@ BIP151Stream.prototype.verify = function verify(tag) { * @property {Boolean} handshake */ -function BIP151(cipher) { - if (!(this instanceof BIP151)) - return new BIP151(cipher); +class BIP151 extends EventEmitter { + /** + * Create a BIP151 input and output stream. + * @constructor + * @param {Number} cipher + */ - EventEmitter.call(this); + constructor(cipher) { + super(); - this.input = new BIP151Stream(cipher); - this.output = new BIP151Stream(cipher); + this.input = new BIP151Stream(cipher); + this.output = new BIP151Stream(cipher); - this.initReceived = false; - this.ackReceived = false; - this.initSent = false; - this.ackSent = false; - this.completed = false; - this.handshake = false; + this.initReceived = false; + this.ackReceived = false; + this.initSent = false; + this.ackSent = false; + this.completed = false; + this.handshake = false; - this.pending = []; - this.total = 0; - this.waiting = 4; - this.hasSize = false; + this.pending = []; + this.total = 0; + this.waiting = 4; + this.hasSize = false; - this.timeout = null; - this.job = null; - this.onShake = null; + this.timeout = null; + this.job = null; + this.onShake = null; - this.bip150 = null; + this.bip150 = null; + } + + /** + * Emit an error. + * @param {...String} msg + */ + + error() { + const msg = format.apply(null, arguments); + this.emit('error', new Error(msg)); + } + + /** + * Test whether handshake has completed. + * @returns {Boolean} + */ + + isReady() { + return this.initSent + && this.ackReceived + && this.initReceived + && this.ackSent; + } + + /** + * Render an `encinit` packet. Contains the + * input public key and cipher number. + * @returns {Buffer} + */ + + toEncinit() { + assert(!this.initSent, 'Cannot init twice.'); + this.initSent = true; + return new EncinitPacket(this.input.getPublicKey(), this.input.cipher); + } + + /** + * Render `encack` packet. Contains the + * output stream public key. + * @returns {Buffer} + */ + + toEncack() { + assert(this.output.sid, 'Cannot ack before init.'); + assert(!this.ackSent, 'Cannot ack twice.'); + this.ackSent = true; + + if (this.isReady()) { + assert(!this.completed, 'No encack after timeout.'); + this.handshake = true; + this.emit('handshake'); + } + + return new EncackPacket(this.output.getPublicKey()); + } + + /** + * Render `encack` packet with an all + * zero public key, notifying of a rekey + * for the output stream. + * @returns {Buffer} + */ + + toRekey() { + assert(this.handshake, 'Cannot rekey before handshake.'); + return new EncackPacket(encoding.ZERO_KEY); + } + + /** + * Handle `encinit` from remote peer. + * @param {Buffer} + */ + + encinit(publicKey, cipher) { + assert(cipher === this.output.cipher, 'Cipher mismatch.'); + assert(!this.initReceived, 'Already initialized.'); + assert(!this.completed, 'No encinit after timeout.'); + this.initReceived = true; + this.output.init(publicKey); + } + + /** + * Handle `encack` from remote peer. + * @param {Buffer} data + */ + + encack(publicKey) { + assert(this.initSent, 'Unsolicited ACK.'); + + if (publicKey.equals(encoding.ZERO_KEY)) { + assert(this.handshake, 'No initialization before rekey.'); + + if (this.bip150 && this.bip150.auth) { + this.bip150.rekeyInput(); + return; + } + + this.input.rekey(); + + return; + } + + assert(!this.ackReceived, 'Already ACKed.'); + assert(!this.completed, 'No encack after timeout.'); + this.ackReceived = true; + + this.input.init(publicKey); + + if (this.isReady()) { + this.handshake = true; + this.emit('handshake'); + } + } + + /** + * Cleanup handshake job. + * @returns {Job} + */ + + cleanup() { + const job = this.job; + + assert(!this.completed, 'Already completed.'); + assert(job, 'No completion job.'); + + this.completed = true; + this.job = null; + + if (this.timeout != null) { + clearTimeout(this.timeout); + this.timeout = null; + } + + if (this.onShake) { + this.removeListener('handshake', this.onShake); + this.onShake = null; + } + + return job; + } + + /** + * Complete the timeout for handshake. + * @param {Object} result + */ + + resolve(result) { + const job = this.cleanup(); + job.resolve(result); + } + + /** + * Complete the timeout for handshake with error. + * @param {Error} err + */ + + reject(err) { + const job = this.cleanup(); + job.reject(err); + } + + /** + * Set a timeout and wait for handshake to complete. + * @param {Number} timeout - Timeout in ms. + * @returns {Promise} + */ + + wait(timeout) { + return new Promise((resolve, reject) => { + this._wait(timeout, resolve, reject); + }); + } + + /** + * Set a timeout and wait for handshake to complete. + * @private + * @param {Number} timeout + * @param {Function} resolve + * @param {Function} reject + */ + + _wait(timeout, resolve, reject) { + assert(!this.handshake, 'Cannot wait for init after handshake.'); + + this.job = { resolve, reject }; + + this.timeout = setTimeout(() => { + this.reject(new Error('BIP151 handshake timed out.')); + }, timeout); + + this.onShake = this.resolve.bind(this); + this.once('handshake', this.onShake); + } + + /** + * Destroy BIP151 state and streams. + */ + + destroy() { + if (!this.job) + return; + + this.reject(new Error('BIP151 stream was destroyed.')); + } + + /** + * Add buffer size to `processed`, + * check whether we need to rekey. + * @param {Buffer} packet + */ + + maybeRekey(packet) { + if (!this.output.shouldRekey(packet)) + return; + + this.emit('rekey'); + + if (this.bip150 && this.bip150.auth) { + this.bip150.rekeyOutput(); + return; + } + + this.output.rekey(); + } + + /** + * Calculate packet size. + * @param {String} cmd + * @param {Buffer} body + * @returns {Number} + */ + + packetSize(cmd, body) { + let size = 0; + size += 4; + size += encoding.sizeVarString(cmd, 'ascii'); + size += 4; + size += body.length; + size += 16; + return size; + } + + /** + * Frame plaintext payload for the output stream. + * @param {String} cmd + * @param {Buffer} body + * @returns {Buffer} Ciphertext payload + */ + + packet(cmd, body) { + const size = this.packetSize(cmd, body); + const bw = bio.write(size); + const payloadSize = size - 20; + + bw.writeU32(payloadSize); + bw.writeVarString(cmd, 'ascii'); + bw.writeU32(body.length); + bw.writeBytes(body); + bw.seek(16); + + const msg = bw.render(); + const payload = msg.slice(4, 4 + payloadSize); + + this.maybeRekey(msg); + + this.output.encryptSize(msg); + this.output.encrypt(payload); + this.output.final().copy(msg, 4 + payloadSize); + this.output.sequence(); + + return msg; + } + + /** + * Feed ciphertext payload chunk + * to the input stream. Potentially + * emits a `packet` event. + * @param {Buffer} data + */ + + feed(data) { + this.total += data.length; + this.pending.push(data); + + while (this.total >= this.waiting) { + const chunk = this.read(this.waiting); + this.parse(chunk); + } + } + + /** + * Read and consume a number of bytes + * from the buffered stream. + * @param {Number} size + * @returns {Buffer} + */ + + read(size) { + assert(this.total >= size, 'Reading too much.'); + + if (size === 0) + return Buffer.alloc(0); + + const pending = this.pending[0]; + + if (pending.length > size) { + const chunk = pending.slice(0, size); + this.pending[0] = pending.slice(size); + this.total -= chunk.length; + return chunk; + } + + if (pending.length === size) { + const chunk = this.pending.shift(); + this.total -= chunk.length; + return chunk; + } + + const chunk = Buffer.allocUnsafe(size); + let off = 0; + + while (off < chunk.length) { + const pending = this.pending[0]; + const len = pending.copy(chunk, off); + if (len === pending.length) + this.pending.shift(); + else + this.pending[0] = pending.slice(len); + off += len; + } + + assert.strictEqual(off, chunk.length); + + this.total -= chunk.length; + + return chunk; + } + + /** + * Parse a ciphertext payload chunk. + * Potentially emits a `packet` event. + * @param {Buffer} data + */ + + parse(data) { + if (!this.hasSize) { + const size = this.input.decryptSize(data); + + assert(this.waiting === 4); + assert(data.length === 4); + + // Allow 3 batched packets of max message size (12mb). + // Not technically standard, but this protects us + // from buffering tons of data due to either an + // potential dos'er or a cipher state mismatch. + // Note that 6 is the minimum size: + // varint-cmdlen(1) str-cmd(1) u32-size(4) payload(0) + if (size < 6 || size > BIP151.MAX_MESSAGE) { + this.error('Bad packet size: %d.', size); + return; + } + + this.hasSize = true; + this.waiting = size + 16; + + return; + } + + const payload = data.slice(0, this.waiting - 16); + const tag = data.slice(this.waiting - 16, this.waiting); + + this.hasSize = false; + this.waiting = 4; + + // Authenticate payload before decrypting. + // This ensures the cipher state isn't altered + // if the payload integrity has been compromised. + this.input.auth(payload); + this.input.final(); + + if (!this.input.verify(tag)) { + this.input.sequence(); + this.error('Bad tag: %s.', tag.toString('hex')); + return; + } + + this.input.decrypt(payload); + this.input.sequence(); + + const br = bio.read(payload); + + while (br.left()) { + let cmd, body; + + try { + cmd = br.readVarString('ascii'); + body = br.readBytes(br.readU32()); + } catch (e) { + this.emit('error', e); + return; + } + + this.emit('packet', cmd, body); + } + } } -Object.setPrototypeOf(BIP151.prototype, EventEmitter.prototype); - /** * Cipher list. * @enum {Number} @@ -342,410 +753,6 @@ BIP151.ciphers = { BIP151.MAX_MESSAGE = 12 * 1000 * 1000; -/** - * Emit an error. - * @param {...String} msg - */ - -BIP151.prototype.error = function error() { - const msg = format.apply(null, arguments); - this.emit('error', new Error(msg)); -}; - -/** - * Test whether handshake has completed. - * @returns {Boolean} - */ - -BIP151.prototype.isReady = function isReady() { - return this.initSent - && this.ackReceived - && this.initReceived - && this.ackSent; -}; - -/** - * Render an `encinit` packet. Contains the - * input public key and cipher number. - * @returns {Buffer} - */ - -BIP151.prototype.toEncinit = function toEncinit() { - assert(!this.initSent, 'Cannot init twice.'); - this.initSent = true; - return new EncinitPacket(this.input.getPublicKey(), this.input.cipher); -}; - -/** - * Render `encack` packet. Contains the - * output stream public key. - * @returns {Buffer} - */ - -BIP151.prototype.toEncack = function toEncack() { - assert(this.output.sid, 'Cannot ack before init.'); - assert(!this.ackSent, 'Cannot ack twice.'); - this.ackSent = true; - - if (this.isReady()) { - assert(!this.completed, 'No encack after timeout.'); - this.handshake = true; - this.emit('handshake'); - } - - return new EncackPacket(this.output.getPublicKey()); -}; - -/** - * Render `encack` packet with an all - * zero public key, notifying of a rekey - * for the output stream. - * @returns {Buffer} - */ - -BIP151.prototype.toRekey = function toRekey() { - assert(this.handshake, 'Cannot rekey before handshake.'); - return new EncackPacket(encoding.ZERO_KEY); -}; - -/** - * Handle `encinit` from remote peer. - * @param {Buffer} - */ - -BIP151.prototype.encinit = function encinit(publicKey, cipher) { - assert(cipher === this.output.cipher, 'Cipher mismatch.'); - assert(!this.initReceived, 'Already initialized.'); - assert(!this.completed, 'No encinit after timeout.'); - this.initReceived = true; - this.output.init(publicKey); -}; - -/** - * Handle `encack` from remote peer. - * @param {Buffer} data - */ - -BIP151.prototype.encack = function encack(publicKey) { - assert(this.initSent, 'Unsolicited ACK.'); - - if (publicKey.equals(encoding.ZERO_KEY)) { - assert(this.handshake, 'No initialization before rekey.'); - - if (this.bip150 && this.bip150.auth) { - this.bip150.rekeyInput(); - return; - } - - this.input.rekey(); - - return; - } - - assert(!this.ackReceived, 'Already ACKed.'); - assert(!this.completed, 'No encack after timeout.'); - this.ackReceived = true; - - this.input.init(publicKey); - - if (this.isReady()) { - this.handshake = true; - this.emit('handshake'); - } -}; - -/** - * Cleanup handshake job. - * @returns {Job} - */ - -BIP151.prototype.cleanup = function cleanup() { - const job = this.job; - - assert(!this.completed, 'Already completed.'); - assert(job, 'No completion job.'); - - this.completed = true; - this.job = null; - - if (this.timeout != null) { - clearTimeout(this.timeout); - this.timeout = null; - } - - if (this.onShake) { - this.removeListener('handshake', this.onShake); - this.onShake = null; - } - - return job; -}; - -/** - * Complete the timeout for handshake. - * @param {Object} result - */ - -BIP151.prototype.resolve = function resolve(result) { - const job = this.cleanup(); - job.resolve(result); -}; - -/** - * Complete the timeout for handshake with error. - * @param {Error} err - */ - -BIP151.prototype.reject = function reject(err) { - const job = this.cleanup(); - job.reject(err); -}; - -/** - * Set a timeout and wait for handshake to complete. - * @param {Number} timeout - Timeout in ms. - * @returns {Promise} - */ - -BIP151.prototype.wait = function wait(timeout) { - return new Promise((resolve, reject) => { - this._wait(timeout, resolve, reject); - }); -}; - -/** - * Set a timeout and wait for handshake to complete. - * @private - * @param {Number} timeout - * @param {Function} resolve - * @param {Function} reject - */ - -BIP151.prototype._wait = function _wait(timeout, resolve, reject) { - assert(!this.handshake, 'Cannot wait for init after handshake.'); - - this.job = { resolve, reject }; - - this.timeout = setTimeout(() => { - this.reject(new Error('BIP151 handshake timed out.')); - }, timeout); - - this.onShake = this.resolve.bind(this); - this.once('handshake', this.onShake); -}; - -/** - * Destroy BIP151 state and streams. - */ - -BIP151.prototype.destroy = function destroy() { - if (!this.job) - return; - - this.reject(new Error('BIP151 stream was destroyed.')); -}; - -/** - * Add buffer size to `processed`, - * check whether we need to rekey. - * @param {Buffer} packet - */ - -BIP151.prototype.maybeRekey = function maybeRekey(packet) { - if (!this.output.shouldRekey(packet)) - return; - - this.emit('rekey'); - - if (this.bip150 && this.bip150.auth) { - this.bip150.rekeyOutput(); - return; - } - - this.output.rekey(); -}; - -/** - * Calculate packet size. - * @param {String} cmd - * @param {Buffer} body - * @returns {Number} - */ - -BIP151.prototype.packetSize = function packetSize(cmd, body) { - let size = 0; - size += 4; - size += encoding.sizeVarString(cmd, 'ascii'); - size += 4; - size += body.length; - size += 16; - return size; -}; - -/** - * Frame plaintext payload for the output stream. - * @param {String} cmd - * @param {Buffer} body - * @returns {Buffer} Ciphertext payload - */ - -BIP151.prototype.packet = function packet(cmd, body) { - const size = this.packetSize(cmd, body); - const bw = bio.write(size); - const payloadSize = size - 20; - - bw.writeU32(payloadSize); - bw.writeVarString(cmd, 'ascii'); - bw.writeU32(body.length); - bw.writeBytes(body); - bw.seek(16); - - const msg = bw.render(); - const payload = msg.slice(4, 4 + payloadSize); - - this.maybeRekey(msg); - - this.output.encryptSize(msg); - this.output.encrypt(payload); - this.output.final().copy(msg, 4 + payloadSize); - this.output.sequence(); - - return msg; -}; - -/** - * Feed ciphertext payload chunk - * to the input stream. Potentially - * emits a `packet` event. - * @param {Buffer} data - */ - -BIP151.prototype.feed = function feed(data) { - this.total += data.length; - this.pending.push(data); - - while (this.total >= this.waiting) { - const chunk = this.read(this.waiting); - this.parse(chunk); - } -}; - -/** - * Read and consume a number of bytes - * from the buffered stream. - * @param {Number} size - * @returns {Buffer} - */ - -BIP151.prototype.read = function read(size) { - assert(this.total >= size, 'Reading too much.'); - - if (size === 0) - return Buffer.alloc(0); - - const pending = this.pending[0]; - - if (pending.length > size) { - const chunk = pending.slice(0, size); - this.pending[0] = pending.slice(size); - this.total -= chunk.length; - return chunk; - } - - if (pending.length === size) { - const chunk = this.pending.shift(); - this.total -= chunk.length; - return chunk; - } - - const chunk = Buffer.allocUnsafe(size); - let off = 0; - - while (off < chunk.length) { - const pending = this.pending[0]; - const len = pending.copy(chunk, off); - if (len === pending.length) - this.pending.shift(); - else - this.pending[0] = pending.slice(len); - off += len; - } - - assert.strictEqual(off, chunk.length); - - this.total -= chunk.length; - - return chunk; -}; - -/** - * Parse a ciphertext payload chunk. - * Potentially emits a `packet` event. - * @param {Buffer} data - */ - -BIP151.prototype.parse = function parse(data) { - if (!this.hasSize) { - const size = this.input.decryptSize(data); - - assert(this.waiting === 4); - assert(data.length === 4); - - // Allow 3 batched packets of max message size (12mb). - // Not technically standard, but this protects us - // from buffering tons of data due to either an - // potential dos'er or a cipher state mismatch. - // Note that 6 is the minimum size: - // varint-cmdlen(1) str-cmd(1) u32-size(4) payload(0) - if (size < 6 || size > BIP151.MAX_MESSAGE) { - this.error('Bad packet size: %d.', size); - return; - } - - this.hasSize = true; - this.waiting = size + 16; - - return; - } - - const payload = data.slice(0, this.waiting - 16); - const tag = data.slice(this.waiting - 16, this.waiting); - - this.hasSize = false; - this.waiting = 4; - - // Authenticate payload before decrypting. - // This ensures the cipher state isn't altered - // if the payload integrity has been compromised. - this.input.auth(payload); - this.input.final(); - - if (!this.input.verify(tag)) { - this.input.sequence(); - this.error('Bad tag: %s.', tag.toString('hex')); - return; - } - - this.input.decrypt(payload); - this.input.sequence(); - - const br = bio.read(payload); - - while (br.left()) { - let cmd, body; - - try { - cmd = br.readVarString('ascii'); - body = br.readBytes(br.readU32()); - } catch (e) { - this.emit('error', e); - return; - } - - this.emit('packet', cmd, body); - } -}; - /* * Expose */ diff --git a/lib/net/framer.js b/lib/net/framer.js index 964ca450..57c90731 100644 --- a/lib/net/framer.js +++ b/lib/net/framer.js @@ -12,57 +12,60 @@ const Network = require('../protocol/network'); const hash256 = require('bcrypto/lib/hash256'); /** - * Protocol packet framer + * Protocol Message Framer * @alias module:net.Framer - * @constructor - * @param {Network} network */ -function Framer(network) { - if (!(this instanceof Framer)) - return new Framer(network); +class Framer { + /** + * Create a framer. + * @constructor + * @param {Network} network + */ - this.network = Network.get(network); + constructor(network) { + this.network = Network.get(network); + } + + /** + * Frame a payload with a header. + * @param {String} cmd - Packet type. + * @param {Buffer} payload + * @param {Buffer?} checksum - Precomputed checksum. + * @returns {Buffer} Payload with header prepended. + */ + + packet(cmd, payload, checksum) { + assert(payload, 'No payload.'); + assert(cmd.length < 12); + assert(payload.length <= 0xffffffff); + + const msg = Buffer.allocUnsafe(24 + payload.length); + + // Magic value + msg.writeUInt32LE(this.network.magic, 0, true); + + // Command + msg.write(cmd, 4, 'ascii'); + + for (let i = 4 + cmd.length; i < 16; i++) + msg[i] = 0; + + // Payload length + msg.writeUInt32LE(payload.length, 16, true); + + if (!checksum) + checksum = hash256.digest(payload); + + // Checksum + checksum.copy(msg, 20, 0, 4); + + payload.copy(msg, 24); + + return msg; + } } -/** - * Frame a payload with a header. - * @param {String} cmd - Packet type. - * @param {Buffer} payload - * @param {Buffer?} checksum - Precomputed checksum. - * @returns {Buffer} Payload with header prepended. - */ - -Framer.prototype.packet = function packet(cmd, payload, checksum) { - assert(payload, 'No payload.'); - assert(cmd.length < 12); - assert(payload.length <= 0xffffffff); - - const msg = Buffer.allocUnsafe(24 + payload.length); - - // Magic value - msg.writeUInt32LE(this.network.magic, 0, true); - - // Command - msg.write(cmd, 4, 'ascii'); - - for (let i = 4 + cmd.length; i < 16; i++) - msg[i] = 0; - - // Payload length - msg.writeUInt32LE(payload.length, 16, true); - - if (!checksum) - checksum = hash256.digest(payload); - - // Checksum - checksum.copy(msg, 20, 0, 4); - - payload.copy(msg, 24); - - return msg; -}; - /* * Expose */ diff --git a/lib/net/hostlist.js b/lib/net/hostlist.js index bafe88a3..b5013732 100644 --- a/lib/net/hostlist.js +++ b/lib/net/hostlist.js @@ -24,37 +24,1160 @@ const POOL32 = Buffer.allocUnsafe(32); /** * Host List * @alias module:net.HostList - * @constructor - * @param {Object} options */ -function HostList(options) { - if (!(this instanceof HostList)) - return new HostList(options); +class HostList { + /** + * Create a host list. + * @constructor + * @param {Object} options + */ - this.options = new HostListOptions(options); - this.network = this.options.network; - this.logger = this.options.logger.context('hostlist'); - this.address = this.options.address; - this.resolve = this.options.resolve; + constructor(options) { + this.options = new HostListOptions(options); + this.network = this.options.network; + this.logger = this.options.logger.context('hostlist'); + this.address = this.options.address; + this.resolve = this.options.resolve; - this.dnsSeeds = []; - this.dnsNodes = []; + this.dnsSeeds = []; + this.dnsNodes = []; - this.map = new Map(); - this.fresh = []; - this.totalFresh = 0; - this.used = []; - this.totalUsed = 0; - this.nodes = []; - this.local = new Map(); - this.banned = new Map(); + this.map = new Map(); + this.fresh = []; + this.totalFresh = 0; + this.used = []; + this.totalUsed = 0; + this.nodes = []; + this.local = new Map(); + this.banned = new Map(); - this.timer = null; - this.needsFlush = false; - this.flushing = false; + this.timer = null; + this.needsFlush = false; + this.flushing = false; - this.init(); + this.init(); + } + + /** + * Initialize list. + * @private + */ + + init() { + const options = this.options; + const scores = HostList.scores; + const hosts = IP.getPublic(); + const port = this.address.port; + + for (let i = 0; i < this.options.maxBuckets; i++) + this.fresh.push(new Map()); + + for (let i = 0; i < this.options.maxBuckets; i++) + this.used.push(new List()); + + this.setSeeds(options.seeds); + this.setNodes(options.nodes); + + this.pushLocal(this.address, scores.MANUAL); + this.addLocal(options.host, options.port, scores.BIND); + + for (const host of hosts) + this.addLocal(host, port, scores.IF); + } + + /** + * Open hostlist and read hosts file. + * @method + * @returns {Promise} + */ + + async open() { + try { + await this.loadFile(); + } catch (e) { + this.logger.warning('Hosts deserialization failed.'); + this.logger.error(e); + } + + if (this.size() === 0) + this.injectSeeds(); + + await this.discoverNodes(); + + this.start(); + } + + /** + * Close hostlist. + * @method + * @returns {Promise} + */ + + async close() { + this.stop(); + await this.flush(); + this.reset(); + } + + /** + * Start flush interval. + */ + + start() { + if (!this.options.persistent) + return; + + if (!this.options.filename) + return; + + assert(this.timer == null); + this.timer = setInterval(() => this.flush(), this.options.flushInterval); + } + + /** + * Stop flush interval. + */ + + stop() { + if (!this.options.persistent) + return; + + if (!this.options.filename) + return; + + assert(this.timer != null); + clearInterval(this.timer); + this.timer = null; + } + + /** + * Read and initialize from hosts file. + * @method + * @returns {Promise} + */ + + injectSeeds() { + const nodes = seeds.get(this.network.type); + + for (const node of nodes) { + const addr = NetAddress.fromHostname(node, this.network); + + if (!addr.isRoutable()) + continue; + + if (!this.options.onion && addr.isOnion()) + continue; + + if (addr.port === 0) + continue; + + this.add(addr); + } + } + + /** + * Read and initialize from hosts file. + * @method + * @returns {Promise} + */ + + async loadFile() { + const filename = this.options.filename; + + if (fs.unsupported) + return; + + if (!this.options.persistent) + return; + + if (!filename) + return; + + let data; + try { + data = await fs.readFile(filename, 'utf8'); + } catch (e) { + if (e.code === 'ENOENT') + return; + throw e; + } + + const json = JSON.parse(data); + + this.fromJSON(json); + } + + /** + * Flush addrs to hosts file. + * @method + * @returns {Promise} + */ + + async flush() { + const filename = this.options.filename; + + if (fs.unsupported) + return; + + if (!this.options.persistent) + return; + + if (!filename) + return; + + if (!this.needsFlush) + return; + + if (this.flushing) + return; + + this.needsFlush = false; + + this.logger.debug('Writing hosts to %s.', filename); + + const json = this.toJSON(); + const data = JSON.stringify(json); + + this.flushing = true; + + try { + await fs.writeFile(filename, data, 'utf8'); + } catch (e) { + this.logger.warning('Writing hosts failed.'); + this.logger.error(e); + } + + this.flushing = false; + } + + /** + * Get list size. + * @returns {Number} + */ + + size() { + return this.totalFresh + this.totalUsed; + } + + /** + * Test whether the host list is full. + * @returns {Boolean} + */ + + isFull() { + const max = this.options.maxBuckets * this.options.maxEntries; + return this.size() >= max; + } + + /** + * Reset host list. + */ + + reset() { + this.map.clear(); + + for (const bucket of this.fresh) + bucket.clear(); + + for (const bucket of this.used) + bucket.reset(); + + this.totalFresh = 0; + this.totalUsed = 0; + + this.nodes.length = 0; + } + + /** + * Mark a peer as banned. + * @param {String} host + */ + + ban(host) { + this.banned.set(host, util.now()); + } + + /** + * Unban host. + * @param {String} host + */ + + unban(host) { + this.banned.delete(host); + } + + /** + * Clear banned hosts. + */ + + clearBanned() { + this.banned.clear(); + } + + /** + * Test whether the host is banned. + * @param {String} host + * @returns {Boolean} + */ + + isBanned(host) { + const time = this.banned.get(host); + + if (time == null) + return false; + + if (util.now() > time + this.options.banTime) { + this.banned.delete(host); + return false; + } + + return true; + } + + /** + * Allocate a new host. + * @returns {HostEntry} + */ + + getHost() { + let buckets = null; + + if (this.totalFresh > 0) + buckets = this.fresh; + + if (this.totalUsed > 0) { + if (this.totalFresh === 0 || random(2) === 0) + buckets = this.used; + } + + if (!buckets) + return null; + + const now = this.network.now(); + let factor = 1; + + for (;;) { + let index = random(buckets.length); + const bucket = buckets[index]; + + if (bucket.size === 0) + continue; + + index = random(bucket.size); + + let entry; + if (buckets === this.used) { + entry = bucket.head; + while (index--) + entry = entry.next; + } else { + for (entry of bucket.values()) { + if (index === 0) + break; + index--; + } + } + + const num = random(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 {Map} + */ + + freshBucket(entry) { + const addr = entry.addr; + const src = entry.src; + const data = concat32(addr.raw, src.raw); + const hash = murmur3.sum(data, 0xfba4c795); + const index = hash % this.fresh.length; + return this.fresh[index]; + } + + /** + * Get used bucket for host. + * @private + * @param {HostEntry} entry + * @returns {List} + */ + + usedBucket(entry) { + const addr = entry.addr; + const hash = murmur3.sum(addr.raw, 0xfba4c795); + const index = hash % this.used.length; + return this.used[index]; + } + + /** + * Add host to host list. + * @param {NetAddress} addr + * @param {NetAddress?} src + * @returns {Boolean} + */ + + add(addr, src) { + assert(addr.port !== 0); + + let entry = this.map.get(addr.hostname); + + if (entry) { + const now = this.network.now(); + let penalty = 2 * 60 * 60; + let interval = 24 * 60 * 60; + + // No source means we're inserting + // this ourselves. No penalty. + if (!src) + penalty = 0; + + // Update services. + entry.addr.services |= addr.services; + entry.addr.services >>>= 0; + + // Online? + if (now - addr.time < 24 * 60 * 60) + interval = 60 * 60; + + // Periodically update time. + if (entry.addr.time < addr.time - interval - penalty) { + entry.addr.time = addr.time; + this.needsFlush = true; + } + + // Do not update if no new + // information is present. + if (entry.addr.time && addr.time <= entry.addr.time) + 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 === HostList.MAX_REFS) + return false; + + assert(entry.refCount < HostList.MAX_REFS); + + // Stochastic test: previous refCount + // N: 2^N times harder to increase it. + let factor = 1; + for (let i = 0; i < entry.refCount; i++) + factor *= 2; + + if (random(factor) !== 0) + return false; + } else { + if (this.isFull()) + return false; + + if (!src) + src = this.address; + + entry = new HostEntry(addr, src); + + this.totalFresh++; + } + + const bucket = this.freshBucket(entry); + + if (bucket.has(entry.key())) + return false; + + if (bucket.size >= this.options.maxEntries) + this.evictFresh(bucket); + + bucket.set(entry.key(), entry); + entry.refCount++; + + this.map.set(entry.key(), entry); + this.needsFlush = true; + + return true; + } + + /** + * Evict a host from fresh bucket. + * @param {Map} bucket + */ + + evictFresh(bucket) { + let old = null; + + for (const entry of bucket.values()) { + if (this.isStale(entry)) { + bucket.delete(entry.key()); + + if (--entry.refCount === 0) { + this.map.delete(entry.key()); + this.totalFresh--; + } + + continue; + } + + if (!old) { + old = entry; + continue; + } + + if (entry.addr.time < old.addr.time) + old = entry; + } + + if (!old) + return; + + bucket.delete(old.key()); + + if (--old.refCount === 0) { + this.map.delete(old.key()); + this.totalFresh--; + } + } + + /** + * Test whether a host is evictable. + * @param {HostEntry} entry + * @returns {Boolean} + */ + + isStale(entry) { + const now = this.network.now(); + + if (entry.lastAttempt && entry.lastAttempt >= now - 60) + return false; + + if (entry.addr.time > now + 10 * 60) + return true; + + if (entry.addr.time === 0) + return true; + + if (now - entry.addr.time > HostList.HORIZON_DAYS * 24 * 60 * 60) + return true; + + if (entry.lastSuccess === 0 && entry.attempts >= HostList.RETRIES) + return true; + + if (now - entry.lastSuccess > HostList.MIN_FAIL_DAYS * 24 * 60 * 60) { + if (entry.attempts >= HostList.MAX_FAILURES) + return true; + } + + return false; + } + + /** + * Remove host from host list. + * @param {String} hostname + * @returns {NetAddress} + */ + + remove(hostname) { + const entry = this.map.get(hostname); + + if (!entry) + return null; + + if (entry.used) { + let head = entry; + + assert(entry.refCount === 0); + + while (head.prev) + head = head.prev; + + for (const bucket of this.used) { + if (bucket.head === head) { + bucket.remove(entry); + this.totalUsed--; + head = null; + break; + } + } + + assert(!head); + } else { + for (const bucket of this.fresh) { + if (bucket.delete(entry.key())) + entry.refCount--; + } + + this.totalFresh--; + assert(entry.refCount === 0); + } + + this.map.delete(entry.key()); + + return entry.addr; + } + + /** + * Mark host as failed. + * @param {String} hostname + */ + + markAttempt(hostname) { + const entry = this.map.get(hostname); + const now = this.network.now(); + + if (!entry) + return; + + entry.attempts++; + entry.lastAttempt = now; + } + + /** + * Mark host as successfully connected. + * @param {String} hostname + */ + + markSuccess(hostname) { + const entry = this.map.get(hostname); + const now = this.network.now(); + + if (!entry) + return; + + if (now - entry.addr.time > 20 * 60) + entry.addr.time = now; + } + + /** + * Mark host as successfully ack'd. + * @param {String} hostname + * @param {Number} services + */ + + markAck(hostname, services) { + const entry = this.map.get(hostname); + + if (!entry) + return; + + const now = this.network.now(); + + entry.addr.services |= services; + entry.addr.services >>>= 0; + + entry.lastSuccess = now; + entry.lastAttempt = now; + entry.attempts = 0; + + if (entry.used) + return; + + assert(entry.refCount > 0); + + // Remove from fresh. + let old; + for (const bucket of this.fresh) { + if (bucket.delete(entry.key())) { + entry.refCount--; + old = bucket; + } + } + + assert(old); + assert(entry.refCount === 0); + this.totalFresh--; + + // Find room in used bucket. + const bucket = this.usedBucket(entry); + + if (bucket.size < this.options.maxEntries) { + entry.used = true; + bucket.push(entry); + this.totalUsed++; + return; + } + + // No room. Evict. + const evicted = this.evictUsed(bucket); + let fresh = this.freshBucket(evicted); + + // Move to entry's old bucket if no room. + if (fresh.size >= this.options.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 + */ + + evictUsed(bucket) { + let old = bucket.head; + + for (let entry = bucket.head; entry; entry = entry.next) { + if (entry.addr.time < old.addr.time) + old = entry; + } + + return old; + } + + /** + * Convert address list to array. + * @returns {NetAddress[]} + */ + + toArray() { + const out = []; + + for (const entry of this.map.values()) + out.push(entry.addr); + + assert.strictEqual(out.length, this.size()); + + return out; + } + + /** + * Add a preferred seed. + * @param {String} host + */ + + addSeed(host) { + const ip = IP.fromHostname(host, this.network.port); + + if (ip.type === IP.types.DNS) { + // Defer for resolution. + this.dnsSeeds.push(ip); + return null; + } + + const addr = NetAddress.fromHost(ip.host, ip.port, this.network); + + this.add(addr); + + return addr; + } + + /** + * Add a priority node. + * @param {String} host + * @returns {NetAddress} + */ + + addNode(host) { + const ip = IP.fromHostname(host, this.network.port); + + if (ip.type === IP.types.DNS) { + // Defer for resolution. + this.dnsNodes.push(ip); + return null; + } + + const addr = NetAddress.fromHost(ip.host, ip.port, this.network); + + this.nodes.push(addr); + this.add(addr); + + return addr; + } + + /** + * Remove a priority node. + * @param {String} host + * @returns {Boolean} + */ + + removeNode(host) { + const addr = IP.fromHostname(host, this.network.port); + + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + + if (node.host !== addr.host) + continue; + + if (node.port !== addr.port) + continue; + + this.nodes.splice(i, 1); + + return true; + } + + return false; + } + + /** + * Set initial seeds. + * @param {String[]} seeds + */ + + setSeeds(seeds) { + this.dnsSeeds.length = 0; + + for (const host of seeds) + this.addSeed(host); + } + + /** + * Set priority nodes. + * @param {String[]} nodes + */ + + setNodes(nodes) { + this.dnsNodes.length = 0; + this.nodes.length = 0; + + for (const host of nodes) + this.addNode(host); + } + + /** + * Add a local address. + * @param {String} host + * @param {Number} port + * @param {Number} score + * @returns {Boolean} + */ + + addLocal(host, port, score) { + const addr = NetAddress.fromHost(host, port, this.network); + addr.services = this.options.services; + return this.pushLocal(addr, score); + } + + /** + * Add a local address. + * @param {NetAddress} addr + * @param {Number} score + * @returns {Boolean} + */ + + pushLocal(addr, score) { + if (!addr.isRoutable()) + return false; + + if (this.local.has(addr.hostname)) + return false; + + const local = new LocalAddress(addr, score); + + this.local.set(addr.hostname, local); + + return true; + } + + /** + * Get local address based on reachability. + * @param {NetAddress?} src + * @returns {NetAddress} + */ + + getLocal(src) { + let bestReach = -1; + let bestScore = -1; + let bestDest = null; + + if (!src) + src = this.address; + + if (this.local.size === 0) + return null; + + for (const dest of this.local.values()) { + const reach = src.getReachability(dest.addr); + + if (reach < bestReach) + continue; + + if (reach > bestReach || dest.score > bestScore) { + bestReach = reach; + bestScore = dest.score; + bestDest = dest.addr; + } + } + + bestDest.time = this.network.now(); + + return bestDest; + } + + /** + * Mark local address as seen during a handshake. + * @param {NetAddress} addr + * @returns {Boolean} + */ + + markLocal(addr) { + const local = this.local.get(addr.hostname); + + if (!local) + return false; + + local.score++; + + return true; + } + + /** + * Discover hosts from seeds. + * @method + * @returns {Promise} + */ + + async discoverSeeds() { + const jobs = []; + + for (const seed of this.dnsSeeds) + jobs.push(this.populateSeed(seed)); + + await Promise.all(jobs); + } + + /** + * Discover hosts from nodes. + * @method + * @returns {Promise} + */ + + async discoverNodes() { + const jobs = []; + + for (const node of this.dnsNodes) + jobs.push(this.populateNode(node)); + + await Promise.all(jobs); + } + + /** + * Lookup node's domain. + * @method + * @param {Object} addr + * @returns {Promise} + */ + + async populateNode(addr) { + const addrs = await this.populate(addr); + + if (addrs.length === 0) + return; + + this.nodes.push(addrs[0]); + this.add(addrs[0]); + } + + /** + * Populate from seed. + * @method + * @param {Object} seed + * @returns {Promise} + */ + + async populateSeed(seed) { + const addrs = await this.populate(seed); + + for (const addr of addrs) + this.add(addr); + } + + /** + * Lookup hosts from dns host. + * @method + * @param {Object} target + * @returns {Promise} + */ + + async populate(target) { + const addrs = []; + + assert(target.type === IP.types.DNS, 'Resolved host passed.'); + + this.logger.info('Resolving host: %s.', target.host); + + let hosts; + try { + hosts = await this.resolve(target.host); + } catch (e) { + this.logger.error(e); + return addrs; + } + + for (const host of hosts) { + const addr = NetAddress.fromHost(host, target.port, this.network); + addrs.push(addr); + } + + return addrs; + } + + /** + * Convert host list to json-friendly object. + * @returns {Object} + */ + + toJSON() { + const addrs = []; + const fresh = []; + const used = []; + + for (const entry of this.map.values()) + addrs.push(entry.toJSON()); + + for (const bucket of this.fresh) { + const keys = []; + for (const key of bucket.keys()) + keys.push(key); + fresh.push(keys); + } + + for (const bucket of this.used) { + const keys = []; + for (let entry = bucket.head; entry; entry = entry.next) + keys.push(entry.key()); + used.push(keys); + } + + return { + version: HostList.VERSION, + addrs: addrs, + fresh: fresh, + used: used + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + * @returns {HostList} + */ + + fromJSON(json) { + const sources = new Map(); + const map = new Map(); + let totalFresh = 0; + let totalUsed = 0; + const fresh = []; + const used = []; + + assert(json && typeof json === 'object'); + + assert(json.version === HostList.VERSION, + 'Bad address serialization version.'); + + assert(Array.isArray(json.addrs)); + + for (const addr of json.addrs) { + const entry = HostEntry.fromJSON(addr, this.network); + let src = sources.get(entry.src.hostname); + + // Save some memory. + if (!src) { + src = entry.src; + sources.set(src.hostname, src); + } + + entry.src = src; + + map.set(entry.key(), entry); + } + + assert(Array.isArray(json.fresh)); + assert(json.fresh.length <= this.options.maxBuckets, + 'Buckets mismatch.'); + + for (const keys of json.fresh) { + const bucket = new Map(); + + for (const key of keys) { + const entry = map.get(key); + assert(entry); + if (entry.refCount === 0) + totalFresh++; + entry.refCount++; + bucket.set(key, entry); + } + + assert(bucket.size <= this.options.maxEntries, + 'Bucket size mismatch.'); + + fresh.push(bucket); + } + + assert(fresh.length === this.fresh.length, + 'Buckets mismatch.'); + + assert(Array.isArray(json.used)); + assert(json.used.length <= this.options.maxBuckets, + 'Buckets mismatch.'); + + for (const keys of json.used) { + const bucket = new List(); + + for (const key of keys) { + const entry = map.get(key); + assert(entry); + assert(entry.refCount === 0); + assert(!entry.used); + entry.used = true; + totalUsed++; + bucket.push(entry); + } + + assert(bucket.size <= this.options.maxEntries, + 'Bucket size mismatch.'); + + used.push(bucket); + } + + assert(used.length === this.used.length, + 'Buckets mismatch.'); + + for (const entry of map.values()) + assert(entry.used || entry.refCount > 0); + + this.map = map; + this.fresh = fresh; + this.totalFresh = totalFresh; + this.used = used; + this.totalUsed = totalUsed; + + return this; + } + + /** + * Instantiate host list from json object. + * @param {Object} options + * @param {Object} json + * @returns {HostList} + */ + + static fromJSON(options, json) { + return new this(options).fromJSON(json); + } } /** @@ -129,1469 +1252,361 @@ HostList.scores = { }; /** - * Initialize list. - * @private - */ - -HostList.prototype.init = function init() { - const options = this.options; - const scores = HostList.scores; - const hosts = IP.getPublic(); - const port = this.address.port; - - for (let i = 0; i < this.options.maxBuckets; i++) - this.fresh.push(new Map()); - - for (let i = 0; i < this.options.maxBuckets; i++) - this.used.push(new List()); - - this.setSeeds(options.seeds); - this.setNodes(options.nodes); - - this.pushLocal(this.address, scores.MANUAL); - this.addLocal(options.host, options.port, scores.BIND); - - for (const host of hosts) - this.addLocal(host, port, scores.IF); -}; - -/** - * Open hostlist and read hosts file. - * @method - * @returns {Promise} - */ - -HostList.prototype.open = async function open() { - try { - await this.loadFile(); - } catch (e) { - this.logger.warning('Hosts deserialization failed.'); - this.logger.error(e); - } - - if (this.size() === 0) - this.injectSeeds(); - - await this.discoverNodes(); - - this.start(); -}; - -/** - * Close hostlist. - * @method - * @returns {Promise} - */ - -HostList.prototype.close = async function close() { - this.stop(); - await this.flush(); - this.reset(); -}; - -/** - * Start flush interval. - */ - -HostList.prototype.start = function start() { - if (!this.options.persistent) - return; - - if (!this.options.filename) - return; - - assert(this.timer == null); - this.timer = setInterval(() => this.flush(), this.options.flushInterval); -}; - -/** - * Stop flush interval. - */ - -HostList.prototype.stop = function stop() { - if (!this.options.persistent) - return; - - if (!this.options.filename) - return; - - assert(this.timer != null); - clearInterval(this.timer); - this.timer = null; -}; - -/** - * Read and initialize from hosts file. - * @method - * @returns {Promise} - */ - -HostList.prototype.injectSeeds = function injectSeeds() { - const nodes = seeds.get(this.network.type); - - for (const node of nodes) { - const addr = NetAddress.fromHostname(node, this.network); - - if (!addr.isRoutable()) - continue; - - if (!this.options.onion && addr.isOnion()) - continue; - - if (addr.port === 0) - continue; - - this.add(addr); - } -}; - -/** - * Read and initialize from hosts file. - * @method - * @returns {Promise} - */ - -HostList.prototype.loadFile = async function loadFile() { - const filename = this.options.filename; - - if (fs.unsupported) - return; - - if (!this.options.persistent) - return; - - if (!filename) - return; - - let data; - try { - data = await fs.readFile(filename, 'utf8'); - } catch (e) { - if (e.code === 'ENOENT') - return; - throw e; - } - - const json = JSON.parse(data); - - this.fromJSON(json); -}; - -/** - * Flush addrs to hosts file. - * @method - * @returns {Promise} - */ - -HostList.prototype.flush = async function flush() { - const filename = this.options.filename; - - if (fs.unsupported) - return; - - if (!this.options.persistent) - return; - - if (!filename) - return; - - if (!this.needsFlush) - return; - - if (this.flushing) - return; - - this.needsFlush = false; - - this.logger.debug('Writing hosts to %s.', filename); - - const json = this.toJSON(); - const data = JSON.stringify(json); - - this.flushing = true; - - try { - await fs.writeFile(filename, data, 'utf8'); - } catch (e) { - this.logger.warning('Writing hosts failed.'); - this.logger.error(e); - } - - this.flushing = false; -}; - -/** - * Get list size. - * @returns {Number} - */ - -HostList.prototype.size = function size() { - return this.totalFresh + this.totalUsed; -}; - -/** - * Test whether the host list is full. - * @returns {Boolean} - */ - -HostList.prototype.isFull = function isFull() { - const max = this.options.maxBuckets * this.options.maxEntries; - return this.size() >= max; -}; - -/** - * Reset host list. - */ - -HostList.prototype.reset = function reset() { - this.map.clear(); - - for (const bucket of this.fresh) - bucket.clear(); - - for (const bucket of this.used) - bucket.reset(); - - this.totalFresh = 0; - this.totalUsed = 0; - - this.nodes.length = 0; -}; - -/** - * Mark a peer as banned. - * @param {String} host - */ - -HostList.prototype.ban = function ban(host) { - this.banned.set(host, util.now()); -}; - -/** - * Unban host. - * @param {String} host - */ - -HostList.prototype.unban = function unban(host) { - this.banned.delete(host); -}; - -/** - * Clear banned hosts. - */ - -HostList.prototype.clearBanned = function clearBanned() { - this.banned.clear(); -}; - -/** - * Test whether the host is banned. - * @param {String} host - * @returns {Boolean} - */ - -HostList.prototype.isBanned = function isBanned(host) { - const time = this.banned.get(host); - - if (time == null) - return false; - - if (util.now() > time + this.options.banTime) { - this.banned.delete(host); - return false; - } - - return true; -}; - -/** - * Allocate a new host. - * @returns {HostEntry} - */ - -HostList.prototype.getHost = function getHost() { - let buckets = null; - - if (this.totalFresh > 0) - buckets = this.fresh; - - if (this.totalUsed > 0) { - if (this.totalFresh === 0 || random(2) === 0) - buckets = this.used; - } - - if (!buckets) - return null; - - const now = this.network.now(); - let factor = 1; - - for (;;) { - let index = random(buckets.length); - const bucket = buckets[index]; - - if (bucket.size === 0) - continue; - - index = random(bucket.size); - - let entry; - if (buckets === this.used) { - entry = bucket.head; - while (index--) - entry = entry.next; - } else { - for (entry of bucket.values()) { - if (index === 0) - break; - index--; - } - } - - const num = random(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 {Map} - */ - -HostList.prototype.freshBucket = function freshBucket(entry) { - const addr = entry.addr; - const src = entry.src; - const data = concat32(addr.raw, src.raw); - const hash = murmur3.sum(data, 0xfba4c795); - const 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) { - const addr = entry.addr; - const hash = murmur3.sum(addr.raw, 0xfba4c795); - const 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) { - assert(addr.port !== 0); - - let entry = this.map.get(addr.hostname); - - if (entry) { - const now = this.network.now(); - let penalty = 2 * 60 * 60; - let interval = 24 * 60 * 60; - - // No source means we're inserting - // this ourselves. No penalty. - if (!src) - penalty = 0; - - // Update services. - entry.addr.services |= addr.services; - entry.addr.services >>>= 0; - - // Online? - if (now - addr.time < 24 * 60 * 60) - interval = 60 * 60; - - // Periodically update time. - if (entry.addr.time < addr.time - interval - penalty) { - entry.addr.time = addr.time; - this.needsFlush = true; - } - - // Do not update if no new - // information is present. - if (entry.addr.time && addr.time <= entry.addr.time) - 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 === HostList.MAX_REFS) - return false; - - assert(entry.refCount < HostList.MAX_REFS); - - // Stochastic test: previous refCount - // N: 2^N times harder to increase it. - let factor = 1; - for (let i = 0; i < entry.refCount; i++) - factor *= 2; - - if (random(factor) !== 0) - return false; - } else { - if (this.isFull()) - return false; - - if (!src) - src = this.address; - - entry = new HostEntry(addr, src); - - this.totalFresh++; - } - - const bucket = this.freshBucket(entry); - - if (bucket.has(entry.key())) - return false; - - if (bucket.size >= this.options.maxEntries) - this.evictFresh(bucket); - - bucket.set(entry.key(), entry); - entry.refCount++; - - this.map.set(entry.key(), entry); - this.needsFlush = true; - - return true; -}; - -/** - * Evict a host from fresh bucket. - * @param {Map} bucket - */ - -HostList.prototype.evictFresh = function evictFresh(bucket) { - let old = null; - - for (const entry of bucket.values()) { - if (this.isStale(entry)) { - bucket.delete(entry.key()); - - if (--entry.refCount === 0) { - this.map.delete(entry.key()); - this.totalFresh--; - } - - continue; - } - - if (!old) { - old = entry; - continue; - } - - if (entry.addr.time < old.addr.time) - old = entry; - } - - if (!old) - return; - - bucket.delete(old.key()); - - if (--old.refCount === 0) { - this.map.delete(old.key()); - this.totalFresh--; - } -}; - -/** - * Test whether a host is evictable. - * @param {HostEntry} entry - * @returns {Boolean} - */ - -HostList.prototype.isStale = function isStale(entry) { - const now = this.network.now(); - - if (entry.lastAttempt && entry.lastAttempt >= now - 60) - return false; - - if (entry.addr.time > now + 10 * 60) - return true; - - if (entry.addr.time === 0) - return true; - - if (now - entry.addr.time > HostList.HORIZON_DAYS * 24 * 60 * 60) - return true; - - if (entry.lastSuccess === 0 && entry.attempts >= HostList.RETRIES) - return true; - - if (now - entry.lastSuccess > HostList.MIN_FAIL_DAYS * 24 * 60 * 60) { - if (entry.attempts >= HostList.MAX_FAILURES) - return true; - } - - return false; -}; - -/** - * Remove host from host list. - * @param {String} hostname - * @returns {NetAddress} - */ - -HostList.prototype.remove = function remove(hostname) { - const entry = this.map.get(hostname); - - if (!entry) - return null; - - if (entry.used) { - let head = entry; - - assert(entry.refCount === 0); - - while (head.prev) - head = head.prev; - - for (const bucket of this.used) { - if (bucket.head === head) { - bucket.remove(entry); - this.totalUsed--; - head = null; - break; - } - } - - assert(!head); - } else { - for (const bucket of this.fresh) { - if (bucket.delete(entry.key())) - entry.refCount--; - } - - this.totalFresh--; - assert(entry.refCount === 0); - } - - this.map.delete(entry.key()); - - return entry.addr; -}; - -/** - * Mark host as failed. - * @param {String} hostname - */ - -HostList.prototype.markAttempt = function markAttempt(hostname) { - const entry = this.map.get(hostname); - const now = this.network.now(); - - if (!entry) - return; - - entry.attempts++; - entry.lastAttempt = now; -}; - -/** - * Mark host as successfully connected. - * @param {String} hostname - */ - -HostList.prototype.markSuccess = function markSuccess(hostname) { - const entry = this.map.get(hostname); - const now = this.network.now(); - - if (!entry) - return; - - if (now - entry.addr.time > 20 * 60) - entry.addr.time = now; -}; - -/** - * Mark host as successfully ack'd. - * @param {String} hostname - * @param {Number} services - */ - -HostList.prototype.markAck = function markAck(hostname, services) { - const entry = this.map.get(hostname); - - if (!entry) - return; - - const now = this.network.now(); - - entry.addr.services |= services; - entry.addr.services >>>= 0; - - entry.lastSuccess = now; - entry.lastAttempt = now; - entry.attempts = 0; - - if (entry.used) - return; - - assert(entry.refCount > 0); - - // Remove from fresh. - let old; - for (const bucket of this.fresh) { - if (bucket.delete(entry.key())) { - entry.refCount--; - old = bucket; - } - } - - assert(old); - assert(entry.refCount === 0); - this.totalFresh--; - - // Find room in used bucket. - const bucket = this.usedBucket(entry); - - if (bucket.size < this.options.maxEntries) { - entry.used = true; - bucket.push(entry); - this.totalUsed++; - return; - } - - // No room. Evict. - const evicted = this.evictUsed(bucket); - let fresh = this.freshBucket(evicted); - - // Move to entry's old bucket if no room. - if (fresh.size >= this.options.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) { - let old = bucket.head; - - for (let entry = bucket.head; entry; entry = entry.next) { - if (entry.addr.time < old.addr.time) - old = entry; - } - - return old; -}; - -/** - * Convert address list to array. - * @returns {NetAddress[]} - */ - -HostList.prototype.toArray = function toArray() { - const out = []; - - for (const entry of this.map.values()) - out.push(entry.addr); - - assert.strictEqual(out.length, this.size()); - - return out; -}; - -/** - * Add a preferred seed. - * @param {String} host - */ - -HostList.prototype.addSeed = function addSeed(host) { - const ip = IP.fromHostname(host, this.network.port); - - if (ip.type === IP.types.DNS) { - // Defer for resolution. - this.dnsSeeds.push(ip); - return null; - } - - const addr = NetAddress.fromHost(ip.host, ip.port, this.network); - - this.add(addr); - - return addr; -}; - -/** - * Add a priority node. - * @param {String} host - * @returns {NetAddress} - */ - -HostList.prototype.addNode = function addNode(host) { - const ip = IP.fromHostname(host, this.network.port); - - if (ip.type === IP.types.DNS) { - // Defer for resolution. - this.dnsNodes.push(ip); - return null; - } - - const addr = NetAddress.fromHost(ip.host, ip.port, this.network); - - this.nodes.push(addr); - this.add(addr); - - return addr; -}; - -/** - * Remove a priority node. - * @param {String} host - * @returns {Boolean} - */ - -HostList.prototype.removeNode = function removeNode(host) { - const addr = IP.fromHostname(host, this.network.port); - - for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - - if (node.host !== addr.host) - continue; - - if (node.port !== addr.port) - continue; - - this.nodes.splice(i, 1); - - return true; - } - - return false; -}; - -/** - * Set initial seeds. - * @param {String[]} seeds - */ - -HostList.prototype.setSeeds = function setSeeds(seeds) { - this.dnsSeeds.length = 0; - - for (const host of seeds) - this.addSeed(host); -}; - -/** - * Set priority nodes. - * @param {String[]} nodes - */ - -HostList.prototype.setNodes = function setNodes(nodes) { - this.dnsNodes.length = 0; - this.nodes.length = 0; - - for (const host of nodes) - this.addNode(host); -}; - -/** - * Add a local address. - * @param {String} host - * @param {Number} port - * @param {Number} score - * @returns {Boolean} - */ - -HostList.prototype.addLocal = function addLocal(host, port, score) { - const addr = NetAddress.fromHost(host, port, this.network); - addr.services = this.options.services; - return this.pushLocal(addr, score); -}; - -/** - * Add a local address. - * @param {NetAddress} addr - * @param {Number} score - * @returns {Boolean} - */ - -HostList.prototype.pushLocal = function pushLocal(addr, score) { - if (!addr.isRoutable()) - return false; - - if (this.local.has(addr.hostname)) - return false; - - const local = new LocalAddress(addr, score); - - this.local.set(addr.hostname, local); - - return true; -}; - -/** - * Get local address based on reachability. - * @param {NetAddress?} src - * @returns {NetAddress} - */ - -HostList.prototype.getLocal = function getLocal(src) { - let bestReach = -1; - let bestScore = -1; - let bestDest = null; - - if (!src) - src = this.address; - - if (this.local.size === 0) - return null; - - for (const dest of this.local.values()) { - const reach = src.getReachability(dest.addr); - - if (reach < bestReach) - continue; - - if (reach > bestReach || dest.score > bestScore) { - bestReach = reach; - bestScore = dest.score; - bestDest = dest.addr; - } - } - - bestDest.time = this.network.now(); - - return bestDest; -}; - -/** - * Mark local address as seen during a handshake. - * @param {NetAddress} addr - * @returns {Boolean} - */ - -HostList.prototype.markLocal = function markLocal(addr) { - const local = this.local.get(addr.hostname); - - if (!local) - return false; - - local.score++; - - return true; -}; - -/** - * Discover hosts from seeds. - * @method - * @returns {Promise} - */ - -HostList.prototype.discoverSeeds = async function discoverSeeds() { - const jobs = []; - - for (const seed of this.dnsSeeds) - jobs.push(this.populateSeed(seed)); - - await Promise.all(jobs); -}; - -/** - * Discover hosts from nodes. - * @method - * @returns {Promise} - */ - -HostList.prototype.discoverNodes = async function discoverNodes() { - const jobs = []; - - for (const node of this.dnsNodes) - jobs.push(this.populateNode(node)); - - await Promise.all(jobs); -}; - -/** - * Lookup node's domain. - * @method - * @param {Object} addr - * @returns {Promise} - */ - -HostList.prototype.populateNode = async function populateNode(addr) { - const addrs = await this.populate(addr); - - if (addrs.length === 0) - return; - - this.nodes.push(addrs[0]); - this.add(addrs[0]); -}; - -/** - * Populate from seed. - * @method - * @param {Object} seed - * @returns {Promise} - */ - -HostList.prototype.populateSeed = async function populateSeed(seed) { - const addrs = await this.populate(seed); - - for (const addr of addrs) - this.add(addr); -}; - -/** - * Lookup hosts from dns host. - * @method - * @param {Object} target - * @returns {Promise} - */ - -HostList.prototype.populate = async function populate(target) { - const addrs = []; - - assert(target.type === IP.types.DNS, 'Resolved host passed.'); - - this.logger.info('Resolving host: %s.', target.host); - - let hosts; - try { - hosts = await this.resolve(target.host); - } catch (e) { - this.logger.error(e); - return addrs; - } - - for (const host of hosts) { - const addr = NetAddress.fromHost(host, target.port, this.network); - addrs.push(addr); - } - - return addrs; -}; - -/** - * Convert host list to json-friendly object. - * @returns {Object} - */ - -HostList.prototype.toJSON = function toJSON() { - const addrs = []; - const fresh = []; - const used = []; - - for (const entry of this.map.values()) - addrs.push(entry.toJSON()); - - for (const bucket of this.fresh) { - const keys = []; - for (const key of bucket.keys()) - keys.push(key); - fresh.push(keys); - } - - for (const bucket of this.used) { - const keys = []; - for (let entry = bucket.head; entry; entry = entry.next) - keys.push(entry.key()); - used.push(keys); - } - - return { - version: HostList.VERSION, - addrs: addrs, - fresh: fresh, - used: used - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - * @returns {HostList} - */ - -HostList.prototype.fromJSON = function fromJSON(json) { - const sources = new Map(); - const map = new Map(); - let totalFresh = 0; - let totalUsed = 0; - const fresh = []; - const used = []; - - assert(json && typeof json === 'object'); - - assert(json.version === HostList.VERSION, - 'Bad address serialization version.'); - - assert(Array.isArray(json.addrs)); - - for (const addr of json.addrs) { - const entry = HostEntry.fromJSON(addr, this.network); - let src = sources.get(entry.src.hostname); - - // Save some memory. - if (!src) { - src = entry.src; - sources.set(src.hostname, src); - } - - entry.src = src; - - map.set(entry.key(), entry); - } - - assert(Array.isArray(json.fresh)); - assert(json.fresh.length <= this.options.maxBuckets, - 'Buckets mismatch.'); - - for (const keys of json.fresh) { - const bucket = new Map(); - - for (const key of keys) { - const entry = map.get(key); - assert(entry); - if (entry.refCount === 0) - totalFresh++; - entry.refCount++; - bucket.set(key, entry); - } - - assert(bucket.size <= this.options.maxEntries, - 'Bucket size mismatch.'); - - fresh.push(bucket); - } - - assert(fresh.length === this.fresh.length, - 'Buckets mismatch.'); - - assert(Array.isArray(json.used)); - assert(json.used.length <= this.options.maxBuckets, - 'Buckets mismatch.'); - - for (const keys of json.used) { - const bucket = new List(); - - for (const key of keys) { - const entry = map.get(key); - assert(entry); - assert(entry.refCount === 0); - assert(!entry.used); - entry.used = true; - totalUsed++; - bucket.push(entry); - } - - assert(bucket.size <= this.options.maxEntries, - 'Bucket size mismatch.'); - - used.push(bucket); - } - - assert(used.length === this.used.length, - 'Buckets mismatch.'); - - for (const entry of map.values()) - assert(entry.used || entry.refCount > 0); - - this.map = map; - this.fresh = fresh; - this.totalFresh = totalFresh; - this.used = used; - this.totalUsed = totalUsed; - - return this; -}; - -/** - * Instantiate host list from json object. - * @param {Object} options - * @param {Object} json - * @returns {HostList} - */ - -HostList.fromJSON = function fromJSON(options, json) { - return new HostEntry(options).fromJSON(json); -}; - -/** - * HostEntry + * Host Entry * @alias module:net.HostEntry - * @constructor - * @param {NetAddress} addr - * @param {NetAddress} src */ -function HostEntry(addr, src) { - if (!(this instanceof HostEntry)) - return new HostEntry(addr, src); +class HostEntry { + /** + * Create a host entry. + * @constructor + * @param {NetAddress} addr + * @param {NetAddress} src + */ - this.addr = addr || new NetAddress(); - this.src = src || new NetAddress(); - this.prev = null; - this.next = null; - this.used = false; - this.refCount = 0; - this.attempts = 0; - this.lastSuccess = 0; - this.lastAttempt = 0; + constructor(addr, src) { + this.addr = addr || new NetAddress(); + this.src = src || new NetAddress(); + this.prev = null; + this.next = null; + this.used = false; + this.refCount = 0; + this.attempts = 0; + this.lastSuccess = 0; + this.lastAttempt = 0; - if (addr) - this.fromOptions(addr, src); + if (addr) + this.fromOptions(addr, src); + } + + /** + * Inject properties from options. + * @private + * @param {NetAddress} addr + * @param {NetAddress} src + * @returns {HostEntry} + */ + + fromOptions(addr, src) { + assert(addr instanceof NetAddress); + assert(src instanceof NetAddress); + this.addr = addr; + this.src = src; + return this; + } + + /** + * Instantiate host entry from options. + * @param {NetAddress} addr + * @param {NetAddress} src + * @returns {HostEntry} + */ + + static fromOptions(addr, src) { + return new this().fromOptions(addr, src); + } + + /** + * Get key suitable for a hash table (hostname). + * @returns {String} + */ + + key() { + return this.addr.hostname; + } + + /** + * Get host priority. + * @param {Number} now + * @returns {Number} + */ + + chance(now) { + let c = 1; + + if (now - this.lastAttempt < 60 * 10) + c *= 0.01; + + c *= Math.pow(0.66, Math.min(this.attempts, 8)); + + return c; + } + + /** + * Inspect host address. + * @returns {Object} + */ + + 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) + }; + } + + /** + * Convert host entry to json-friendly object. + * @returns {Object} + */ + + toJSON() { + return { + addr: this.addr.hostname, + src: this.src.hostname, + services: this.addr.services.toString(2), + time: this.addr.time, + attempts: this.attempts, + lastSuccess: this.lastSuccess, + lastAttempt: this.lastAttempt + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + * @param {Network} network + * @returns {HostEntry} + */ + + fromJSON(json, network) { + assert(json && typeof json === 'object'); + assert(typeof json.addr === 'string'); + assert(typeof json.src === 'string'); + + this.addr.fromHostname(json.addr, network); + + if (json.services != null) { + assert(typeof json.services === 'string'); + assert(json.services.length > 0); + assert(json.services.length <= 32); + const services = parseInt(json.services, 2); + assert((services >>> 0) === services); + this.addr.services = services; + } + + if (json.time != null) { + assert(Number.isSafeInteger(json.time)); + assert(json.time >= 0); + this.addr.time = json.time; + } + + if (json.src != null) { + assert(typeof json.src === 'string'); + this.src.fromHostname(json.src, network); + } + + if (json.attempts != null) { + assert((json.attempts >>> 0) === json.attempts); + this.attempts = json.attempts; + } + + if (json.lastSuccess != null) { + assert(Number.isSafeInteger(json.lastSuccess)); + assert(json.lastSuccess >= 0); + this.lastSuccess = json.lastSuccess; + } + + if (json.lastAttempt != null) { + assert(Number.isSafeInteger(json.lastAttempt)); + assert(json.lastAttempt >= 0); + this.lastAttempt = json.lastAttempt; + } + + return this; + } + + /** + * Instantiate host entry from json object. + * @param {Object} json + * @param {Network} network + * @returns {HostEntry} + */ + + static fromJSON(json, network) { + return new this().fromJSON(json, network); + } } /** - * Inject properties from options. - * @private - * @param {NetAddress} addr - * @param {NetAddress} src - * @returns {HostEntry} - */ - -HostEntry.prototype.fromOptions = function fromOptions(addr, src) { - assert(addr instanceof NetAddress); - assert(src instanceof NetAddress); - this.addr = addr; - this.src = src; - return this; -}; - -/** - * Instantiate host entry from options. - * @param {NetAddress} addr - * @param {NetAddress} src - * @returns {HostEntry} - */ - -HostEntry.fromOptions = function fromOptions(addr, src) { - return new HostEntry().fromOptions(addr, src); -}; - -/** - * 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) { - let c = 1; - - if (now - this.lastAttempt < 60 * 10) - c *= 0.01; - - c *= Math.pow(0.66, Math.min(this.attempts, 8)); - - return c; -}; - -/** - * 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) - }; -}; - -/** - * Convert host entry to json-friendly object. - * @returns {Object} - */ - -HostEntry.prototype.toJSON = function toJSON() { - return { - addr: this.addr.hostname, - src: this.src.hostname, - services: this.addr.services.toString(2), - time: this.addr.time, - attempts: this.attempts, - lastSuccess: this.lastSuccess, - lastAttempt: this.lastAttempt - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - * @param {Network} network - * @returns {HostEntry} - */ - -HostEntry.prototype.fromJSON = function fromJSON(json, network) { - assert(json && typeof json === 'object'); - assert(typeof json.addr === 'string'); - assert(typeof json.src === 'string'); - - this.addr.fromHostname(json.addr, network); - - if (json.services != null) { - assert(typeof json.services === 'string'); - assert(json.services.length > 0); - assert(json.services.length <= 32); - const services = parseInt(json.services, 2); - assert((services >>> 0) === services); - this.addr.services = services; - } - - if (json.time != null) { - assert(Number.isSafeInteger(json.time)); - assert(json.time >= 0); - this.addr.time = json.time; - } - - if (json.src != null) { - assert(typeof json.src === 'string'); - this.src.fromHostname(json.src, network); - } - - if (json.attempts != null) { - assert((json.attempts >>> 0) === json.attempts); - this.attempts = json.attempts; - } - - if (json.lastSuccess != null) { - assert(Number.isSafeInteger(json.lastSuccess)); - assert(json.lastSuccess >= 0); - this.lastSuccess = json.lastSuccess; - } - - if (json.lastAttempt != null) { - assert(Number.isSafeInteger(json.lastAttempt)); - assert(json.lastAttempt >= 0); - this.lastAttempt = json.lastAttempt; - } - - return this; -}; - -/** - * Instantiate host entry from json object. - * @param {Object} json - * @param {Network} network - * @returns {HostEntry} - */ - -HostEntry.fromJSON = function fromJSON(json, network) { - return new HostEntry().fromJSON(json, network); -}; - -/** - * LocalAddress + * Local Address * @alias module:net.LocalAddress - * @constructor - * @param {NetAddress} addr - * @param {Number?} score */ -function LocalAddress(addr, score) { - this.addr = addr; - this.score = score || 0; +class LocalAddress { + /** + * Create a local address. + * @constructor + * @param {NetAddress} addr + * @param {Number?} score + */ + + constructor(addr, score) { + this.addr = addr; + this.score = score || 0; + } } /** * Host List Options * @alias module:net.HostListOptions - * @constructor - * @param {Object?} options */ -function HostListOptions(options) { - if (!(this instanceof HostListOptions)) - return new HostListOptions(options); +class HostListOptions { + /** + * Create host list options. + * @constructor + * @param {Object?} options + */ - this.network = Network.primary; - this.logger = Logger.global; - this.resolve = dns.lookup; - this.host = '0.0.0.0'; - this.port = this.network.port; - this.services = common.LOCAL_SERVICES; - this.onion = false; - this.banTime = common.BAN_TIME; - - this.address = new NetAddress(); - this.address.services = this.services; - this.address.time = this.network.now(); - - this.seeds = this.network.seeds; - this.nodes = []; - - this.maxBuckets = 20; - this.maxEntries = 50; - - this.prefix = null; - this.filename = null; - this.persistent = false; - this.flushInterval = 120000; - - if (options) - this.fromOptions(options); -} - -/** - * Inject properties from options. - * @private - * @param {Object} options - */ - -HostListOptions.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Options are required.'); - - if (options.network != null) { - this.network = Network.get(options.network); - this.seeds = this.network.seeds; - this.address.port = this.network.port; + constructor(options) { + this.network = Network.primary; + this.logger = Logger.global; + this.resolve = dns.lookup; + this.host = '0.0.0.0'; this.port = this.network.port; + this.services = common.LOCAL_SERVICES; + this.onion = false; + this.banTime = common.BAN_TIME; + + this.address = new NetAddress(); + this.address.services = this.services; + this.address.time = this.network.now(); + + this.seeds = this.network.seeds; + this.nodes = []; + + this.maxBuckets = 20; + this.maxEntries = 50; + + this.prefix = null; + this.filename = null; + this.persistent = false; + this.flushInterval = 120000; + + if (options) + this.fromOptions(options); } - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; + /** + * Inject properties from options. + * @private + * @param {Object} options + */ + + fromOptions(options) { + assert(options, 'Options are required.'); + + if (options.network != null) { + this.network = Network.get(options.network); + this.seeds = this.network.seeds; + this.address.port = this.network.port; + this.port = this.network.port; + } + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.resolve != null) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.banTime != null) { + assert(options.banTime >= 0); + this.banTime = options.banTime; + } + + if (options.seeds) { + assert(Array.isArray(options.seeds)); + this.seeds = options.seeds; + } + + if (options.nodes) { + assert(Array.isArray(options.nodes)); + this.nodes = options.nodes; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + const raw = IP.toBuffer(options.host); + this.host = IP.toString(raw); + if (IP.isRoutable(raw)) + this.address.setHost(this.host); + } + + if (options.port != null) { + assert(typeof options.port === 'number'); + assert(options.port > 0 && options.port <= 0xffff); + this.port = options.port; + this.address.setPort(this.port); + } + + if (options.publicHost != null) { + assert(typeof options.publicHost === 'string'); + this.address.setHost(options.publicHost); + } + + if (options.publicPort != null) { + assert(typeof options.publicPort === 'number'); + assert(options.publicPort > 0 && options.publicPort <= 0xffff); + this.address.setPort(options.publicPort); + } + + if (options.services != null) { + assert(typeof options.services === 'number'); + this.services = options.services; + } + + if (options.onion != null) { + assert(typeof options.onion === 'boolean'); + this.onion = options.onion; + } + + if (options.maxBuckets != null) { + assert(typeof options.maxBuckets === 'number'); + this.maxBuckets = options.maxBuckets; + } + + if (options.maxEntries != null) { + assert(typeof options.maxEntries === 'number'); + this.maxEntries = options.maxEntries; + } + + if (options.persistent != null) { + assert(typeof options.persistent === 'boolean'); + this.persistent = options.persistent; + } + + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.filename = path.join(this.prefix, 'hosts.json'); + } + + if (options.filename != null) { + assert(typeof options.filename === 'string'); + this.filename = options.filename; + } + + if (options.flushInterval != null) { + assert(options.flushInterval >= 0); + this.flushInterval = options.flushInterval; + } + + this.address.time = this.network.now(); + this.address.services = this.services; + + return this; } - - if (options.resolve != null) { - assert(typeof options.resolve === 'function'); - this.resolve = options.resolve; - } - - if (options.banTime != null) { - assert(options.banTime >= 0); - this.banTime = options.banTime; - } - - if (options.seeds) { - assert(Array.isArray(options.seeds)); - this.seeds = options.seeds; - } - - if (options.nodes) { - assert(Array.isArray(options.nodes)); - this.nodes = options.nodes; - } - - if (options.host != null) { - assert(typeof options.host === 'string'); - const raw = IP.toBuffer(options.host); - this.host = IP.toString(raw); - if (IP.isRoutable(raw)) - this.address.setHost(this.host); - } - - if (options.port != null) { - assert(typeof options.port === 'number'); - assert(options.port > 0 && options.port <= 0xffff); - this.port = options.port; - this.address.setPort(this.port); - } - - if (options.publicHost != null) { - assert(typeof options.publicHost === 'string'); - this.address.setHost(options.publicHost); - } - - if (options.publicPort != null) { - assert(typeof options.publicPort === 'number'); - assert(options.publicPort > 0 && options.publicPort <= 0xffff); - this.address.setPort(options.publicPort); - } - - if (options.services != null) { - assert(typeof options.services === 'number'); - this.services = options.services; - } - - if (options.onion != null) { - assert(typeof options.onion === 'boolean'); - this.onion = options.onion; - } - - if (options.maxBuckets != null) { - assert(typeof options.maxBuckets === 'number'); - this.maxBuckets = options.maxBuckets; - } - - if (options.maxEntries != null) { - assert(typeof options.maxEntries === 'number'); - this.maxEntries = options.maxEntries; - } - - if (options.persistent != null) { - assert(typeof options.persistent === 'boolean'); - this.persistent = options.persistent; - } - - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - this.filename = path.join(this.prefix, 'hosts.json'); - } - - if (options.filename != null) { - assert(typeof options.filename === 'string'); - this.filename = options.filename; - } - - if (options.flushInterval != null) { - assert(options.flushInterval >= 0); - this.flushInterval = options.flushInterval; - } - - this.address.time = this.network.now(); - this.address.services = this.services; - - return this; -}; +} /* * Helpers diff --git a/lib/net/parser.js b/lib/net/parser.js index 705c486c..e6559528 100644 --- a/lib/net/parser.js +++ b/lib/net/parser.js @@ -18,170 +18,178 @@ const common = require('./common'); const packets = require('./packets'); /** - * Protocol packet parser + * Protocol Message Parser * @alias module:net.Parser - * @constructor - * @param {Network} network + * @extends EventEmitter * @emits Parser#error * @emits Parser#packet */ -function Parser(network) { - if (!(this instanceof Parser)) - return new Parser(network); +class Parser extends EventEmitter { + /** + * Create a parser. + * @constructor + * @param {Network} network + */ - EventEmitter.call(this); + constructor(network) { + super(); - this.network = Network.get(network); + this.network = Network.get(network); - this.pending = []; - this.total = 0; - this.waiting = 24; - this.header = null; -} + this.pending = []; + this.total = 0; + this.waiting = 24; + this.header = null; + } -Object.setPrototypeOf(Parser.prototype, EventEmitter.prototype); + /** + * Emit an error. + * @private + * @param {...String} msg + */ -/** - * Emit an error. - * @private - * @param {...String} msg - */ + error() { + const msg = format.apply(null, arguments); + this.emit('error', new Error(msg)); + } -Parser.prototype.error = function error() { - const msg = format.apply(null, arguments); - this.emit('error', new Error(msg)); -}; + /** + * Feed data to the parser. + * @param {Buffer} data + */ -/** - * Feed data to the parser. - * @param {Buffer} data - */ + feed(data) { + this.total += data.length; + this.pending.push(data); -Parser.prototype.feed = function feed(data) { - this.total += data.length; - this.pending.push(data); + while (this.total >= this.waiting) { + const chunk = Buffer.allocUnsafe(this.waiting); + let off = 0; - while (this.total >= this.waiting) { - const chunk = Buffer.allocUnsafe(this.waiting); - let off = 0; + while (off < chunk.length) { + const len = this.pending[0].copy(chunk, off); + if (len === this.pending[0].length) + this.pending.shift(); + else + this.pending[0] = this.pending[0].slice(len); + off += len; + } - while (off < chunk.length) { - const len = this.pending[0].copy(chunk, off); - if (len === this.pending[0].length) - this.pending.shift(); - else - this.pending[0] = this.pending[0].slice(len); - off += len; + assert.strictEqual(off, chunk.length); + + this.total -= chunk.length; + this.parse(chunk); + } + } + + /** + * Parse a fully-buffered chunk. + * @param {Buffer} chunk + */ + + parse(data) { + assert(data.length <= common.MAX_MESSAGE); + + if (!this.header) { + this.header = this.parseHeader(data); + return; } - assert.strictEqual(off, chunk.length); + const hash = hash256.digest(data); + const checksum = hash.readUInt32LE(0, true); - this.total -= chunk.length; - this.parse(chunk); - } -}; + if (checksum !== this.header.checksum) { + this.waiting = 24; + this.header = null; + this.error('Invalid checksum: %s.', checksum.toString(16)); + return; + } -/** - * Parse a fully-buffered chunk. - * @param {Buffer} chunk - */ + let payload; + try { + payload = this.parsePayload(this.header.cmd, data); + } catch (e) { + this.waiting = 24; + this.header = null; + this.emit('error', e); + return; + } -Parser.prototype.parse = function parse(data) { - assert(data.length <= common.MAX_MESSAGE); - - if (!this.header) { - this.header = this.parseHeader(data); - return; - } - - const hash = hash256.digest(data); - const checksum = hash.readUInt32LE(0, true); - - if (checksum !== this.header.checksum) { this.waiting = 24; this.header = null; - this.error('Invalid checksum: %s.', checksum.toString(16)); - return; + + this.emit('packet', payload); } - let payload; - try { - payload = this.parsePayload(this.header.cmd, data); - } catch (e) { - this.waiting = 24; - this.header = null; - this.emit('error', e); - return; + /** + * Parse buffered packet header. + * @param {Buffer} data - Header. + * @returns {Header} + */ + + parseHeader(data) { + const magic = data.readUInt32LE(0, true); + + if (magic !== this.network.magic) { + this.error('Invalid magic value: %s.', magic.toString(16)); + return null; + } + + // Count length of the cmd. + let i = 0; + for (; data[i + 4] !== 0 && i < 12; i++); + + if (i === 12) { + this.error('Non NULL-terminated command.'); + return null; + } + + const cmd = data.toString('ascii', 4, 4 + i); + + const size = data.readUInt32LE(16, true); + + if (size > common.MAX_MESSAGE) { + this.waiting = 24; + this.error('Packet length too large: %d.', size); + return null; + } + + this.waiting = size; + + const checksum = data.readUInt32LE(20, true); + + return new Header(cmd, size, checksum); } - this.waiting = 24; - this.header = null; + /** + * Parse a payload. + * @param {String} cmd - Packet type. + * @param {Buffer} data - Payload. + * @returns {Object} + */ - this.emit('packet', payload); -}; - -/** - * Parse buffered packet header. - * @param {Buffer} data - Header. - * @returns {Header} - */ - -Parser.prototype.parseHeader = function parseHeader(data) { - const magic = data.readUInt32LE(0, true); - - if (magic !== this.network.magic) { - this.error('Invalid magic value: %s.', magic.toString(16)); - return null; + parsePayload(cmd, data) { + return packets.fromRaw(cmd, data); } - - // Count length of the cmd. - let i = 0; - for (; data[i + 4] !== 0 && i < 12; i++); - - if (i === 12) { - this.error('Non NULL-terminated command.'); - return null; - } - - const cmd = data.toString('ascii', 4, 4 + i); - - const size = data.readUInt32LE(16, true); - - if (size > common.MAX_MESSAGE) { - this.waiting = 24; - this.error('Packet length too large: %d.', size); - return null; - } - - this.waiting = size; - - const checksum = data.readUInt32LE(20, true); - - return new Header(cmd, size, checksum); -}; - -/** - * Parse a payload. - * @param {String} cmd - Packet type. - * @param {Buffer} data - Payload. - * @returns {Object} - */ - -Parser.prototype.parsePayload = function parsePayload(cmd, data) { - return packets.fromRaw(cmd, data); -}; +} /** * Packet Header - * @constructor * @ignore */ -function Header(cmd, size, checksum) { - this.cmd = cmd; - this.size = size; - this.checksum = checksum; +class Header { + /** + * Create a header. + * @constructor + */ + + constructor(cmd, size, checksum) { + this.cmd = cmd; + this.size = size; + this.checksum = checksum; + } } /* diff --git a/lib/net/peer.js b/lib/net/peer.js index b0c019bf..9495c0e5 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -33,10 +33,9 @@ const invTypes = InvItem.types; const packetTypes = packets.types; /** - * Represents a remote peer. + * Represents a network peer. * @alias module:net.Peer - * @constructor - * @param {PeerOptions} options + * @extends EventEmitter * @property {net.Socket} socket * @property {NetAddress} address * @property {Parser} parser @@ -60,93 +59,2097 @@ const packetTypes = packets.types; * sent (unix time). * @property {Number} minPing - Lowest ping time seen. * @property {Number} banScore - * @emits Peer#ack */ -function Peer(options) { - if (!(this instanceof Peer)) - return new Peer(options); +class Peer extends EventEmitter { + /** + * Create a peer. + * @alias module:net.Peer + * @constructor + * @param {PeerOptions} options + */ - EventEmitter.call(this); + constructor(options) { + super(); - this.options = options; - this.network = this.options.network; - this.logger = this.options.logger.context('peer'); - this.locker = new Lock(); + this.options = options; + this.network = this.options.network; + this.logger = this.options.logger.context('peer'); + this.locker = new Lock(); - this.parser = new Parser(this.network); - this.framer = new Framer(this.network); + this.parser = new Parser(this.network); + this.framer = new Framer(this.network); - this.id = -1; - this.socket = null; - this.opened = false; - this.outbound = false; - this.loader = false; - this.address = new NetAddress(); - this.local = new NetAddress(); - this.connected = false; - this.destroyed = false; - this.ack = false; - this.handshake = false; - this.time = 0; - this.lastSend = 0; - this.lastRecv = 0; - this.drainSize = 0; - this.drainQueue = []; - this.banScore = 0; - this.invQueue = []; - this.onPacket = null; + this.id = -1; + this.socket = null; + this.opened = false; + this.outbound = false; + this.loader = false; + this.address = new NetAddress(); + this.local = new NetAddress(); + this.connected = false; + this.destroyed = false; + this.ack = false; + this.handshake = false; + this.time = 0; + this.lastSend = 0; + this.lastRecv = 0; + this.drainSize = 0; + this.drainQueue = []; + this.banScore = 0; + this.invQueue = []; + this.onPacket = null; - this.next = null; - this.prev = null; + this.next = null; + this.prev = null; - this.version = -1; - this.services = 0; - this.height = -1; - this.agent = null; - this.noRelay = false; - this.preferHeaders = false; - this.hashContinue = null; - this.spvFilter = null; - this.feeRate = -1; - this.bip151 = null; - this.bip150 = null; - this.compactMode = -1; - this.compactWitness = false; - this.merkleBlock = null; - this.merkleTime = -1; - this.merkleMatches = 0; - this.merkleMap = null; - this.syncing = false; - this.sentAddr = false; - this.sentGetAddr = false; - this.challenge = null; - this.lastPong = -1; - this.lastPing = -1; - this.minPing = -1; - this.blockTime = -1; + this.version = -1; + this.services = 0; + this.height = -1; + this.agent = null; + this.noRelay = false; + this.preferHeaders = false; + this.hashContinue = null; + this.spvFilter = null; + this.feeRate = -1; + this.bip151 = null; + this.bip150 = null; + this.compactMode = -1; + this.compactWitness = false; + this.merkleBlock = null; + this.merkleTime = -1; + this.merkleMatches = 0; + this.merkleMap = null; + this.syncing = false; + this.sentAddr = false; + this.sentGetAddr = false; + this.challenge = null; + this.lastPong = -1; + this.lastPing = -1; + this.minPing = -1; + this.blockTime = -1; - this.bestHash = null; - this.bestHeight = -1; + this.bestHash = null; + this.bestHeight = -1; - this.connectTimeout = null; - this.pingTimer = null; - this.invTimer = null; - this.stallTimer = null; + this.connectTimeout = null; + this.pingTimer = null; + this.invTimer = null; + this.stallTimer = null; - this.addrFilter = new RollingFilter(5000, 0.001); - this.invFilter = new RollingFilter(50000, 0.000001); + this.addrFilter = new RollingFilter(5000, 0.001); + this.invFilter = new RollingFilter(50000, 0.000001); - this.blockMap = new Map(); - this.txMap = new Map(); - this.responseMap = new Map(); - this.compactBlocks = new Map(); + this.blockMap = new Map(); + this.txMap = new Map(); + this.responseMap = new Map(); + this.compactBlocks = new Map(); - this.init(); + this.init(); + } + + /** + * Create inbound peer from socket. + * @param {PeerOptions} options + * @param {net.Socket} socket + * @returns {Peer} + */ + + static fromInbound(options, socket) { + const peer = new this(options); + peer.accept(socket); + return peer; + } + + /** + * Create outbound peer from net address. + * @param {PeerOptions} options + * @param {NetAddress} addr + * @returns {Peer} + */ + + static fromOutbound(options, addr) { + const peer = new this(options); + peer.connect(addr); + return peer; + } + + /** + * Create a peer from options. + * @param {Object} options + * @returns {Peer} + */ + + static fromOptions(options) { + return new this(new PeerOptions(options)); + } + + /** + * Begin peer initialization. + * @private + */ + + init() { + this.parser.on('packet', async (packet) => { + try { + await this.readPacket(packet); + } catch (e) { + this.error(e); + this.destroy(); + } + }); + + this.parser.on('error', (err) => { + if (this.destroyed) + return; + + this.error(err); + this.sendReject('malformed', 'error parsing message'); + this.increaseBan(10); + }); + } + + /** + * Getter to retrieve hostname. + * @returns {String} + */ + + hostname() { + return this.address.hostname; + } + + /** + * Frame a payload with a header. + * @param {String} cmd - Packet type. + * @param {Buffer} payload + * @returns {Buffer} Payload with header prepended. + */ + + framePacket(cmd, payload, checksum) { + if (this.bip151 && this.bip151.handshake) + return this.bip151.packet(cmd, payload); + return this.framer.packet(cmd, payload, checksum); + } + + /** + * Feed data to the parser. + * @param {Buffer} data + */ + + feedParser(data) { + if (this.bip151 && this.bip151.handshake) + return this.bip151.feed(data); + return this.parser.feed(data); + } + + /** + * Set BIP151 cipher type. + * @param {Number} cipher + */ + + setCipher(cipher) { + assert(!this.bip151, 'BIP151 already set.'); + assert(this.socket, 'Peer must be initialized with a socket.'); + assert(!this.opened, 'Cannot set cipher after open.'); + + this.bip151 = new BIP151(cipher); + + this.bip151.on('error', (err) => { + this.error(err); + this.destroy(); + }); + + this.bip151.on('rekey', () => { + if (this.destroyed) + return; + + this.logger.debug('Rekeying with peer (%s).', this.hostname()); + this.send(this.bip151.toRekey()); + }); + + this.bip151.on('packet', (cmd, body) => { + let payload = null; + try { + payload = this.parser.parsePayload(cmd, body); + } catch (e) { + this.parser.error(e); + return; + } + this.parser.emit('packet', payload); + }); + } + + /** + * Set BIP150 auth. + * @param {AuthDB} db + * @param {Buffer} key + */ + + setAuth(db, key) { + const bip151 = this.bip151; + const hostname = this.hostname(); + const outbound = this.outbound; + + assert(this.bip151, 'BIP151 not set.'); + assert(!this.bip150, 'BIP150 already set.'); + assert(this.socket, 'Peer must be initialized with a socket.'); + assert(!this.opened, 'Cannot set auth after open.'); + + this.bip150 = new BIP150(bip151, hostname, outbound, db, key); + this.bip151.bip150 = this.bip150; + } + + /** + * Bind to socket. + * @param {net.Socket} socket + */ + + _bind(socket) { + assert(!this.socket); + + this.socket = socket; + + this.socket.once('error', (err) => { + if (!this.connected) + return; + + this.error(err); + this.destroy(); + }); + + this.socket.once('close', () => { + this.error('Socket hangup.'); + this.destroy(); + }); + + this.socket.on('drain', () => { + this.handleDrain(); + }); + + this.socket.on('data', (chunk) => { + this.lastRecv = Date.now(); + this.feedParser(chunk); + }); + + this.socket.setNoDelay(true); + } + + /** + * Accept an inbound socket. + * @param {net.Socket} socket + * @returns {net.Socket} + */ + + accept(socket) { + assert(!this.socket); + + this.address = NetAddress.fromSocket(socket, this.network); + this.address.services = 0; + this.time = Date.now(); + this.outbound = false; + this.connected = true; + + this._bind(socket); + + return socket; + } + + /** + * Create the socket and begin connecting. This method + * will use `options.createSocket` if provided. + * @param {NetAddress} addr + * @returns {net.Socket} + */ + + connect(addr) { + assert(!this.socket); + + const socket = this.options.createSocket(addr.port, addr.host); + + this.address = addr; + this.outbound = true; + this.connected = false; + + this._bind(socket); + + return socket; + } + + /** + * Open and perform initial handshake (without rejection). + * @method + * @returns {Promise} + */ + + async tryOpen() { + try { + await this.open(); + } catch (e) { + ; + } + } + + /** + * Open and perform initial handshake. + * @method + * @returns {Promise} + */ + + async open() { + try { + await this._open(); + } catch (e) { + this.error(e); + this.destroy(); + throw e; + } + } + + /** + * Open and perform initial handshake. + * @method + * @returns {Promise} + */ + + async _open() { + this.opened = true; + + // Connect to peer. + await this.initConnect(); + await this.initStall(); + await this.initBIP151(); + await this.initBIP150(); + await this.initVersion(); + await this.finalize(); + + assert(!this.destroyed); + + // Finally we can let the pool know + // that this peer is ready to go. + this.emit('open'); + } + + /** + * Wait for connection. + * @private + * @returns {Promise} + */ + + initConnect() { + if (this.connected) { + assert(!this.outbound); + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const cleanup = () => { + if (this.connectTimeout != null) { + clearTimeout(this.connectTimeout); + this.connectTimeout = null; + } + // eslint-disable-next-line no-use-before-define + this.socket.removeListener('error', onError); + }; + + const onError = (err) => { + cleanup(); + reject(err); + }; + + this.socket.once('connect', () => { + this.time = Date.now(); + this.connected = true; + this.emit('connect'); + + cleanup(); + resolve(); + }); + + this.socket.once('error', onError); + + this.connectTimeout = setTimeout(() => { + this.connectTimeout = null; + cleanup(); + reject(new Error('Connection timed out.')); + }, 10000); + }); + } + + /** + * Setup stall timer. + * @private + * @returns {Promise} + */ + + initStall() { + assert(!this.stallTimer); + assert(!this.destroyed); + this.stallTimer = setInterval(() => { + this.maybeTimeout(); + }, Peer.STALL_INTERVAL); + return Promise.resolve(); + } + + /** + * Handle `connect` event (called immediately + * if a socket was passed into peer). + * @method + * @private + * @returns {Promise} + */ + + async initBIP151() { + assert(!this.destroyed); + + // Send encinit. Wait for handshake to complete. + if (!this.bip151) + return; + + assert(!this.bip151.completed); + + this.logger.info('Attempting BIP151 handshake (%s).', this.hostname()); + + this.send(this.bip151.toEncinit()); + + try { + await this.bip151.wait(3000); + } catch (err) { + this.error(err); + } + + if (this.destroyed) + throw new Error('Peer was destroyed during BIP151 handshake.'); + + assert(this.bip151.completed); + + if (this.bip151.handshake) { + this.logger.info('BIP151 handshake complete (%s).', this.hostname()); + this.logger.info('Connection is encrypted (%s).', this.hostname()); + } + } + + /** + * Handle post bip151-handshake. + * @method + * @private + * @returns {Promise} + */ + + async initBIP150() { + assert(!this.destroyed); + + if (!this.bip150) + return; + + assert(this.bip151); + assert(!this.bip150.completed); + + if (!this.bip151.handshake) + throw new Error('BIP151 handshake was not completed for BIP150.'); + + this.logger.info('Attempting BIP150 handshake (%s).', this.hostname()); + + if (this.bip150.outbound) { + if (!this.bip150.peerIdentity) + throw new Error('No known identity for peer.'); + this.send(this.bip150.toChallenge()); + } + + await this.bip150.wait(3000); + + assert(!this.destroyed); + assert(this.bip150.completed); + + if (this.bip150.auth) { + this.logger.info('BIP150 handshake complete (%s).', this.hostname()); + this.logger.info('Peer is authed (%s): %s.', + this.hostname(), this.bip150.getAddress()); + } + } + + /** + * Handle post handshake. + * @method + * @private + * @returns {Promise} + */ + + async initVersion() { + assert(!this.destroyed); + + // Say hello. + this.sendVersion(); + + if (!this.ack) { + await this.wait(packetTypes.VERACK, 10000); + assert(this.ack); + } + + // Wait for _their_ version. + if (this.version === -1) { + this.logger.debug( + 'Peer sent a verack without a version (%s).', + this.hostname()); + + await this.wait(packetTypes.VERSION, 10000); + + assert(this.version !== -1); + } + + if (this.destroyed) + throw new Error('Peer was destroyed during handshake.'); + + this.handshake = true; + + this.logger.debug('Version handshake complete (%s).', this.hostname()); + } + + /** + * Finalize peer after handshake. + * @method + * @private + * @returns {Promise} + */ + + async finalize() { + assert(!this.destroyed); + + // Setup the ping interval. + this.pingTimer = setInterval(() => { + this.sendPing(); + }, Peer.PING_INTERVAL); + + // Setup the inv flusher. + this.invTimer = setInterval(() => { + this.flushInv(); + }, Peer.INV_INTERVAL); + } + + /** + * Broadcast blocks to peer. + * @param {Block[]} blocks + */ + + announceBlock(blocks) { + if (!this.handshake) + return; + + if (this.destroyed) + return; + + if (!Array.isArray(blocks)) + blocks = [blocks]; + + const inv = []; + + for (const block of blocks) { + assert(block instanceof Block); + + // Don't send if they already have it. + if (this.invFilter.test(block.hash())) + continue; + + // Send them the block immediately if + // they're using compact block mode 1. + if (this.compactMode === 1) { + this.invFilter.add(block.hash()); + this.sendCompactBlock(block); + continue; + } + + // Convert item to block headers + // for peers that request it. + if (this.preferHeaders) { + inv.push(block.toHeaders()); + continue; + } + + inv.push(block.toInv()); + } + + if (this.preferHeaders) { + this.sendHeaders(inv); + return; + } + + this.queueInv(inv); + } + + /** + * Broadcast transactions to peer. + * @param {TX[]} txs + */ + + announceTX(txs) { + if (!this.handshake) + return; + + if (this.destroyed) + return; + + // Do not send txs to spv clients + // that have relay unset. + if (this.noRelay) + return; + + if (!Array.isArray(txs)) + txs = [txs]; + + const inv = []; + + for (const tx of txs) { + assert(tx instanceof TX); + + // Don't send if they already have it. + if (this.invFilter.test(tx.hash())) + continue; + + // Check the peer's bloom + // filter if they're using spv. + if (this.spvFilter) { + if (!tx.isWatched(this.spvFilter)) + continue; + } + + // Check the fee filter. + if (this.feeRate !== -1) { + const hash = tx.hash('hex'); + const rate = this.options.getRate(hash); + if (rate !== -1 && rate < this.feeRate) + continue; + } + + inv.push(tx.toInv()); + } + + this.queueInv(inv); + } + + /** + * Send inv to a peer. + * @param {InvItem[]} items + */ + + queueInv(items) { + if (!this.handshake) + return; + + if (this.destroyed) + return; + + if (!Array.isArray(items)) + items = [items]; + + let hasBlock = false; + + for (const item of items) { + if (item.type === invTypes.BLOCK) + hasBlock = true; + this.invQueue.push(item); + } + + if (this.invQueue.length >= 500 || hasBlock) + this.flushInv(); + } + + /** + * Flush inv queue. + * @private + */ + + flushInv() { + if (this.destroyed) + return; + + const queue = this.invQueue; + + if (queue.length === 0) + return; + + this.invQueue = []; + + this.logger.spam('Serving %d inv items to %s.', + queue.length, this.hostname()); + + const items = []; + + for (const item of queue) { + if (!this.invFilter.added(item.hash, 'hex')) + continue; + + items.push(item); + } + + for (let i = 0; i < items.length; i += 1000) { + const chunk = items.slice(i, i + 1000); + this.send(new packets.InvPacket(chunk)); + } + } + + /** + * Force send an inv (no filter check). + * @param {InvItem[]} items + */ + + sendInv(items) { + if (!this.handshake) + return; + + if (this.destroyed) + return; + + if (!Array.isArray(items)) + items = [items]; + + for (const item of items) + this.invFilter.add(item.hash, 'hex'); + + if (items.length === 0) + return; + + this.logger.spam('Serving %d inv items to %s.', + items.length, this.hostname()); + + for (let i = 0; i < items.length; i += 1000) { + const chunk = items.slice(i, i + 1000); + this.send(new packets.InvPacket(chunk)); + } + } + + /** + * Send headers to a peer. + * @param {Headers[]} items + */ + + sendHeaders(items) { + if (!this.handshake) + return; + + if (this.destroyed) + return; + + if (!Array.isArray(items)) + items = [items]; + + for (const item of items) + this.invFilter.add(item.hash()); + + if (items.length === 0) + return; + + this.logger.spam('Serving %d headers to %s.', + items.length, this.hostname()); + + for (let i = 0; i < items.length; i += 2000) { + const chunk = items.slice(i, i + 2000); + this.send(new packets.HeadersPacket(chunk)); + } + } + + /** + * Send a compact block. + * @private + * @param {Block} block + * @returns {Boolean} + */ + + sendCompactBlock(block) { + const witness = this.compactWitness; + const compact = BIP152.CompactBlock.fromBlock(block, witness); + this.send(new packets.CmpctBlockPacket(compact, witness)); + } + + /** + * Send a `version` packet. + */ + + sendVersion() { + const packet = new packets.VersionPacket(); + packet.version = this.options.version; + packet.services = this.options.services; + packet.time = this.network.now(); + packet.remote = this.address; + packet.local.setNull(); + packet.local.services = this.options.services; + packet.nonce = this.options.createNonce(this.hostname()); + packet.agent = this.options.agent; + packet.height = this.options.getHeight(); + packet.noRelay = this.options.noRelay; + this.send(packet); + } + + /** + * Send a `getaddr` packet. + */ + + sendGetAddr() { + if (this.sentGetAddr) + return; + + this.sentGetAddr = true; + this.send(new packets.GetAddrPacket()); + } + + /** + * Send a `ping` packet. + */ + + sendPing() { + if (!this.handshake) + return; + + if (this.version <= common.PONG_VERSION) { + this.send(new packets.PingPacket()); + return; + } + + if (this.challenge) { + this.logger.debug( + 'Peer has not responded to ping (%s).', + this.hostname()); + return; + } + + this.lastPing = Date.now(); + this.challenge = common.nonce(); + + this.send(new packets.PingPacket(this.challenge)); + } + + /** + * Send `filterload` to update the local bloom filter. + */ + + sendFilterLoad(filter) { + if (!this.handshake) + return; + + if (!this.options.spv) + return; + + if (!(this.services & services.BLOOM)) + return; + + this.send(new packets.FilterLoadPacket(filter)); + } + + /** + * Set a fee rate filter for the peer. + * @param {Rate} rate + */ + + sendFeeRate(rate) { + if (!this.handshake) + return; + + this.send(new packets.FeeFilterPacket(rate)); + } + + /** + * Disconnect from and destroy the peer. + */ + + destroy() { + const connected = this.connected; + + if (this.destroyed) + return; + + this.destroyed = true; + this.connected = false; + + this.socket.destroy(); + this.socket = null; + + if (this.bip151) + this.bip151.destroy(); + + if (this.bip150) + this.bip150.destroy(); + + if (this.pingTimer != null) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + + if (this.invTimer != null) { + clearInterval(this.invTimer); + this.invTimer = null; + } + + if (this.stallTimer != null) { + clearInterval(this.stallTimer); + this.stallTimer = null; + } + + if (this.connectTimeout != null) { + clearTimeout(this.connectTimeout); + this.connectTimeout = null; + } + + const jobs = this.drainQueue; + + this.drainSize = 0; + this.drainQueue = []; + + for (const job of jobs) + job.reject(new Error('Peer was destroyed.')); + + for (const [cmd, entry] of this.responseMap) { + this.responseMap.delete(cmd); + entry.reject(new Error('Peer was destroyed.')); + } + + this.locker.destroy(); + + this.emit('close', connected); + } + + /** + * Write data to the peer's socket. + * @param {Buffer} data + */ + + write(data) { + if (this.destroyed) + throw new Error('Peer is destroyed (write).'); + + this.lastSend = Date.now(); + + if (this.socket.write(data) === false) + this.needsDrain(data.length); + } + + /** + * Send a packet. + * @param {Packet} packet + */ + + send(packet) { + if (this.destroyed) + throw new Error('Peer is destroyed (send).'); + + // Used cached hashes as the + // packet checksum for speed. + let checksum = null; + if (packet.type === packetTypes.TX) { + const tx = packet.tx; + if (packet.witness) { + if (!tx.isCoinbase()) + checksum = tx.witnessHash(); + } else { + checksum = tx.hash(); + } + } + + this.sendRaw(packet.cmd, packet.toRaw(), checksum); + + this.addTimeout(packet); + } + + /** + * Send a packet. + * @param {Packet} packet + */ + + sendRaw(cmd, body, checksum) { + const payload = this.framePacket(cmd, body, checksum); + this.write(payload); + } + + /** + * Wait for a drain event. + * @returns {Promise} + */ + + drain() { + if (this.destroyed) + return Promise.reject(new Error('Peer is destroyed.')); + + if (this.drainSize === 0) + return Promise.resolve(); + + return new Promise((resolve, reject) => { + this.drainQueue.push({ resolve, reject }); + }); + } + + /** + * Handle drain event. + * @private + */ + + handleDrain() { + const jobs = this.drainQueue; + + this.drainSize = 0; + + if (jobs.length === 0) + return; + + this.drainQueue = []; + + for (const job of jobs) + job.resolve(); + } + + /** + * Add to drain counter. + * @private + * @param {Number} size + */ + + needsDrain(size) { + this.drainSize += size; + + if (this.drainSize >= Peer.DRAIN_MAX) { + this.logger.warning( + 'Peer is not reading: %dmb buffered (%s).', + this.drainSize / (1 << 20), + this.hostname()); + this.error('Peer stalled (drain).'); + this.destroy(); + } + } + + /** + * Potentially add response timeout. + * @private + * @param {Packet} packet + */ + + addTimeout(packet) { + const timeout = Peer.RESPONSE_TIMEOUT; + + if (!this.outbound) + return; + + switch (packet.type) { + case packetTypes.MEMPOOL: + this.request(packetTypes.INV, timeout); + break; + case packetTypes.GETBLOCKS: + if (!this.options.isFull()) + this.request(packetTypes.INV, timeout); + break; + case packetTypes.GETHEADERS: + this.request(packetTypes.HEADERS, timeout * 2); + break; + case packetTypes.GETDATA: + this.request(packetTypes.DATA, timeout * 2); + break; + case packetTypes.GETBLOCKTXN: + this.request(packetTypes.BLOCKTXN, timeout); + break; + } + } + + /** + * Potentially finish response timeout. + * @private + * @param {Packet} packet + */ + + fulfill(packet) { + switch (packet.type) { + case packetTypes.BLOCK: + case packetTypes.CMPCTBLOCK: + case packetTypes.MERKLEBLOCK: + case packetTypes.TX: + case packetTypes.NOTFOUND: { + const entry = this.response(packetTypes.DATA, packet); + assert(!entry || entry.jobs.length === 0); + break; + } + } + + return this.response(packet.type, packet); + } + + /** + * Potentially timeout peer if it hasn't responded. + * @private + */ + + maybeTimeout() { + const now = Date.now(); + + for (const [key, entry] of this.responseMap) { + if (now > entry.timeout) { + const name = packets.typesByVal[key]; + this.error('Peer is stalling (%s).', name.toLowerCase()); + this.destroy(); + return; + } + } + + if (this.merkleBlock) { + assert(this.merkleTime !== -1); + if (now > this.merkleTime + Peer.BLOCK_TIMEOUT) { + this.error('Peer is stalling (merkleblock).'); + this.destroy(); + return; + } + } + + if (this.syncing && this.loader && !this.options.isFull()) { + if (now > this.blockTime + Peer.BLOCK_TIMEOUT) { + this.error('Peer is stalling (block).'); + this.destroy(); + return; + } + } + + if (this.options.isFull() || !this.syncing) { + for (const time of this.blockMap.values()) { + if (now > time + Peer.BLOCK_TIMEOUT) { + this.error('Peer is stalling (block).'); + this.destroy(); + return; + } + } + + for (const time of this.txMap.values()) { + if (now > time + Peer.TX_TIMEOUT) { + this.error('Peer is stalling (tx).'); + this.destroy(); + return; + } + } + + for (const block of this.compactBlocks.values()) { + if (now > block.now + Peer.RESPONSE_TIMEOUT) { + this.error('Peer is stalling (blocktxn).'); + this.destroy(); + return; + } + } + } + + if (now > this.time + 60000) { + assert(this.time !== 0); + + if (this.lastRecv === 0 || this.lastSend === 0) { + this.error('Peer is stalling (no message).'); + this.destroy(); + return; + } + + if (now > this.lastSend + Peer.TIMEOUT_INTERVAL) { + this.error('Peer is stalling (send).'); + this.destroy(); + return; + } + + const mult = this.version <= common.PONG_VERSION ? 4 : 1; + + if (now > this.lastRecv + Peer.TIMEOUT_INTERVAL * mult) { + this.error('Peer is stalling (recv).'); + this.destroy(); + return; + } + + if (this.challenge && now > this.lastPing + Peer.TIMEOUT_INTERVAL) { + this.error('Peer is stalling (ping).'); + this.destroy(); + return; + } + } + } + + /** + * Wait for a packet to be received from peer. + * @private + * @param {Number} type - Packet type. + * @param {Number} timeout + * @returns {RequestEntry} + */ + + request(type, timeout) { + if (this.destroyed) + return null; + + let entry = this.responseMap.get(type); + + if (!entry) { + entry = new RequestEntry(); + this.responseMap.set(type, entry); + } + + entry.setTimeout(timeout); + + return entry; + } + + /** + * Fulfill awaiting requests created with {@link Peer#request}. + * @private + * @param {Number} type - Packet type. + * @param {Object} payload + */ + + response(type, payload) { + const entry = this.responseMap.get(type); + + if (!entry) + return null; + + this.responseMap.delete(type); + + return entry; + } + + /** + * Wait for a packet to be received from peer. + * @private + * @param {Number} type - Packet type. + * @returns {Promise} - Returns Object(payload). + * Executed on timeout or once packet is received. + */ + + wait(type, timeout) { + return new Promise((resolve, reject) => { + if (this.destroyed) { + reject(new Error('Peer is destroyed (request).')); + return; + } + + const entry = this.request(type); + + entry.setTimeout(timeout); + entry.addJob(resolve, reject); + }); + } + + /** + * Emit an error and destroy the peer. + * @private + * @param {...String|Error} err + */ + + error(err) { + if (this.destroyed) + return; + + if (typeof err === 'string') { + const msg = format.apply(null, arguments); + err = new Error(msg); + } + + if (typeof err.code === 'string' && err.code[0] === 'E') { + const msg = err.code; + err = new Error(msg); + err.code = msg; + err.message = `Socket Error: ${msg}`; + } + + err.message += ` (${this.hostname()})`; + + this.emit('error', err); + } + + /** + * Calculate peer block inv type (filtered, + * compact, witness, or non-witness). + * @returns {Number} + */ + + blockType() { + if (this.options.spv) + return invTypes.FILTERED_BLOCK; + + if (this.options.compact + && this.hasCompactSupport() + && this.hasCompact()) { + return invTypes.CMPCT_BLOCK; + } + + if (this.hasWitness()) + return invTypes.WITNESS_BLOCK; + + return invTypes.BLOCK; + } + + /** + * Calculate peer tx inv type (witness or non-witness). + * @returns {Number} + */ + + txType() { + if (this.hasWitness()) + return invTypes.WITNESS_TX; + + return invTypes.TX; + } + + /** + * Send `getdata` to peer. + * @param {InvItem[]} items + */ + + getData(items) { + this.send(new packets.GetDataPacket(items)); + } + + /** + * Send batched `getdata` to peer. + * @param {InvType} type + * @param {Hash[]} hashes + */ + + getItems(type, hashes) { + const items = []; + + for (const hash of hashes) + items.push(new InvItem(type, hash)); + + if (items.length === 0) + return; + + this.getData(items); + } + + /** + * Send batched `getdata` to peer (blocks). + * @param {Hash[]} hashes + */ + + getBlock(hashes) { + this.getItems(this.blockType(), hashes); + } + + /** + * Send batched `getdata` to peer (txs). + * @param {Hash[]} hashes + */ + + getTX(hashes) { + this.getItems(this.txType(), hashes); + } + + /** + * Send `getdata` to peer for a single block. + * @param {Hash} hash + */ + + getFullBlock(hash) { + assert(!this.options.spv); + + let type = invTypes.BLOCK; + + if (this.hasWitness()) + type |= InvItem.WITNESS_FLAG; + + this.getItems(type, [hash]); + } + + /** + * Handle a packet payload. + * @method + * @private + * @param {Packet} packet + */ + + async readPacket(packet) { + if (this.destroyed) + return; + + // The "pre-handshake" packets get + // to bypass the lock, since they + // are meant to change the way input + // is handled at a low level. They + // must be handled immediately. + switch (packet.type) { + case packetTypes.ENCINIT: + case packetTypes.ENCACK: + case packetTypes.AUTHCHALLENGE: + case packetTypes.AUTHREPLY: + case packetTypes.AUTHPROPOSE: + case packetTypes.PONG: { + try { + this.socket.pause(); + await this.handlePacket(packet); + } finally { + if (!this.destroyed) + this.socket.resume(); + } + break; + } + default: { + const unlock = await this.locker.lock(); + try { + this.socket.pause(); + await this.handlePacket(packet); + } finally { + if (!this.destroyed) + this.socket.resume(); + unlock(); + } + break; + } + } + } + + /** + * Handle a packet payload without a lock. + * @method + * @private + * @param {Packet} packet + */ + + async handlePacket(packet) { + if (this.destroyed) + throw new Error('Destroyed peer sent a packet.'); + + if (this.bip151 + && this.bip151.job + && !this.bip151.completed + && packet.type !== packetTypes.ENCINIT + && packet.type !== packetTypes.ENCACK) { + this.bip151.reject(new Error('Message before BIP151 handshake.')); + } + + if (this.bip150 + && this.bip150.job + && !this.bip150.completed + && packet.type !== packetTypes.AUTHCHALLENGE + && packet.type !== packetTypes.AUTHREPLY + && packet.type !== packetTypes.AUTHPROPOSE) { + this.bip150.reject(new Error('Message before BIP150 auth.')); + } + + const entry = this.fulfill(packet); + + switch (packet.type) { + case packetTypes.VERSION: + await this.handleVersion(packet); + break; + case packetTypes.VERACK: + await this.handleVerack(packet); + break; + case packetTypes.PING: + await this.handlePing(packet); + break; + case packetTypes.PONG: + await this.handlePong(packet); + break; + case packetTypes.SENDHEADERS: + await this.handleSendHeaders(packet); + break; + case packetTypes.FILTERLOAD: + await this.handleFilterLoad(packet); + break; + case packetTypes.FILTERADD: + await this.handleFilterAdd(packet); + break; + case packetTypes.FILTERCLEAR: + await this.handleFilterClear(packet); + break; + case packetTypes.FEEFILTER: + await this.handleFeeFilter(packet); + break; + case packetTypes.SENDCMPCT: + await this.handleSendCmpct(packet); + break; + case packetTypes.ENCINIT: + await this.handleEncinit(packet); + break; + case packetTypes.ENCACK: + await this.handleEncack(packet); + break; + case packetTypes.AUTHCHALLENGE: + await this.handleAuthChallenge(packet); + break; + case packetTypes.AUTHREPLY: + await this.handleAuthReply(packet); + break; + case packetTypes.AUTHPROPOSE: + await this.handleAuthPropose(packet); + break; + } + + if (this.onPacket) + await this.onPacket(packet); + + this.emit('packet', packet); + + if (entry) + entry.resolve(packet); + } + + /** + * Handle `version` packet. + * @method + * @private + * @param {VersionPacket} packet + */ + + async handleVersion(packet) { + if (this.version !== -1) + throw new Error('Peer sent a duplicate version.'); + + this.version = packet.version; + this.services = packet.services; + this.height = packet.height; + this.agent = packet.agent; + this.noRelay = packet.noRelay; + this.local = packet.remote; + + if (!this.network.selfConnect) { + if (this.options.hasNonce(packet.nonce)) + throw new Error('We connected to ourself. Oops.'); + } + + if (this.version < common.MIN_VERSION) + throw new Error('Peer does not support required protocol version.'); + + if (this.outbound) { + if (!(this.services & services.NETWORK)) + throw new Error('Peer does not support network services.'); + + if (this.options.headers) { + if (this.version < common.HEADERS_VERSION) + throw new Error('Peer does not support getheaders.'); + } + + if (this.options.spv) { + if (!(this.services & services.BLOOM)) + throw new Error('Peer does not support BIP37.'); + + if (this.version < common.BLOOM_VERSION) + throw new Error('Peer does not support BIP37.'); + } + + if (this.options.hasWitness()) { + if (!(this.services & services.WITNESS)) + throw new Error('Peer does not support segregated witness.'); + } + + if (this.options.compact) { + if (!this.hasCompactSupport()) { + this.logger.debug( + 'Peer does not support compact blocks (%s).', + this.hostname()); + } + } + } + + this.send(new packets.VerackPacket()); + } + + /** + * Handle `verack` packet. + * @method + * @private + * @param {VerackPacket} packet + */ + + async handleVerack(packet) { + if (this.ack) { + this.logger.debug('Peer sent duplicate ack (%s).', this.hostname()); + return; + } + + this.ack = true; + this.logger.debug('Received verack (%s).', this.hostname()); + } + + /** + * Handle `ping` packet. + * @method + * @private + * @param {PingPacket} packet + */ + + async handlePing(packet) { + if (!packet.nonce) + return; + + this.send(new packets.PongPacket(packet.nonce)); + } + + /** + * Handle `pong` packet. + * @method + * @private + * @param {PongPacket} packet + */ + + async handlePong(packet) { + const nonce = packet.nonce; + const now = Date.now(); + + if (!this.challenge) { + this.logger.debug('Peer sent an unsolicited pong (%s).', this.hostname()); + return; + } + + if (!nonce.equals(this.challenge)) { + if (nonce.equals(encoding.ZERO_U64)) { + this.logger.debug('Peer sent a zero nonce (%s).', this.hostname()); + this.challenge = null; + return; + } + this.logger.debug('Peer sent the wrong nonce (%s).', this.hostname()); + return; + } + + if (now >= this.lastPing) { + this.lastPong = now; + if (this.minPing === -1) + this.minPing = now - this.lastPing; + this.minPing = Math.min(this.minPing, now - this.lastPing); + } else { + this.logger.debug('Timing mismatch (what?) (%s).', this.hostname()); + } + + this.challenge = null; + } + + /** + * Handle `sendheaders` packet. + * @method + * @private + * @param {SendHeadersPacket} packet + */ + + async handleSendHeaders(packet) { + if (this.preferHeaders) { + this.logger.debug( + 'Peer sent a duplicate sendheaders (%s).', + this.hostname()); + return; + } + + this.preferHeaders = true; + } + + /** + * Handle `filterload` packet. + * @method + * @private + * @param {FilterLoadPacket} packet + */ + + async handleFilterLoad(packet) { + if (!packet.isWithinConstraints()) { + this.increaseBan(100); + return; + } + + this.spvFilter = packet.filter; + this.noRelay = false; + } + + /** + * Handle `filteradd` packet. + * @method + * @private + * @param {FilterAddPacket} packet + */ + + async handleFilterAdd(packet) { + const data = packet.data; + + if (data.length > consensus.MAX_SCRIPT_PUSH) { + this.increaseBan(100); + return; + } + + if (this.spvFilter) + this.spvFilter.add(data); + + this.noRelay = false; + } + + /** + * Handle `filterclear` packet. + * @method + * @private + * @param {FilterClearPacket} packet + */ + + async handleFilterClear(packet) { + if (this.spvFilter) + this.spvFilter.reset(); + + this.noRelay = false; + } + + /** + * Handle `feefilter` packet. + * @method + * @private + * @param {FeeFilterPacket} packet + */ + + async handleFeeFilter(packet) { + const rate = packet.rate; + + if (rate < 0 || rate > consensus.MAX_MONEY) { + this.increaseBan(100); + return; + } + + this.feeRate = rate; + } + + /** + * Handle `sendcmpct` packet. + * @method + * @private + * @param {SendCmpctPacket} + */ + + async handleSendCmpct(packet) { + if (this.compactMode !== -1) { + this.logger.debug( + 'Peer sent a duplicate sendcmpct (%s).', + this.hostname()); + return; + } + + if (packet.version > 2) { + // Ignore + this.logger.info( + 'Peer request compact blocks version %d (%s).', + packet.version, this.hostname()); + return; + } + + if (packet.mode > 1) { + this.logger.info( + 'Peer request compact blocks mode %d (%s).', + packet.mode, this.hostname()); + return; + } + + this.logger.info( + 'Peer initialized compact blocks (mode=%d, version=%d) (%s).', + packet.mode, packet.version, this.hostname()); + + this.compactMode = packet.mode; + this.compactWitness = packet.version === 2; + } + + /** + * Handle `encinit` packet. + * @method + * @private + * @param {EncinitPacket} packet + */ + + async handleEncinit(packet) { + if (!this.bip151) + return; + + this.bip151.encinit(packet.publicKey, packet.cipher); + + this.send(this.bip151.toEncack()); + } + + /** + * Handle `encack` packet. + * @method + * @private + * @param {EncackPacket} packet + */ + + async handleEncack(packet) { + if (!this.bip151) + return; + + this.bip151.encack(packet.publicKey); + } + + /** + * Handle `authchallenge` packet. + * @method + * @private + * @param {AuthChallengePacket} packet + */ + + async handleAuthChallenge(packet) { + if (!this.bip150) + return; + + const sig = this.bip150.challenge(packet.hash); + + this.send(new packets.AuthReplyPacket(sig)); + } + + /** + * Handle `authreply` packet. + * @method + * @private + * @param {AuthReplyPacket} packet + */ + + async handleAuthReply(packet) { + if (!this.bip150) + return; + + const hash = this.bip150.reply(packet.signature); + + if (hash) + this.send(new packets.AuthProposePacket(hash)); + } + + /** + * Handle `authpropose` packet. + * @method + * @private + * @param {AuthProposePacket} packet + */ + + async handleAuthPropose(packet) { + if (!this.bip150) + return; + + const hash = this.bip150.propose(packet.hash); + + this.send(new packets.AuthChallengePacket(hash)); + } + + /** + * Send `getheaders` to peer. Note that unlike + * `getblocks`, `getheaders` can have a null locator. + * @param {Hash[]?} locator - Chain locator. + * @param {Hash?} stop - Hash to stop at. + */ + + sendGetHeaders(locator, stop) { + const packet = new packets.GetHeadersPacket(locator, stop); + + let hash = null; + if (packet.locator.length > 0) + hash = encoding.revHex(packet.locator[0]); + + let end = null; + if (stop) + end = encoding.revHex(stop); + + this.logger.debug( + 'Requesting headers packet from peer with getheaders (%s).', + this.hostname()); + + this.logger.debug( + 'Sending getheaders (hash=%s, stop=%s).', + hash, end); + + this.send(packet); + } + + /** + * Send `getblocks` to peer. + * @param {Hash[]} locator - Chain locator. + * @param {Hash?} stop - Hash to stop at. + */ + + sendGetBlocks(locator, stop) { + const packet = new packets.GetBlocksPacket(locator, stop); + + let hash = null; + if (packet.locator.length > 0) + hash = encoding.revHex(packet.locator[0]); + + let end = null; + if (stop) + end = encoding.revHex(stop); + + this.logger.debug( + 'Requesting inv packet from peer with getblocks (%s).', + this.hostname()); + + this.logger.debug( + 'Sending getblocks (hash=%s, stop=%s).', + hash, end); + + this.send(packet); + } + + /** + * Send `mempool` to peer. + */ + + sendMempool() { + if (!this.handshake) + return; + + if (!(this.services & services.BLOOM)) { + this.logger.debug( + 'Cannot request mempool for non-bloom peer (%s).', + this.hostname()); + return; + } + + this.logger.debug( + 'Requesting inv packet from peer with mempool (%s).', + this.hostname()); + + this.send(new packets.MempoolPacket()); + } + + /** + * Send `reject` to peer. + * @param {Number} code + * @param {String} reason + * @param {String} msg + * @param {Hash} hash + */ + + sendReject(code, reason, msg, hash) { + const reject = packets.RejectPacket.fromReason(code, reason, msg, hash); + + if (msg) { + this.logger.debug('Rejecting %s %s (%s): code=%s reason=%s.', + msg, encoding.revHex(hash), this.hostname(), code, reason); + } else { + this.logger.debug('Rejecting packet from %s: code=%s reason=%s.', + this.hostname(), code, reason); + } + + this.logger.debug( + 'Sending reject packet to peer (%s).', + this.hostname()); + + this.send(reject); + } + + /** + * Send a `sendcmpct` packet. + * @param {Number} mode + */ + + sendCompact(mode) { + if (this.services & common.services.WITNESS) { + if (this.version >= common.COMPACT_WITNESS_VERSION) { + this.logger.info( + 'Initializing witness compact blocks (%s).', + this.hostname()); + this.send(new packets.SendCmpctPacket(mode, 2)); + return; + } + } + + if (this.version >= common.COMPACT_VERSION) { + this.logger.info( + 'Initializing normal compact blocks (%s).', + this.hostname()); + + this.send(new packets.SendCmpctPacket(mode, 1)); + } + } + + /** + * Increase banscore on peer. + * @param {Number} score + * @returns {Boolean} + */ + + increaseBan(score) { + this.banScore += score; + + if (this.banScore >= this.options.banScore) { + this.logger.debug('Ban threshold exceeded (%s).', this.hostname()); + this.ban(); + return true; + } + + return false; + } + + /** + * Ban peer. + */ + + ban() { + this.emit('ban'); + } + + /** + * Send a `reject` packet to peer. + * @param {String} msg + * @param {VerifyError} err + * @returns {Boolean} + */ + + reject(msg, err) { + this.sendReject(err.code, err.reason, msg, err.hash); + return this.increaseBan(err.score); + } + + /** + * Test whether required services are available. + * @param {Number} services + * @returns {Boolean} + */ + + hasServices(services) { + return (this.services & services) === services; + } + + /** + * Test whether the WITNESS service bit is set. + * @returns {Boolean} + */ + + hasWitness() { + return (this.services & services.WITNESS) !== 0; + } + + /** + * Test whether the peer supports compact blocks. + * @returns {Boolean} + */ + + hasCompactSupport() { + if (this.version < common.COMPACT_VERSION) + return false; + + if (!this.options.hasWitness()) + return true; + + if (!(this.services & services.WITNESS)) + return false; + + return this.version >= common.COMPACT_WITNESS_VERSION; + } + + /** + * Test whether the peer sent us a + * compatible compact block handshake. + * @returns {Boolean} + */ + + hasCompact() { + if (this.compactMode === -1) + return false; + + if (!this.options.hasWitness()) + return true; + + if (!this.compactWitness) + return false; + + return true; + } + + /** + * Inspect the peer. + * @returns {String} + */ + + inspect() { + return ''; + } } -Object.setPrototypeOf(Peer.prototype, EventEmitter.prototype); - /** * Max output bytes buffered before * invoking stall behavior for peer. @@ -221,2240 +2224,250 @@ Peer.TX_TIMEOUT = 120000; Peer.TIMEOUT_INTERVAL = 20 * 60000; /** - * Create inbound peer from socket. - * @param {PeerOptions} options - * @param {net.Socket} socket - * @returns {Peer} - */ - -Peer.fromInbound = function fromInbound(options, socket) { - const peer = new Peer(options); - peer.accept(socket); - return peer; -}; - -/** - * Create outbound peer from net address. - * @param {PeerOptions} options - * @param {NetAddress} addr - * @returns {Peer} - */ - -Peer.fromOutbound = function fromOutbound(options, addr) { - const peer = new Peer(options); - peer.connect(addr); - return peer; -}; - -/** - * Create a peer from options. - * @param {Object} options - * @returns {Peer} - */ - -Peer.fromOptions = function fromOptions(options) { - return new Peer(new PeerOptions(options)); -}; - -/** - * Begin peer initialization. - * @private - */ - -Peer.prototype.init = function init() { - this.parser.on('packet', async (packet) => { - try { - await this.readPacket(packet); - } catch (e) { - this.error(e); - this.destroy(); - } - }); - - this.parser.on('error', (err) => { - if (this.destroyed) - return; - - this.error(err); - this.sendReject('malformed', 'error parsing message'); - this.increaseBan(10); - }); -}; - -/** - * Getter to retrieve hostname. - * @returns {String} - */ - -Peer.prototype.hostname = function hostname() { - return this.address.hostname; -}; - -/** - * Frame a payload with a header. - * @param {String} cmd - Packet type. - * @param {Buffer} payload - * @returns {Buffer} Payload with header prepended. - */ - -Peer.prototype.framePacket = function framePacket(cmd, payload, checksum) { - if (this.bip151 && this.bip151.handshake) - return this.bip151.packet(cmd, payload); - return this.framer.packet(cmd, payload, checksum); -}; - -/** - * Feed data to the parser. - * @param {Buffer} data - */ - -Peer.prototype.feedParser = function feedParser(data) { - if (this.bip151 && this.bip151.handshake) - return this.bip151.feed(data); - return this.parser.feed(data); -}; - -/** - * Set BIP151 cipher type. - * @param {Number} cipher - */ - -Peer.prototype.setCipher = function setCipher(cipher) { - assert(!this.bip151, 'BIP151 already set.'); - assert(this.socket, 'Peer must be initialized with a socket.'); - assert(!this.opened, 'Cannot set cipher after open.'); - - this.bip151 = new BIP151(cipher); - - this.bip151.on('error', (err) => { - this.error(err); - this.destroy(); - }); - - this.bip151.on('rekey', () => { - if (this.destroyed) - return; - - this.logger.debug('Rekeying with peer (%s).', this.hostname()); - this.send(this.bip151.toRekey()); - }); - - this.bip151.on('packet', (cmd, body) => { - let payload = null; - try { - payload = this.parser.parsePayload(cmd, body); - } catch (e) { - this.parser.error(e); - return; - } - this.parser.emit('packet', payload); - }); -}; - -/** - * Set BIP150 auth. - * @param {AuthDB} db - * @param {Buffer} key - */ - -Peer.prototype.setAuth = function setAuth(db, key) { - const bip151 = this.bip151; - const hostname = this.hostname(); - const outbound = this.outbound; - - assert(this.bip151, 'BIP151 not set.'); - assert(!this.bip150, 'BIP150 already set.'); - assert(this.socket, 'Peer must be initialized with a socket.'); - assert(!this.opened, 'Cannot set auth after open.'); - - this.bip150 = new BIP150(bip151, hostname, outbound, db, key); - this.bip151.bip150 = this.bip150; -}; - -/** - * Bind to socket. - * @param {net.Socket} socket - */ - -Peer.prototype._bind = function _bind(socket) { - assert(!this.socket); - - this.socket = socket; - - this.socket.once('error', (err) => { - if (!this.connected) - return; - - this.error(err); - this.destroy(); - }); - - this.socket.once('close', () => { - this.error('Socket hangup.'); - this.destroy(); - }); - - this.socket.on('drain', () => { - this.handleDrain(); - }); - - this.socket.on('data', (chunk) => { - this.lastRecv = Date.now(); - this.feedParser(chunk); - }); - - this.socket.setNoDelay(true); -}; - -/** - * Accept an inbound socket. - * @param {net.Socket} socket - * @returns {net.Socket} - */ - -Peer.prototype.accept = function accept(socket) { - assert(!this.socket); - - this.address = NetAddress.fromSocket(socket, this.network); - this.address.services = 0; - this.time = Date.now(); - this.outbound = false; - this.connected = true; - - this._bind(socket); - - return socket; -}; - -/** - * Create the socket and begin connecting. This method - * will use `options.createSocket` if provided. - * @param {NetAddress} addr - * @returns {net.Socket} - */ - -Peer.prototype.connect = function connect(addr) { - assert(!this.socket); - - const socket = this.options.createSocket(addr.port, addr.host); - - this.address = addr; - this.outbound = true; - this.connected = false; - - this._bind(socket); - - return socket; -}; - -/** - * Open and perform initial handshake (without rejection). - * @method - * @returns {Promise} - */ - -Peer.prototype.tryOpen = async function tryOpen() { - try { - await this.open(); - } catch (e) { - ; - } -}; - -/** - * Open and perform initial handshake. - * @method - * @returns {Promise} - */ - -Peer.prototype.open = async function open() { - try { - await this._open(); - } catch (e) { - this.error(e); - this.destroy(); - throw e; - } -}; - -/** - * Open and perform initial handshake. - * @method - * @returns {Promise} - */ - -Peer.prototype._open = async function _open() { - this.opened = true; - - // Connect to peer. - await this.initConnect(); - await this.initStall(); - await this.initBIP151(); - await this.initBIP150(); - await this.initVersion(); - await this.finalize(); - - assert(!this.destroyed); - - // Finally we can let the pool know - // that this peer is ready to go. - this.emit('open'); -}; - -/** - * Wait for connection. - * @private - * @returns {Promise} - */ - -Peer.prototype.initConnect = function initConnect() { - if (this.connected) { - assert(!this.outbound); - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const cleanup = () => { - if (this.connectTimeout != null) { - clearTimeout(this.connectTimeout); - this.connectTimeout = null; - } - // eslint-disable-next-line no-use-before-define - this.socket.removeListener('error', onError); - }; - - const onError = (err) => { - cleanup(); - reject(err); - }; - - this.socket.once('connect', () => { - this.time = Date.now(); - this.connected = true; - this.emit('connect'); - - cleanup(); - resolve(); - }); - - this.socket.once('error', onError); - - this.connectTimeout = setTimeout(() => { - this.connectTimeout = null; - cleanup(); - reject(new Error('Connection timed out.')); - }, 10000); - }); -}; - -/** - * Setup stall timer. - * @private - * @returns {Promise} - */ - -Peer.prototype.initStall = function initStall() { - assert(!this.stallTimer); - assert(!this.destroyed); - this.stallTimer = setInterval(() => { - this.maybeTimeout(); - }, Peer.STALL_INTERVAL); - return Promise.resolve(); -}; - -/** - * Handle `connect` event (called immediately - * if a socket was passed into peer). - * @method - * @private - * @returns {Promise} - */ - -Peer.prototype.initBIP151 = async function initBIP151() { - assert(!this.destroyed); - - // Send encinit. Wait for handshake to complete. - if (!this.bip151) - return; - - assert(!this.bip151.completed); - - this.logger.info('Attempting BIP151 handshake (%s).', this.hostname()); - - this.send(this.bip151.toEncinit()); - - try { - await this.bip151.wait(3000); - } catch (err) { - this.error(err); - } - - if (this.destroyed) - throw new Error('Peer was destroyed during BIP151 handshake.'); - - assert(this.bip151.completed); - - if (this.bip151.handshake) { - this.logger.info('BIP151 handshake complete (%s).', this.hostname()); - this.logger.info('Connection is encrypted (%s).', this.hostname()); - } -}; - -/** - * Handle post bip151-handshake. - * @method - * @private - * @returns {Promise} - */ - -Peer.prototype.initBIP150 = async function initBIP150() { - assert(!this.destroyed); - - if (!this.bip150) - return; - - assert(this.bip151); - assert(!this.bip150.completed); - - if (!this.bip151.handshake) - throw new Error('BIP151 handshake was not completed for BIP150.'); - - this.logger.info('Attempting BIP150 handshake (%s).', this.hostname()); - - if (this.bip150.outbound) { - if (!this.bip150.peerIdentity) - throw new Error('No known identity for peer.'); - this.send(this.bip150.toChallenge()); - } - - await this.bip150.wait(3000); - - assert(!this.destroyed); - assert(this.bip150.completed); - - if (this.bip150.auth) { - this.logger.info('BIP150 handshake complete (%s).', this.hostname()); - this.logger.info('Peer is authed (%s): %s.', - this.hostname(), this.bip150.getAddress()); - } -}; - -/** - * Handle post handshake. - * @method - * @private - * @returns {Promise} - */ - -Peer.prototype.initVersion = async function initVersion() { - assert(!this.destroyed); - - // Say hello. - this.sendVersion(); - - if (!this.ack) { - await this.wait(packetTypes.VERACK, 10000); - assert(this.ack); - } - - // Wait for _their_ version. - if (this.version === -1) { - this.logger.debug( - 'Peer sent a verack without a version (%s).', - this.hostname()); - - await this.wait(packetTypes.VERSION, 10000); - - assert(this.version !== -1); - } - - if (this.destroyed) - throw new Error('Peer was destroyed during handshake.'); - - this.handshake = true; - - this.logger.debug('Version handshake complete (%s).', this.hostname()); -}; - -/** - * Finalize peer after handshake. - * @method - * @private - * @returns {Promise} - */ - -Peer.prototype.finalize = async function finalize() { - assert(!this.destroyed); - - // Setup the ping interval. - this.pingTimer = setInterval(() => { - this.sendPing(); - }, Peer.PING_INTERVAL); - - // Setup the inv flusher. - this.invTimer = setInterval(() => { - this.flushInv(); - }, Peer.INV_INTERVAL); -}; - -/** - * Broadcast blocks to peer. - * @param {Block[]} blocks - */ - -Peer.prototype.announceBlock = function announceBlock(blocks) { - if (!this.handshake) - return; - - if (this.destroyed) - return; - - if (!Array.isArray(blocks)) - blocks = [blocks]; - - const inv = []; - - for (const block of blocks) { - assert(block instanceof Block); - - // Don't send if they already have it. - if (this.invFilter.test(block.hash())) - continue; - - // Send them the block immediately if - // they're using compact block mode 1. - if (this.compactMode === 1) { - this.invFilter.add(block.hash()); - this.sendCompactBlock(block); - continue; - } - - // Convert item to block headers - // for peers that request it. - if (this.preferHeaders) { - inv.push(block.toHeaders()); - continue; - } - - inv.push(block.toInv()); - } - - if (this.preferHeaders) { - this.sendHeaders(inv); - return; - } - - this.queueInv(inv); -}; - -/** - * Broadcast transactions to peer. - * @param {TX[]} txs - */ - -Peer.prototype.announceTX = function announceTX(txs) { - if (!this.handshake) - return; - - if (this.destroyed) - return; - - // Do not send txs to spv clients - // that have relay unset. - if (this.noRelay) - return; - - if (!Array.isArray(txs)) - txs = [txs]; - - const inv = []; - - for (const tx of txs) { - assert(tx instanceof TX); - - // Don't send if they already have it. - if (this.invFilter.test(tx.hash())) - continue; - - // Check the peer's bloom - // filter if they're using spv. - if (this.spvFilter) { - if (!tx.isWatched(this.spvFilter)) - continue; - } - - // Check the fee filter. - if (this.feeRate !== -1) { - const hash = tx.hash('hex'); - const rate = this.options.getRate(hash); - if (rate !== -1 && rate < this.feeRate) - continue; - } - - inv.push(tx.toInv()); - } - - this.queueInv(inv); -}; - -/** - * Send inv to a peer. - * @param {InvItem[]} items - */ - -Peer.prototype.queueInv = function queueInv(items) { - if (!this.handshake) - return; - - if (this.destroyed) - return; - - if (!Array.isArray(items)) - items = [items]; - - let hasBlock = false; - - for (const item of items) { - if (item.type === invTypes.BLOCK) - hasBlock = true; - this.invQueue.push(item); - } - - if (this.invQueue.length >= 500 || hasBlock) - this.flushInv(); -}; - -/** - * Flush inv queue. - * @private - */ - -Peer.prototype.flushInv = function flushInv() { - if (this.destroyed) - return; - - const queue = this.invQueue; - - if (queue.length === 0) - return; - - this.invQueue = []; - - this.logger.spam('Serving %d inv items to %s.', - queue.length, this.hostname()); - - const items = []; - - for (const item of queue) { - if (!this.invFilter.added(item.hash, 'hex')) - continue; - - items.push(item); - } - - for (let i = 0; i < items.length; i += 1000) { - const chunk = items.slice(i, i + 1000); - this.send(new packets.InvPacket(chunk)); - } -}; - -/** - * Force send an inv (no filter check). - * @param {InvItem[]} items - */ - -Peer.prototype.sendInv = function sendInv(items) { - if (!this.handshake) - return; - - if (this.destroyed) - return; - - if (!Array.isArray(items)) - items = [items]; - - for (const item of items) - this.invFilter.add(item.hash, 'hex'); - - if (items.length === 0) - return; - - this.logger.spam('Serving %d inv items to %s.', - items.length, this.hostname()); - - for (let i = 0; i < items.length; i += 1000) { - const chunk = items.slice(i, i + 1000); - this.send(new packets.InvPacket(chunk)); - } -}; - -/** - * Send headers to a peer. - * @param {Headers[]} items - */ - -Peer.prototype.sendHeaders = function sendHeaders(items) { - if (!this.handshake) - return; - - if (this.destroyed) - return; - - if (!Array.isArray(items)) - items = [items]; - - for (const item of items) - this.invFilter.add(item.hash()); - - if (items.length === 0) - return; - - this.logger.spam('Serving %d headers to %s.', - items.length, this.hostname()); - - for (let i = 0; i < items.length; i += 2000) { - const chunk = items.slice(i, i + 2000); - this.send(new packets.HeadersPacket(chunk)); - } -}; - -/** - * Send a compact block. - * @private - * @param {Block} block - * @returns {Boolean} - */ - -Peer.prototype.sendCompactBlock = function sendCompactBlock(block) { - const witness = this.compactWitness; - const compact = BIP152.CompactBlock.fromBlock(block, witness); - this.send(new packets.CmpctBlockPacket(compact, witness)); -}; - -/** - * Send a `version` packet. - */ - -Peer.prototype.sendVersion = function sendVersion() { - const packet = new packets.VersionPacket(); - packet.version = this.options.version; - packet.services = this.options.services; - packet.time = this.network.now(); - packet.remote = this.address; - packet.local.setNull(); - packet.local.services = this.options.services; - packet.nonce = this.options.createNonce(this.hostname()); - packet.agent = this.options.agent; - packet.height = this.options.getHeight(); - packet.noRelay = this.options.noRelay; - 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. - */ - -Peer.prototype.sendPing = function sendPing() { - if (!this.handshake) - return; - - if (this.version <= common.PONG_VERSION) { - this.send(new packets.PingPacket()); - return; - } - - if (this.challenge) { - this.logger.debug('Peer has not responded to ping (%s).', this.hostname()); - return; - } - - this.lastPing = Date.now(); - this.challenge = common.nonce(); - - this.send(new packets.PingPacket(this.challenge)); -}; - -/** - * Send `filterload` to update the local bloom filter. - */ - -Peer.prototype.sendFilterLoad = function sendFilterLoad(filter) { - if (!this.handshake) - return; - - if (!this.options.spv) - return; - - if (!(this.services & services.BLOOM)) - return; - - this.send(new packets.FilterLoadPacket(filter)); -}; - -/** - * Set a fee rate filter for the peer. - * @param {Rate} rate - */ - -Peer.prototype.sendFeeRate = function sendFeeRate(rate) { - if (!this.handshake) - return; - - this.send(new packets.FeeFilterPacket(rate)); -}; - -/** - * Disconnect from and destroy the peer. - */ - -Peer.prototype.destroy = function destroy() { - const connected = this.connected; - - if (this.destroyed) - return; - - this.destroyed = true; - this.connected = false; - - this.socket.destroy(); - this.socket = null; - - if (this.bip151) - this.bip151.destroy(); - - if (this.bip150) - this.bip150.destroy(); - - if (this.pingTimer != null) { - clearInterval(this.pingTimer); - this.pingTimer = null; - } - - if (this.invTimer != null) { - clearInterval(this.invTimer); - this.invTimer = null; - } - - if (this.stallTimer != null) { - clearInterval(this.stallTimer); - this.stallTimer = null; - } - - if (this.connectTimeout != null) { - clearTimeout(this.connectTimeout); - this.connectTimeout = null; - } - - const jobs = this.drainQueue; - - this.drainSize = 0; - this.drainQueue = []; - - for (const job of jobs) - job.reject(new Error('Peer was destroyed.')); - - for (const [cmd, entry] of this.responseMap) { - this.responseMap.delete(cmd); - entry.reject(new Error('Peer was destroyed.')); - } - - this.locker.destroy(); - - this.emit('close', connected); -}; - -/** - * Write data to the peer's socket. - * @param {Buffer} data - */ - -Peer.prototype.write = function write(data) { - if (this.destroyed) - throw new Error('Peer is destroyed (write).'); - - this.lastSend = Date.now(); - - if (this.socket.write(data) === false) - this.needsDrain(data.length); -}; - -/** - * Send a packet. - * @param {Packet} packet - */ - -Peer.prototype.send = function send(packet) { - if (this.destroyed) - throw new Error('Peer is destroyed (send).'); - - // Used cached hashes as the - // packet checksum for speed. - let checksum = null; - if (packet.type === packetTypes.TX) { - const tx = packet.tx; - if (packet.witness) { - if (!tx.isCoinbase()) - checksum = tx.witnessHash(); - } else { - checksum = tx.hash(); - } - } - - this.sendRaw(packet.cmd, packet.toRaw(), checksum); - - this.addTimeout(packet); -}; - -/** - * Send a packet. - * @param {Packet} packet - */ - -Peer.prototype.sendRaw = function sendRaw(cmd, body, checksum) { - const payload = this.framePacket(cmd, body, checksum); - this.write(payload); -}; - -/** - * Wait for a drain event. - * @returns {Promise} - */ - -Peer.prototype.drain = function drain() { - if (this.destroyed) - return Promise.reject(new Error('Peer is destroyed.')); - - if (this.drainSize === 0) - return Promise.resolve(); - - return new Promise((resolve, reject) => { - this.drainQueue.push({ resolve, reject }); - }); -}; - -/** - * Handle drain event. - * @private - */ - -Peer.prototype.handleDrain = function handleDrain() { - const jobs = this.drainQueue; - - this.drainSize = 0; - - if (jobs.length === 0) - return; - - this.drainQueue = []; - - for (const job of jobs) - job.resolve(); -}; - -/** - * Add to drain counter. - * @private - * @param {Number} size - */ - -Peer.prototype.needsDrain = function needsDrain(size) { - this.drainSize += size; - - if (this.drainSize >= Peer.DRAIN_MAX) { - this.logger.warning( - 'Peer is not reading: %dmb buffered (%s).', - this.drainSize / (1 << 20), - this.hostname()); - this.error('Peer stalled (drain).'); - this.destroy(); - } -}; - -/** - * Potentially add response timeout. - * @private - * @param {Packet} packet - */ - -Peer.prototype.addTimeout = function addTimeout(packet) { - const timeout = Peer.RESPONSE_TIMEOUT; - - if (!this.outbound) - return; - - switch (packet.type) { - case packetTypes.MEMPOOL: - this.request(packetTypes.INV, timeout); - break; - case packetTypes.GETBLOCKS: - if (!this.options.isFull()) - this.request(packetTypes.INV, timeout); - break; - case packetTypes.GETHEADERS: - this.request(packetTypes.HEADERS, timeout * 2); - break; - case packetTypes.GETDATA: - this.request(packetTypes.DATA, timeout * 2); - break; - case packetTypes.GETBLOCKTXN: - this.request(packetTypes.BLOCKTXN, timeout); - break; - } -}; - -/** - * Potentially finish response timeout. - * @private - * @param {Packet} packet - */ - -Peer.prototype.fulfill = function fulfill(packet) { - switch (packet.type) { - case packetTypes.BLOCK: - case packetTypes.CMPCTBLOCK: - case packetTypes.MERKLEBLOCK: - case packetTypes.TX: - case packetTypes.NOTFOUND: { - const entry = this.response(packetTypes.DATA, packet); - assert(!entry || entry.jobs.length === 0); - break; - } - } - - return this.response(packet.type, packet); -}; - -/** - * Potentially timeout peer if it hasn't responded. - * @private - */ - -Peer.prototype.maybeTimeout = function maybeTimeout() { - const now = Date.now(); - - for (const [key, entry] of this.responseMap) { - if (now > entry.timeout) { - const name = packets.typesByVal[key]; - this.error('Peer is stalling (%s).', name.toLowerCase()); - this.destroy(); - return; - } - } - - if (this.merkleBlock) { - assert(this.merkleTime !== -1); - if (now > this.merkleTime + Peer.BLOCK_TIMEOUT) { - this.error('Peer is stalling (merkleblock).'); - this.destroy(); - return; - } - } - - if (this.syncing && this.loader && !this.options.isFull()) { - if (now > this.blockTime + Peer.BLOCK_TIMEOUT) { - this.error('Peer is stalling (block).'); - this.destroy(); - return; - } - } - - if (this.options.isFull() || !this.syncing) { - for (const time of this.blockMap.values()) { - if (now > time + Peer.BLOCK_TIMEOUT) { - this.error('Peer is stalling (block).'); - this.destroy(); - return; - } - } - - for (const time of this.txMap.values()) { - if (now > time + Peer.TX_TIMEOUT) { - this.error('Peer is stalling (tx).'); - this.destroy(); - return; - } - } - - for (const block of this.compactBlocks.values()) { - if (now > block.now + Peer.RESPONSE_TIMEOUT) { - this.error('Peer is stalling (blocktxn).'); - this.destroy(); - return; - } - } - } - - if (now > this.time + 60000) { - assert(this.time !== 0); - - if (this.lastRecv === 0 || this.lastSend === 0) { - this.error('Peer is stalling (no message).'); - this.destroy(); - return; - } - - if (now > this.lastSend + Peer.TIMEOUT_INTERVAL) { - this.error('Peer is stalling (send).'); - this.destroy(); - return; - } - - const mult = this.version <= common.PONG_VERSION ? 4 : 1; - - if (now > this.lastRecv + Peer.TIMEOUT_INTERVAL * mult) { - this.error('Peer is stalling (recv).'); - this.destroy(); - return; - } - - if (this.challenge && now > this.lastPing + Peer.TIMEOUT_INTERVAL) { - this.error('Peer is stalling (ping).'); - this.destroy(); - return; - } - } -}; - -/** - * Wait for a packet to be received from peer. - * @private - * @param {Number} type - Packet type. - * @param {Number} timeout - * @returns {RequestEntry} - */ - -Peer.prototype.request = function request(type, timeout) { - if (this.destroyed) - return null; - - let entry = this.responseMap.get(type); - - if (!entry) { - entry = new RequestEntry(); - this.responseMap.set(type, entry); - } - - entry.setTimeout(timeout); - - return entry; -}; - -/** - * Fulfill awaiting requests created with {@link Peer#request}. - * @private - * @param {Number} type - Packet type. - * @param {Object} payload - */ - -Peer.prototype.response = function response(type, payload) { - const entry = this.responseMap.get(type); - - if (!entry) - return null; - - this.responseMap.delete(type); - - return entry; -}; - -/** - * Wait for a packet to be received from peer. - * @private - * @param {Number} type - Packet type. - * @returns {Promise} - Returns Object(payload). - * Executed on timeout or once packet is received. - */ - -Peer.prototype.wait = function wait(type, timeout) { - return new Promise((resolve, reject) => { - if (this.destroyed) { - reject(new Error('Peer is destroyed (request).')); - return; - } - - const entry = this.request(type); - - entry.setTimeout(timeout); - entry.addJob(resolve, reject); - }); -}; - -/** - * Emit an error and destroy the peer. - * @private - * @param {...String|Error} err - */ - -Peer.prototype.error = function error(err) { - if (this.destroyed) - return; - - if (typeof err === 'string') { - const msg = format.apply(null, arguments); - err = new Error(msg); - } - - if (typeof err.code === 'string' && err.code[0] === 'E') { - const msg = err.code; - err = new Error(msg); - err.code = msg; - err.message = `Socket Error: ${msg}`; - } - - err.message += ` (${this.hostname()})`; - - this.emit('error', err); -}; - -/** - * Calculate peer block inv type (filtered, - * compact, witness, or non-witness). - * @returns {Number} - */ - -Peer.prototype.blockType = function blockType() { - if (this.options.spv) - return invTypes.FILTERED_BLOCK; - - if (this.options.compact - && this.hasCompactSupport() - && this.hasCompact()) { - return invTypes.CMPCT_BLOCK; - } - - if (this.hasWitness()) - return invTypes.WITNESS_BLOCK; - - return invTypes.BLOCK; -}; - -/** - * Calculate peer tx inv type (witness or non-witness). - * @returns {Number} - */ - -Peer.prototype.txType = function txType() { - if (this.hasWitness()) - return invTypes.WITNESS_TX; - - return invTypes.TX; -}; - -/** - * Send `getdata` to peer. - * @param {InvItem[]} items - */ - -Peer.prototype.getData = function getData(items) { - this.send(new packets.GetDataPacket(items)); -}; - -/** - * Send batched `getdata` to peer. - * @param {InvType} type - * @param {Hash[]} hashes - */ - -Peer.prototype.getItems = function getItems(type, hashes) { - const items = []; - - for (const hash of hashes) - items.push(new InvItem(type, hash)); - - if (items.length === 0) - return; - - this.getData(items); -}; - -/** - * Send batched `getdata` to peer (blocks). - * @param {Hash[]} hashes - */ - -Peer.prototype.getBlock = function getBlock(hashes) { - this.getItems(this.blockType(), hashes); -}; - -/** - * Send batched `getdata` to peer (txs). - * @param {Hash[]} hashes - */ - -Peer.prototype.getTX = function getTX(hashes) { - this.getItems(this.txType(), hashes); -}; - -/** - * Send `getdata` to peer for a single block. - * @param {Hash} hash - */ - -Peer.prototype.getFullBlock = function getFullBlock(hash) { - assert(!this.options.spv); - - let type = invTypes.BLOCK; - - if (this.hasWitness()) - type |= InvItem.WITNESS_FLAG; - - this.getItems(type, [hash]); -}; - -/** - * Handle a packet payload. - * @method - * @private - * @param {Packet} packet - */ - -Peer.prototype.readPacket = async function readPacket(packet) { - if (this.destroyed) - return; - - // The "pre-handshake" packets get - // to bypass the lock, since they - // are meant to change the way input - // is handled at a low level. They - // must be handled immediately. - switch (packet.type) { - case packetTypes.ENCINIT: - case packetTypes.ENCACK: - case packetTypes.AUTHCHALLENGE: - case packetTypes.AUTHREPLY: - case packetTypes.AUTHPROPOSE: - case packetTypes.PONG: { - try { - this.socket.pause(); - await this.handlePacket(packet); - } finally { - if (!this.destroyed) - this.socket.resume(); - } - break; - } - default: { - const unlock = await this.locker.lock(); - try { - this.socket.pause(); - await this.handlePacket(packet); - } finally { - if (!this.destroyed) - this.socket.resume(); - unlock(); - } - break; - } - } -}; - -/** - * Handle a packet payload without a lock. - * @method - * @private - * @param {Packet} packet - */ - -Peer.prototype.handlePacket = async function handlePacket(packet) { - if (this.destroyed) - throw new Error('Destroyed peer sent a packet.'); - - if (this.bip151 - && this.bip151.job - && !this.bip151.completed - && packet.type !== packetTypes.ENCINIT - && packet.type !== packetTypes.ENCACK) { - this.bip151.reject(new Error('Message before BIP151 handshake.')); - } - - if (this.bip150 - && this.bip150.job - && !this.bip150.completed - && packet.type !== packetTypes.AUTHCHALLENGE - && packet.type !== packetTypes.AUTHREPLY - && packet.type !== packetTypes.AUTHPROPOSE) { - this.bip150.reject(new Error('Message before BIP150 auth.')); - } - - const entry = this.fulfill(packet); - - switch (packet.type) { - case packetTypes.VERSION: - await this.handleVersion(packet); - break; - case packetTypes.VERACK: - await this.handleVerack(packet); - break; - case packetTypes.PING: - await this.handlePing(packet); - break; - case packetTypes.PONG: - await this.handlePong(packet); - break; - case packetTypes.SENDHEADERS: - await this.handleSendHeaders(packet); - break; - case packetTypes.FILTERLOAD: - await this.handleFilterLoad(packet); - break; - case packetTypes.FILTERADD: - await this.handleFilterAdd(packet); - break; - case packetTypes.FILTERCLEAR: - await this.handleFilterClear(packet); - break; - case packetTypes.FEEFILTER: - await this.handleFeeFilter(packet); - break; - case packetTypes.SENDCMPCT: - await this.handleSendCmpct(packet); - break; - case packetTypes.ENCINIT: - await this.handleEncinit(packet); - break; - case packetTypes.ENCACK: - await this.handleEncack(packet); - break; - case packetTypes.AUTHCHALLENGE: - await this.handleAuthChallenge(packet); - break; - case packetTypes.AUTHREPLY: - await this.handleAuthReply(packet); - break; - case packetTypes.AUTHPROPOSE: - await this.handleAuthPropose(packet); - break; - } - - if (this.onPacket) - await this.onPacket(packet); - - this.emit('packet', packet); - - if (entry) - entry.resolve(packet); -}; - -/** - * Handle `version` packet. - * @method - * @private - * @param {VersionPacket} packet - */ - -Peer.prototype.handleVersion = async function handleVersion(packet) { - if (this.version !== -1) - throw new Error('Peer sent a duplicate version.'); - - this.version = packet.version; - this.services = packet.services; - this.height = packet.height; - this.agent = packet.agent; - this.noRelay = packet.noRelay; - this.local = packet.remote; - - if (!this.network.selfConnect) { - if (this.options.hasNonce(packet.nonce)) - throw new Error('We connected to ourself. Oops.'); - } - - if (this.version < common.MIN_VERSION) - throw new Error('Peer does not support required protocol version.'); - - if (this.outbound) { - if (!(this.services & services.NETWORK)) - throw new Error('Peer does not support network services.'); - - if (this.options.headers) { - if (this.version < common.HEADERS_VERSION) - throw new Error('Peer does not support getheaders.'); - } - - if (this.options.spv) { - if (!(this.services & services.BLOOM)) - throw new Error('Peer does not support BIP37.'); - - if (this.version < common.BLOOM_VERSION) - throw new Error('Peer does not support BIP37.'); - } - - if (this.options.hasWitness()) { - if (!(this.services & services.WITNESS)) - throw new Error('Peer does not support segregated witness.'); - } - - if (this.options.compact) { - if (!this.hasCompactSupport()) { - this.logger.debug( - 'Peer does not support compact blocks (%s).', - this.hostname()); - } - } - } - - this.send(new packets.VerackPacket()); -}; - -/** - * Handle `verack` packet. - * @method - * @private - * @param {VerackPacket} packet - */ - -Peer.prototype.handleVerack = async function handleVerack(packet) { - if (this.ack) { - this.logger.debug('Peer sent duplicate ack (%s).', this.hostname()); - return; - } - - this.ack = true; - this.logger.debug('Received verack (%s).', this.hostname()); -}; - -/** - * Handle `ping` packet. - * @method - * @private - * @param {PingPacket} packet - */ - -Peer.prototype.handlePing = async function handlePing(packet) { - if (!packet.nonce) - return; - - this.send(new packets.PongPacket(packet.nonce)); -}; - -/** - * Handle `pong` packet. - * @method - * @private - * @param {PongPacket} packet - */ - -Peer.prototype.handlePong = async function handlePong(packet) { - const nonce = packet.nonce; - const now = Date.now(); - - if (!this.challenge) { - this.logger.debug('Peer sent an unsolicited pong (%s).', this.hostname()); - return; - } - - if (!nonce.equals(this.challenge)) { - if (nonce.equals(encoding.ZERO_U64)) { - this.logger.debug('Peer sent a zero nonce (%s).', this.hostname()); - this.challenge = null; - return; - } - this.logger.debug('Peer sent the wrong nonce (%s).', this.hostname()); - return; - } - - if (now >= this.lastPing) { - this.lastPong = now; - if (this.minPing === -1) - this.minPing = now - this.lastPing; - this.minPing = Math.min(this.minPing, now - this.lastPing); - } else { - this.logger.debug('Timing mismatch (what?) (%s).', this.hostname()); - } - - this.challenge = null; -}; - -/** - * Handle `sendheaders` packet. - * @method - * @private - * @param {SendHeadersPacket} packet - */ - -Peer.prototype.handleSendHeaders = async function handleSendHeaders(packet) { - if (this.preferHeaders) { - this.logger.debug( - 'Peer sent a duplicate sendheaders (%s).', - this.hostname()); - return; - } - - this.preferHeaders = true; -}; - -/** - * Handle `filterload` packet. - * @method - * @private - * @param {FilterLoadPacket} packet - */ - -Peer.prototype.handleFilterLoad = async function handleFilterLoad(packet) { - if (!packet.isWithinConstraints()) { - this.increaseBan(100); - return; - } - - this.spvFilter = packet.filter; - this.noRelay = false; -}; - -/** - * Handle `filteradd` packet. - * @method - * @private - * @param {FilterAddPacket} packet - */ - -Peer.prototype.handleFilterAdd = async function handleFilterAdd(packet) { - const data = packet.data; - - if (data.length > consensus.MAX_SCRIPT_PUSH) { - this.increaseBan(100); - return; - } - - if (this.spvFilter) - this.spvFilter.add(data); - - this.noRelay = false; -}; - -/** - * Handle `filterclear` packet. - * @method - * @private - * @param {FilterClearPacket} packet - */ - -Peer.prototype.handleFilterClear = async function handleFilterClear(packet) { - if (this.spvFilter) - this.spvFilter.reset(); - - this.noRelay = false; -}; - -/** - * Handle `feefilter` packet. - * @method - * @private - * @param {FeeFilterPacket} packet - */ - -Peer.prototype.handleFeeFilter = async function handleFeeFilter(packet) { - const rate = packet.rate; - - if (rate < 0 || rate > consensus.MAX_MONEY) { - this.increaseBan(100); - return; - } - - this.feeRate = rate; -}; - -/** - * Handle `sendcmpct` packet. - * @method - * @private - * @param {SendCmpctPacket} - */ - -Peer.prototype.handleSendCmpct = async function handleSendCmpct(packet) { - if (this.compactMode !== -1) { - this.logger.debug( - 'Peer sent a duplicate sendcmpct (%s).', - this.hostname()); - return; - } - - if (packet.version > 2) { - // Ignore - this.logger.info( - 'Peer request compact blocks version %d (%s).', - packet.version, this.hostname()); - return; - } - - if (packet.mode > 1) { - this.logger.info( - 'Peer request compact blocks mode %d (%s).', - packet.mode, this.hostname()); - return; - } - - this.logger.info( - 'Peer initialized compact blocks (mode=%d, version=%d) (%s).', - packet.mode, packet.version, this.hostname()); - - this.compactMode = packet.mode; - this.compactWitness = packet.version === 2; -}; - -/** - * Handle `encinit` packet. - * @method - * @private - * @param {EncinitPacket} packet - */ - -Peer.prototype.handleEncinit = async function handleEncinit(packet) { - if (!this.bip151) - return; - - this.bip151.encinit(packet.publicKey, packet.cipher); - - this.send(this.bip151.toEncack()); -}; - -/** - * Handle `encack` packet. - * @method - * @private - * @param {EncackPacket} packet - */ - -Peer.prototype.handleEncack = async function handleEncack(packet) { - if (!this.bip151) - return; - - this.bip151.encack(packet.publicKey); -}; - -/** - * Handle `authchallenge` packet. - * @method - * @private - * @param {AuthChallengePacket} packet - */ - -Peer.prototype.handleAuthChallenge = async function handleAuthChallenge(packet) { - if (!this.bip150) - return; - - const sig = this.bip150.challenge(packet.hash); - - this.send(new packets.AuthReplyPacket(sig)); -}; - -/** - * Handle `authreply` packet. - * @method - * @private - * @param {AuthReplyPacket} packet - */ - -Peer.prototype.handleAuthReply = async function handleAuthReply(packet) { - if (!this.bip150) - return; - - const hash = this.bip150.reply(packet.signature); - - if (hash) - this.send(new packets.AuthProposePacket(hash)); -}; - -/** - * Handle `authpropose` packet. - * @method - * @private - * @param {AuthProposePacket} packet - */ - -Peer.prototype.handleAuthPropose = async function handleAuthPropose(packet) { - if (!this.bip150) - return; - - const hash = this.bip150.propose(packet.hash); - - this.send(new packets.AuthChallengePacket(hash)); -}; - -/** - * Send `getheaders` to peer. Note that unlike - * `getblocks`, `getheaders` can have a null locator. - * @param {Hash[]?} locator - Chain locator. - * @param {Hash?} stop - Hash to stop at. - */ - -Peer.prototype.sendGetHeaders = function sendGetHeaders(locator, stop) { - const packet = new packets.GetHeadersPacket(locator, stop); - - let hash = null; - if (packet.locator.length > 0) - hash = encoding.revHex(packet.locator[0]); - - let end = null; - if (stop) - end = encoding.revHex(stop); - - this.logger.debug( - 'Requesting headers packet from peer with getheaders (%s).', - this.hostname()); - - this.logger.debug( - 'Sending getheaders (hash=%s, stop=%s).', - hash, end); - - this.send(packet); -}; - -/** - * Send `getblocks` to peer. - * @param {Hash[]} locator - Chain locator. - * @param {Hash?} stop - Hash to stop at. - */ - -Peer.prototype.sendGetBlocks = function sendGetBlocks(locator, stop) { - const packet = new packets.GetBlocksPacket(locator, stop); - - let hash = null; - if (packet.locator.length > 0) - hash = encoding.revHex(packet.locator[0]); - - let end = null; - if (stop) - end = encoding.revHex(stop); - - this.logger.debug( - 'Requesting inv packet from peer with getblocks (%s).', - this.hostname()); - - this.logger.debug( - 'Sending getblocks (hash=%s, stop=%s).', - hash, end); - - this.send(packet); -}; - -/** - * Send `mempool` to peer. - */ - -Peer.prototype.sendMempool = function sendMempool() { - if (!this.handshake) - return; - - if (!(this.services & services.BLOOM)) { - this.logger.debug( - 'Cannot request mempool for non-bloom peer (%s).', - this.hostname()); - return; - } - - this.logger.debug( - 'Requesting inv packet from peer with mempool (%s).', - this.hostname()); - - this.send(new packets.MempoolPacket()); -}; - -/** - * Send `reject` to peer. - * @param {Number} code - * @param {String} reason - * @param {String} msg - * @param {Hash} hash - */ - -Peer.prototype.sendReject = function sendReject(code, reason, msg, hash) { - const reject = packets.RejectPacket.fromReason(code, reason, msg, hash); - - if (msg) { - this.logger.debug('Rejecting %s %s (%s): code=%s reason=%s.', - msg, encoding.revHex(hash), this.hostname(), code, reason); - } else { - this.logger.debug('Rejecting packet from %s: code=%s reason=%s.', - this.hostname(), code, reason); - } - - this.logger.debug( - 'Sending reject packet to peer (%s).', - this.hostname()); - - this.send(reject); -}; - -/** - * Send a `sendcmpct` packet. - * @param {Number} mode - */ - -Peer.prototype.sendCompact = function sendCompact(mode) { - if (this.services & common.services.WITNESS) { - if (this.version >= common.COMPACT_WITNESS_VERSION) { - this.logger.info( - 'Initializing witness compact blocks (%s).', - this.hostname()); - this.send(new packets.SendCmpctPacket(mode, 2)); - return; - } - } - - if (this.version >= common.COMPACT_VERSION) { - this.logger.info( - 'Initializing normal compact blocks (%s).', - this.hostname()); - - this.send(new packets.SendCmpctPacket(mode, 1)); - } -}; - -/** - * Increase banscore on peer. - * @param {Number} score - * @returns {Boolean} - */ - -Peer.prototype.increaseBan = function increaseBan(score) { - this.banScore += score; - - if (this.banScore >= this.options.banScore) { - this.logger.debug('Ban threshold exceeded (%s).', this.hostname()); - this.ban(); - return true; - } - - return false; -}; - -/** - * Ban peer. - */ - -Peer.prototype.ban = function ban() { - this.emit('ban'); -}; - -/** - * Send a `reject` packet to peer. - * @param {String} msg - * @param {VerifyError} err - * @returns {Boolean} - */ - -Peer.prototype.reject = function reject(msg, err) { - this.sendReject(err.code, err.reason, msg, err.hash); - return this.increaseBan(err.score); -}; - -/** - * Test whether required services are available. - * @param {Number} services - * @returns {Boolean} - */ - -Peer.prototype.hasServices = function hasServices(services) { - return (this.services & services) === services; -}; - -/** - * Test whether the WITNESS service bit is set. - * @returns {Boolean} - */ - -Peer.prototype.hasWitness = function hasWitness() { - return (this.services & services.WITNESS) !== 0; -}; - -/** - * Test whether the peer supports compact blocks. - * @returns {Boolean} - */ - -Peer.prototype.hasCompactSupport = function hasCompactSupport() { - if (this.version < common.COMPACT_VERSION) - return false; - - if (!this.options.hasWitness()) - return true; - - if (!(this.services & services.WITNESS)) - return false; - - return this.version >= common.COMPACT_WITNESS_VERSION; -}; - -/** - * Test whether the peer sent us a - * compatible compact block handshake. - * @returns {Boolean} - */ - -Peer.prototype.hasCompact = function hasCompact() { - if (this.compactMode === -1) - return false; - - if (!this.options.hasWitness()) - return true; - - if (!this.compactWitness) - return false; - - return true; -}; - -/** - * Inspect the peer. - * @returns {String} - */ - -Peer.prototype.inspect = function inspect() { - return ''; -}; - -/** - * PeerOptions + * Peer Options * @alias module:net.PeerOptions - * @constructor */ -function PeerOptions(options) { - if (!(this instanceof PeerOptions)) - return new PeerOptions(options); +class PeerOptions { + /** + * Create peer options. + * @constructor + */ - this.network = Network.primary; - this.logger = Logger.global; + constructor(options) { + this.network = Network.primary; + this.logger = Logger.global; - this.createSocket = tcp.createSocket; - this.version = common.PROTOCOL_VERSION; - this.services = common.LOCAL_SERVICES; - this.agent = common.USER_AGENT; - this.noRelay = false; - this.spv = false; - this.compact = false; - this.headers = false; - this.banScore = common.BAN_SCORE; + this.createSocket = tcp.createSocket; + this.version = common.PROTOCOL_VERSION; + this.services = common.LOCAL_SERVICES; + this.agent = common.USER_AGENT; + this.noRelay = false; + this.spv = false; + this.compact = false; + this.headers = false; + this.banScore = common.BAN_SCORE; - this.getHeight = PeerOptions.getHeight; - this.isFull = PeerOptions.isFull; - this.hasWitness = PeerOptions.hasWitness; - this.createNonce = PeerOptions.createNonce; - this.hasNonce = PeerOptions.hasNonce; - this.getRate = PeerOptions.getRate; + this.getHeight = PeerOptions.getHeight; + this.isFull = PeerOptions.isFull; + this.hasWitness = PeerOptions.hasWitness; + this.createNonce = PeerOptions.createNonce; + this.hasNonce = PeerOptions.hasNonce; + this.getRate = PeerOptions.getRate; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {PeerOptions} + */ + + fromOptions(options) { + assert(options, 'Options are required.'); + + if (options.network != null) + this.network = Network.get(options.network); + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.createSocket != null) { + assert(typeof options.createSocket === 'function'); + this.createSocket = options.createSocket; + } + + if (options.version != null) { + assert(typeof options.version === 'number'); + this.version = options.version; + } + + if (options.services != null) { + assert(typeof options.services === 'number'); + this.services = options.services; + } + + if (options.agent != null) { + assert(typeof options.agent === 'string'); + this.agent = options.agent; + } + + if (options.noRelay != null) { + assert(typeof options.noRelay === 'boolean'); + this.noRelay = options.noRelay; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + this.spv = options.spv; + } + + if (options.compact != null) { + assert(typeof options.compact === 'boolean'); + this.compact = options.compact; + } + + if (options.headers != null) { + assert(typeof options.headers === 'boolean'); + this.headers = options.headers; + } + + if (options.banScore != null) { + assert(typeof options.banScore === 'number'); + this.banScore = options.banScore; + } + + if (options.getHeight != null) { + assert(typeof options.getHeight === 'function'); + this.getHeight = options.getHeight; + } + + if (options.isFull != null) { + assert(typeof options.isFull === 'function'); + this.isFull = options.isFull; + } + + if (options.hasWitness != null) { + assert(typeof options.hasWitness === 'function'); + this.hasWitness = options.hasWitness; + } + + if (options.createNonce != null) { + assert(typeof options.createNonce === 'function'); + this.createNonce = options.createNonce; + } + + if (options.hasNonce != null) { + assert(typeof options.hasNonce === 'function'); + this.hasNonce = options.hasNonce; + } + + if (options.getRate != null) { + assert(typeof options.getRate === 'function'); + this.getRate = options.getRate; + } + + return this; + } + + /** + * Instantiate options from object. + * @param {Object} options + * @returns {PeerOptions} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Get the chain height. + * @private + * @returns {Number} + */ + + static getHeight() { + return 0; + } + + /** + * Test whether the chain is synced. + * @private + * @returns {Boolean} + */ + + static isFull() { + return false; + } + + /** + * Whether segwit is enabled. + * @private + * @returns {Boolean} + */ + + static hasWitness() { + return true; + } + + /** + * Create a version packet nonce. + * @private + * @param {String} hostname + * @returns {Buffer} + */ + + static createNonce(hostname) { + return common.nonce(); + } + + /** + * Test whether version nonce is ours. + * @private + * @param {Buffer} nonce + * @returns {Boolean} + */ + + static hasNonce(nonce) { + return false; + } + + /** + * Get fee rate for txid. + * @private + * @param {Hash} hash + * @returns {Rate} + */ + + static getRate(hash) { + return -1; + } } /** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {PeerOptions} - */ - -PeerOptions.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Options are required.'); - - if (options.network != null) - this.network = Network.get(options.network); - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.createSocket != null) { - assert(typeof options.createSocket === 'function'); - this.createSocket = options.createSocket; - } - - if (options.version != null) { - assert(typeof options.version === 'number'); - this.version = options.version; - } - - if (options.services != null) { - assert(typeof options.services === 'number'); - this.services = options.services; - } - - if (options.agent != null) { - assert(typeof options.agent === 'string'); - this.agent = options.agent; - } - - if (options.noRelay != null) { - assert(typeof options.noRelay === 'boolean'); - this.noRelay = options.noRelay; - } - - if (options.spv != null) { - assert(typeof options.spv === 'boolean'); - this.spv = options.spv; - } - - if (options.compact != null) { - assert(typeof options.compact === 'boolean'); - this.compact = options.compact; - } - - if (options.headers != null) { - assert(typeof options.headers === 'boolean'); - this.headers = options.headers; - } - - if (options.banScore != null) { - assert(typeof options.banScore === 'number'); - this.banScore = options.banScore; - } - - if (options.getHeight != null) { - assert(typeof options.getHeight === 'function'); - this.getHeight = options.getHeight; - } - - if (options.isFull != null) { - assert(typeof options.isFull === 'function'); - this.isFull = options.isFull; - } - - if (options.hasWitness != null) { - assert(typeof options.hasWitness === 'function'); - this.hasWitness = options.hasWitness; - } - - if (options.createNonce != null) { - assert(typeof options.createNonce === 'function'); - this.createNonce = options.createNonce; - } - - if (options.hasNonce != null) { - assert(typeof options.hasNonce === 'function'); - this.hasNonce = options.hasNonce; - } - - if (options.getRate != null) { - assert(typeof options.getRate === 'function'); - this.getRate = options.getRate; - } - - return this; -}; - -/** - * Instantiate options from object. - * @param {Object} options - * @returns {PeerOptions} - */ - -PeerOptions.fromOptions = function fromOptions(options) { - return new PeerOptions().fromOptions(options); -}; - -/** - * Get the chain height. - * @private - * @returns {Number} - */ - -PeerOptions.getHeight = function getHeight() { - return 0; -}; - -/** - * Test whether the chain is synced. - * @private - * @returns {Boolean} - */ - -PeerOptions.isFull = function isFull() { - return false; -}; - -/** - * Whether segwit is enabled. - * @private - * @returns {Boolean} - */ - -PeerOptions.hasWitness = function hasWitness() { - return true; -}; - -/** - * Create a version packet nonce. - * @private - * @param {String} hostname - * @returns {Buffer} - */ - -PeerOptions.createNonce = function createNonce(hostname) { - return common.nonce(); -}; - -/** - * Test whether version nonce is ours. - * @private - * @param {Buffer} nonce - * @returns {Boolean} - */ - -PeerOptions.hasNonce = function hasNonce(nonce) { - return false; -}; - -/** - * Get fee rate for txid. - * @private - * @param {Hash} hash - * @returns {Rate} - */ - -PeerOptions.getRate = function getRate(hash) { - return -1; -}; - -/** - * RequestEntry - * @constructor + * Request Entry * @ignore */ -function RequestEntry() { - this.timeout = 0; - this.jobs = []; +class RequestEntry { + /** + * Create a request entry. + * @constructor + */ + + constructor() { + this.timeout = 0; + this.jobs = []; + } + + addJob(resolve, reject) { + this.jobs.push({ resolve, reject }); + } + + setTimeout(timeout) { + this.timeout = Date.now() + timeout; + } + + reject(err) { + for (const job of this.jobs) + job.reject(err); + + this.jobs.length = 0; + } + + resolve(result) { + for (const job of this.jobs) + job.resolve(result); + + this.jobs.length = 0; + } } -RequestEntry.prototype.addJob = function addJob(resolve, reject) { - this.jobs.push({ resolve, reject }); -}; - -RequestEntry.prototype.setTimeout = function setTimeout(timeout) { - this.timeout = Date.now() + timeout; -}; - -RequestEntry.prototype.reject = function reject(err) { - for (const job of this.jobs) - job.reject(err); - - this.jobs.length = 0; -}; - -RequestEntry.prototype.resolve = function resolve(result) { - for (const job of this.jobs) - job.resolve(result); - - this.jobs.length = 0; -}; - /* * Expose */ diff --git a/lib/net/pool.js b/lib/net/pool.js index 2ab9385e..fc72043a 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -38,89 +38,3531 @@ const packetTypes = packets.types; const scores = HostList.scores; /** + * Pool * A pool of peers for handling all network activity. * @alias module:net.Pool - * @constructor - * @param {Object} options - * @param {Chain} options.chain - * @param {Mempool?} options.mempool - * @param {Number?} [options.maxOutbound=8] - Maximum number of peers. - * @param {Boolean?} options.spv - Do an SPV sync. - * @param {Boolean?} options.noRelay - Whether to ask - * for relayed transactions. - * @param {Number?} [options.feeRate] - Fee filter rate. - * @param {Number?} [options.invTimeout=60000] - Timeout for broadcasted - * objects. - * @param {Boolean?} options.listen - Whether to spin up a server socket - * and listen for peers. - * @param {Boolean?} options.selfish - A selfish pool. Will not serve blocks, - * headers, hashes, utxos, or transactions to peers. - * @param {Boolean?} options.broadcast - Whether to automatically broadcast - * transactions accepted to our mempool. - * @param {String[]} options.seeds - * @param {Function?} options.createSocket - Custom function to create a socket. - * Must accept (port, host) and return a node-like socket. - * @param {Function?} options.createServer - Custom function to create a server. - * Must return a node-like server. - * @emits Pool#block - * @emits Pool#tx - * @emits Pool#peer - * @emits Pool#open - * @emits Pool#close - * @emits Pool#error - * @emits Pool#reject + * @extends EventEmitter */ -function Pool(options) { - if (!(this instanceof Pool)) - return new Pool(options); +class Pool extends EventEmitter { + /** + * Create a pool. + * @constructor + * @param {Object} options + */ - EventEmitter.call(this); + constructor(options) { + super(); - this.opened = false; - this.options = new PoolOptions(options); + this.opened = false; + this.options = new PoolOptions(options); - this.network = this.options.network; - this.logger = this.options.logger.context('net'); - this.chain = this.options.chain; - this.mempool = this.options.mempool; - this.server = this.options.createServer(); - this.nonces = this.options.nonces; + this.network = this.options.network; + this.logger = this.options.logger.context('net'); + this.chain = this.options.chain; + this.mempool = this.options.mempool; + this.server = this.options.createServer(); + this.nonces = this.options.nonces; - this.locker = new Lock(true); - this.connected = false; - this.disconnecting = false; - this.syncing = false; - this.discovering = false; - this.spvFilter = null; - this.txFilter = null; - this.blockMap = new Set(); - this.txMap = new Set(); - this.compactBlocks = new Set(); - this.invMap = new Map(); - this.pendingFilter = null; - this.pendingRefill = null; + this.locker = new Lock(true); + this.connected = false; + this.disconnecting = false; + this.syncing = false; + this.discovering = false; + this.spvFilter = null; + this.txFilter = null; + this.blockMap = new Set(); + this.txMap = new Set(); + this.compactBlocks = new Set(); + this.invMap = new Map(); + this.pendingFilter = null; + this.pendingRefill = null; - this.checkpoints = false; - this.headerChain = new List(); - this.headerNext = null; - this.headerTip = null; + this.checkpoints = false; + this.headerChain = new List(); + this.headerNext = null; + this.headerTip = null; - this.peers = new PeerList(); - this.authdb = new BIP150.AuthDB(this.options); - this.hosts = new HostList(this.options); - this.id = 0; + this.peers = new PeerList(); + this.authdb = new BIP150.AuthDB(this.options); + this.hosts = new HostList(this.options); + this.id = 0; - if (this.options.spv) - this.spvFilter = BloomFilter.fromRate(20000, 0.001, BloomFilter.flags.ALL); + if (this.options.spv) + this.spvFilter = BloomFilter.fromRate(20000, 0.001, BloomFilter.flags.ALL); - if (!this.options.mempool) - this.txFilter = new RollingFilter(50000, 0.000001); + if (!this.options.mempool) + this.txFilter = new RollingFilter(50000, 0.000001); - this.init(); -}; + this.init(); + } -Object.setPrototypeOf(Pool.prototype, EventEmitter.prototype); + /** + * Initialize the pool. + * @private + */ + + init() { + this.server.on('error', (err) => { + this.emit('error', err); + }); + + this.server.on('connection', (socket) => { + this.handleSocket(socket); + this.emit('connection', socket); + }); + + this.server.on('listening', () => { + const data = this.server.address(); + this.logger.info( + 'Pool server listening on %s (port=%d).', + data.address, data.port); + this.emit('listening', data); + }); + + this.chain.on('block', (block, entry) => { + this.emit('block', block, entry); + }); + + this.chain.on('reset', () => { + if (this.checkpoints) + this.resetChain(); + this.forceSync(); + }); + + this.chain.on('full', () => { + this.sync(); + this.emit('full'); + this.logger.info('Chain is fully synced (height=%d).', this.chain.height); + }); + + this.chain.on('bad orphan', (err, id) => { + this.handleBadOrphan('block', err, id); + }); + + if (this.mempool) { + this.mempool.on('tx', (tx) => { + this.emit('tx', tx); + }); + + this.mempool.on('bad orphan', (err, id) => { + this.handleBadOrphan('tx', err, id); + }); + } + + if (!this.options.selfish && !this.options.spv) { + if (this.mempool) { + this.mempool.on('tx', (tx) => { + this.announceTX(tx); + }); + } + + // Normally we would also broadcast + // competing chains, but we want to + // avoid getting banned if an evil + // miner sends us an invalid competing + // chain that we can't connect and + // verify yet. + this.chain.on('block', (block) => { + if (!this.chain.synced) + return; + this.announceBlock(block); + }); + } + } + + /** + * Open the pool, wait for the chain to load. + * @returns {Promise} + */ + + async open() { + assert(!this.opened, 'Pool is already open.'); + this.opened = true; + + this.logger.info('Pool loaded (maxpeers=%d).', this.options.maxOutbound); + + if (this.options.bip150) { + const key = secp256k1.publicKeyCreate(this.options.identityKey, true); + this.logger.info('Identity public key: %s.', key.toString('hex')); + this.logger.info('Identity address: %s.', BIP150.address(key)); + } + + this.resetChain(); + } + + /** + * Close and destroy the pool. + * @method + * @alias Pool#close + * @returns {Promise} + */ + + async close() { + assert(this.opened, 'Pool is not open.'); + this.opened = false; + return this.disconnect(); + } + + /** + * Reset header chain. + */ + + resetChain() { + if (!this.options.checkpoints) + return; + + this.checkpoints = false; + this.headerTip = null; + this.headerChain.reset(); + this.headerNext = null; + + const tip = this.chain.tip; + + if (tip.height < this.network.lastCheckpoint) { + this.checkpoints = true; + this.headerTip = this.getNextTip(tip.height); + this.headerChain.push(new HeaderEntry(tip.hash, tip.height)); + this.logger.info( + 'Initialized header chain to height %d (checkpoint=%s).', + tip.height, encoding.revHex(this.headerTip.hash)); + } + } + + /** + * Connect to the network. + * @method + * @returns {Promise} + */ + + async connect() { + const unlock = await this.locker.lock(); + try { + return await this._connect(); + } finally { + unlock(); + } + } + + /** + * Connect to the network (no lock). + * @method + * @returns {Promise} + */ + + async _connect() { + assert(this.opened, 'Pool is not opened.'); + + if (this.connected) + return; + + await this.hosts.open(); + await this.authdb.open(); + + await this.discoverGateway(); + await this.discoverExternal(); + await this.discoverSeeds(); + + this.fillOutbound(); + + await this.listen(); + + this.startTimer(); + + this.connected = true; + } + + /** + * Disconnect from the network. + * @method + * @returns {Promise} + */ + + async disconnect() { + const unlock = await this.locker.lock(); + try { + return await this._disconnect(); + } finally { + unlock(); + } + } + + /** + * Disconnect from the network. + * @method + * @returns {Promise} + */ + + async _disconnect() { + if (!this.connected) + return; + + this.disconnecting = true; + + for (const item of this.invMap.values()) + item.resolve(); + + this.peers.destroy(); + + this.blockMap.clear(); + this.txMap.clear(); + + if (this.pendingFilter != null) { + clearTimeout(this.pendingFilter); + this.pendingFilter = null; + } + + if (this.pendingRefill != null) { + clearTimeout(this.pendingRefill); + this.pendingRefill = null; + } + + this.checkpoints = false; + this.headerTip = null; + this.headerChain.reset(); + this.headerNext = null; + + this.stopTimer(); + + await this.authdb.close(); + await this.hosts.close(); + + await this.unlisten(); + + this.disconnecting = false; + this.syncing = false; + this.connected = false; + } + + /** + * Start listening on a server socket. + * @method + * @private + * @returns {Promise} + */ + + async listen() { + assert(this.server); + assert(!this.connected, 'Already listening.'); + + if (!this.options.listen) + return; + + this.server.maxConnections = this.options.maxInbound; + + await this.server.listen(this.options.port, this.options.host); + } + + /** + * Stop listening on server socket. + * @method + * @private + * @returns {Promise} + */ + + async unlisten() { + assert(this.server); + assert(this.connected, 'Not listening.'); + + if (!this.options.listen) + return; + + await this.server.close(); + } + + /** + * Start discovery timer. + * @private + */ + + startTimer() { + assert(this.timer == null, 'Timer already started.'); + this.timer = setInterval(() => this.discover(), Pool.DISCOVERY_INTERVAL); + } + + /** + * Stop discovery timer. + * @private + */ + + stopTimer() { + assert(this.timer != null, 'Timer already stopped.'); + clearInterval(this.timer); + this.timer = null; + } + + /** + * Rediscover seeds and internet gateway. + * Attempt to add port mapping once again. + * @returns {Promise} + */ + + async discover() { + if (this.discovering) + return; + + try { + this.discovering = true; + await this.discoverGateway(); + await this.discoverSeeds(true); + } finally { + this.discovering = false; + } + } + + /** + * Attempt to add port mapping (i.e. + * remote:8333->local:8333) via UPNP. + * @returns {Promise} + */ + + async discoverGateway() { + const src = this.options.publicPort; + const dest = this.options.port; + + // Pointless if we're not listening. + if (!this.options.listen) + return false; + + // UPNP is always optional, since + // it's likely to not work anyway. + if (!this.options.upnp) + return false; + + let wan; + try { + this.logger.debug('Discovering internet gateway (upnp).'); + wan = await UPNP.discover(); + } catch (e) { + this.logger.debug('Could not discover internet gateway (upnp).'); + this.logger.debug(e); + return false; + } + + let host; + try { + host = await wan.getExternalIP(); + } catch (e) { + this.logger.debug('Could not find external IP (upnp).'); + this.logger.debug(e); + return false; + } + + if (this.hosts.addLocal(host, src, scores.UPNP)) + this.logger.info('External IP found (upnp): %s.', host); + + this.logger.debug( + 'Adding port mapping %d->%d.', + src, dest); + + try { + await wan.addPortMapping(host, src, dest); + } catch (e) { + this.logger.debug('Could not add port mapping (upnp).'); + this.logger.debug(e); + return false; + } + + return true; + } + + /** + * Attempt to resolve DNS seeds if necessary. + * @param {Boolean} checkPeers + * @returns {Promise} + */ + + async discoverSeeds(checkPeers) { + if (this.hosts.dnsSeeds.length === 0) + return; + + const max = Math.min(2, this.options.maxOutbound); + const size = this.hosts.size(); + + let total = 0; + for (let peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound) + continue; + + if (peer.connected) { + if (++total > max) + break; + } + } + + if (size === 0 || (checkPeers && total < max)) { + this.logger.warning('Could not find enough peers.'); + this.logger.warning('Hitting DNS seeds...'); + + await this.hosts.discoverSeeds(); + + this.logger.info( + 'Resolved %d hosts from DNS seeds.', + this.hosts.size() - size); + + this.refill(); + } + } + + /** + * Attempt to discover external IP via HTTP. + * @returns {Promise} + */ + + async discoverExternal() { + const port = this.options.publicPort; + + // Pointless if we're not listening. + if (!this.options.listen) + return; + + // Never hit an HTTP server if + // we're using an outbound proxy. + if (this.options.proxy) + return; + + // Try not to hit this if we can avoid it. + if (this.hosts.local.size > 0) + return; + + let host4; + try { + host4 = await external.getIPv4(); + } catch (e) { + this.logger.debug('Could not find external IPv4 (http).'); + this.logger.debug(e); + } + + if (host4 && this.hosts.addLocal(host4, port, scores.HTTP)) + this.logger.info('External IPv4 found (http): %s.', host4); + + let host6; + try { + host6 = await external.getIPv6(); + } catch (e) { + this.logger.debug('Could not find external IPv6 (http).'); + this.logger.debug(e); + } + + if (host6 && this.hosts.addLocal(host6, port, scores.HTTP)) + this.logger.info('External IPv6 found (http): %s.', host6); + } + + /** + * Handle incoming connection. + * @private + * @param {net.Socket} socket + */ + + handleSocket(socket) { + if (!socket.remoteAddress) { + this.logger.debug('Ignoring disconnected peer.'); + socket.destroy(); + return; + } + + const ip = IP.normalize(socket.remoteAddress); + + if (this.peers.inbound >= this.options.maxInbound) { + this.logger.debug('Ignoring peer: too many inbound (%s).', ip); + socket.destroy(); + return; + } + + if (this.hosts.isBanned(ip)) { + this.logger.debug('Ignoring banned peer (%s).', ip); + socket.destroy(); + return; + } + + const host = IP.toHostname(ip, socket.remotePort); + + assert(!this.peers.map.has(host), 'Port collision.'); + + this.addInbound(socket); + } + + /** + * Add a loader peer. Necessary for + * a sync to even begin. + * @private + */ + + addLoader() { + if (!this.opened) + return; + + assert(!this.peers.load); + + for (let peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound) + continue; + + this.logger.info( + 'Repurposing peer for loader (%s).', + peer.hostname()); + + this.setLoader(peer); + + return; + } + + const addr = this.getHost(); + + if (!addr) + return; + + const peer = this.createOutbound(addr); + + this.logger.info('Adding loader peer (%s).', peer.hostname()); + + this.peers.add(peer); + + this.setLoader(peer); + } + + /** + * Add a loader peer. Necessary for + * a sync to even begin. + * @private + */ + + setLoader(peer) { + if (!this.opened) + return; + + assert(peer.outbound); + assert(!this.peers.load); + assert(!peer.loader); + + peer.loader = true; + this.peers.load = peer; + + this.sendSync(peer); + + this.emit('loader', peer); + } + + /** + * Start the blockchain sync. + */ + + startSync() { + if (!this.opened) + return; + + assert(this.connected, 'Pool is not connected!'); + + this.syncing = true; + this.resync(false); + } + + /** + * Force sending of a sync to each peer. + */ + + forceSync() { + if (!this.opened) + return; + + assert(this.connected, 'Pool is not connected!'); + + this.resync(true); + } + + /** + * Send a sync to each peer. + */ + + sync(force) { + this.resync(false); + } + + /** + * Stop the sync. + * @private + */ + + stopSync() { + if (!this.syncing) + return; + + this.syncing = false; + + for (let peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound) + continue; + + if (!peer.syncing) + continue; + + peer.syncing = false; + peer.merkleBlock = null; + peer.merkleTime = -1; + peer.merkleMatches = 0; + peer.merkleMap = null; + peer.blockTime = -1; + peer.blockMap.clear(); + peer.compactBlocks.clear(); + } + + this.blockMap.clear(); + this.compactBlocks.clear(); + } + + /** + * Send a sync to each peer. + * @private + * @param {Boolean?} force + * @returns {Promise} + */ + + async resync(force) { + if (!this.syncing) + return; + + let locator; + try { + locator = await this.chain.getLocator(); + } catch (e) { + this.emit('error', e); + return; + } + + for (let peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound) + continue; + + if (!force && peer.syncing) + continue; + + this.sendLocator(locator, peer); + } + } + + /** + * Test whether a peer is sync-worthy. + * @param {Peer} peer + * @returns {Boolean} + */ + + isSyncable(peer) { + if (!this.syncing) + return false; + + if (peer.destroyed) + return false; + + if (!peer.handshake) + return false; + + if (!(peer.services & services.NETWORK)) + return false; + + if (this.options.hasWitness() && !peer.hasWitness()) + return false; + + if (!peer.loader) { + if (!this.chain.synced) + return false; + } + + return true; + } + + /** + * Start syncing from peer. + * @method + * @param {Peer} peer + * @returns {Promise} + */ + + async sendSync(peer) { + if (peer.syncing) + return false; + + if (!this.isSyncable(peer)) + return false; + + peer.syncing = true; + peer.blockTime = Date.now(); + + let locator; + try { + locator = await this.chain.getLocator(); + } catch (e) { + peer.syncing = false; + peer.blockTime = -1; + this.emit('error', e); + return false; + } + + return this.sendLocator(locator, peer); + } + + /** + * Send a chain locator and start syncing from peer. + * @method + * @param {Hash[]} locator + * @param {Peer} peer + * @returns {Boolean} + */ + + sendLocator(locator, peer) { + if (!this.isSyncable(peer)) + return false; + + // Ask for the mempool if we're synced. + if (this.network.requestMempool) { + if (peer.loader && this.chain.synced) + peer.sendMempool(); + } + + peer.syncing = true; + peer.blockTime = Date.now(); + + if (this.checkpoints) { + peer.sendGetHeaders(locator, this.headerTip.hash); + return true; + } + + peer.sendGetBlocks(locator); + + return true; + } + + /** + * Send `mempool` to all peers. + */ + + sendMempool() { + for (let peer = this.peers.head(); peer; peer = peer.next) + peer.sendMempool(); + } + + /** + * Send `getaddr` to all peers. + */ + + sendGetAddr() { + for (let peer = this.peers.head(); peer; peer = peer.next) + peer.sendGetAddr(); + } + + /** + * Request current header chain blocks. + * @private + * @param {Peer} peer + */ + + resolveHeaders(peer) { + const items = []; + + for (let node = this.headerNext; node; node = node.next) { + this.headerNext = node.next; + + items.push(node.hash); + + if (items.length === 50000) + break; + } + + this.getBlock(peer, items); + } + + /** + * Update all peer heights by their best hash. + * @param {Hash} hash + * @param {Number} height + */ + + resolveHeight(hash, height) { + let total = 0; + + for (let peer = this.peers.head(); peer; peer = peer.next) { + if (peer.bestHash !== hash) + continue; + + if (peer.bestHeight !== height) { + peer.bestHeight = height; + total += 1; + } + } + + if (total > 0) + this.logger.debug('Resolved height for %d peers.', total); + } + + /** + * Find the next checkpoint. + * @private + * @param {Number} height + * @returns {Object} + */ + + getNextTip(height) { + for (const next of this.network.checkpoints) { + if (next.height > height) + return new HeaderEntry(next.hash, next.height); + } + + throw new Error('Next checkpoint not found.'); + } + + /** + * Announce broadcast list to peer. + * @param {Peer} peer + */ + + announceList(peer) { + const blocks = []; + const txs = []; + + for (const item of this.invMap.values()) { + switch (item.type) { + case invTypes.BLOCK: + blocks.push(item.msg); + break; + case invTypes.TX: + txs.push(item.msg); + break; + default: + assert(false, 'Bad item type.'); + break; + } + } + + if (blocks.length > 0) + peer.announceBlock(blocks); + + if (txs.length > 0) + peer.announceTX(txs); + } + + /** + * Get a block/tx from the broadcast map. + * @private + * @param {Peer} peer + * @param {InvItem} item + * @returns {Promise} + */ + + getBroadcasted(peer, item) { + const type = item.isTX() ? invTypes.TX : invTypes.BLOCK; + const entry = this.invMap.get(item.hash); + + if (!entry) + return null; + + if (type !== entry.type) { + this.logger.debug( + 'Peer requested item with the wrong type (%s).', + peer.hostname()); + return null; + } + + this.logger.debug( + 'Peer requested %s %s as a %s packet (%s).', + item.isTX() ? 'tx' : 'block', + item.rhash(), + item.hasWitness() ? 'witness' : 'normal', + peer.hostname()); + + entry.handleAck(peer); + + return entry.msg; + } + + /** + * Get a block/tx either from the broadcast map, mempool, or blockchain. + * @method + * @private + * @param {Peer} peer + * @param {InvItem} item + * @returns {Promise} + */ + + async getItem(peer, item) { + const entry = this.getBroadcasted(peer, item); + + if (entry) + return entry; + + if (this.options.selfish) + return null; + + if (item.isTX()) { + if (!this.mempool) + return null; + return this.mempool.getTX(item.hash); + } + + if (this.chain.options.spv) + return null; + + if (this.chain.options.prune) + return null; + + return this.chain.getBlock(item.hash); + } + + /** + * Send a block from the broadcast list or chain. + * @method + * @private + * @param {Peer} peer + * @param {InvItem} item + * @returns {Boolean} + */ + + async sendBlock(peer, item, witness) { + const broadcasted = this.getBroadcasted(peer, item); + + // Check for a broadcasted item first. + if (broadcasted) { + peer.send(new packets.BlockPacket(broadcasted, witness)); + return true; + } + + if (this.options.selfish + || this.chain.options.spv + || this.chain.options.prune) { + return false; + } + + // If we have the same serialization, we + // can write the raw binary to the socket. + if (witness || !this.options.hasWitness()) { + const block = await this.chain.getRawBlock(item.hash); + + if (block) { + peer.sendRaw('block', block); + return true; + } + + return false; + } + + const block = await this.chain.getBlock(item.hash); + + if (block) { + peer.send(new packets.BlockPacket(block, witness)); + return true; + } + + return false; + } + + /** + * Create an outbound peer with no special purpose. + * @private + * @param {NetAddress} addr + * @returns {Peer} + */ + + createOutbound(addr) { + const cipher = BIP151.ciphers.CHACHAPOLY; + const identity = this.options.identityKey; + const peer = Peer.fromOutbound(this.options, addr); + + this.hosts.markAttempt(addr.hostname); + + if (this.options.bip151) + peer.setCipher(cipher); + + if (this.options.bip150) + peer.setAuth(this.authdb, identity); + + this.bindPeer(peer); + + this.logger.debug('Connecting to %s.', peer.hostname()); + + peer.tryOpen(); + + return peer; + } + + /** + * Accept an inbound socket. + * @private + * @param {net.Socket} socket + * @returns {Peer} + */ + + createInbound(socket) { + const cipher = BIP151.ciphers.CHACHAPOLY; + const identity = this.options.identityKey; + const peer = Peer.fromInbound(this.options, socket); + + if (this.options.bip151) + peer.setCipher(cipher); + + if (this.options.bip150) + peer.setAuth(this.authdb, identity); + + this.bindPeer(peer); + + peer.tryOpen(); + + return peer; + } + + /** + * Allocate new peer id. + * @returns {Number} + */ + + uid() { + const MAX = Number.MAX_SAFE_INTEGER; + + if (this.id >= MAX - this.peers.size() - 1) + this.id = 0; + + // Once we overflow, there's a chance + // of collisions. Unlikely to happen + // unless we have tried to connect 9 + // quadrillion times, but still + // account for it. + do { + this.id += 1; + } while (this.peers.find(this.id)); + + return this.id; + } + + /** + * Bind to peer events. + * @private + * @param {Peer} peer + */ + + bindPeer(peer) { + peer.id = this.uid(); + + peer.onPacket = (packet) => { + return this.handlePacket(peer, packet); + }; + + peer.on('error', (err) => { + this.logger.debug(err); + }); + + peer.once('connect', () => { + this.handleConnect(peer); + }); + + peer.once('open', () => { + this.handleOpen(peer); + }); + + peer.once('close', (connected) => { + this.handleClose(peer, connected); + }); + + peer.once('ban', () => { + this.handleBan(peer); + }); + } + + /** + * Handle peer packet event. + * @method + * @private + * @param {Peer} peer + * @param {Packet} packet + * @returns {Promise} + */ + + async handlePacket(peer, packet) { + switch (packet.type) { + case packetTypes.VERSION: + await this.handleVersion(peer, packet); + break; + case packetTypes.VERACK: + await this.handleVerack(peer, packet); + break; + case packetTypes.PING: + await this.handlePing(peer, packet); + break; + case packetTypes.PONG: + await this.handlePong(peer, packet); + break; + case packetTypes.GETADDR: + await this.handleGetAddr(peer, packet); + break; + case packetTypes.ADDR: + await this.handleAddr(peer, packet); + break; + case packetTypes.INV: + await this.handleInv(peer, packet); + break; + case packetTypes.GETDATA: + await this.handleGetData(peer, packet); + break; + case packetTypes.NOTFOUND: + await this.handleNotFound(peer, packet); + break; + case packetTypes.GETBLOCKS: + await this.handleGetBlocks(peer, packet); + break; + case packetTypes.GETHEADERS: + await this.handleGetHeaders(peer, packet); + break; + case packetTypes.HEADERS: + await this.handleHeaders(peer, packet); + break; + case packetTypes.SENDHEADERS: + await this.handleSendHeaders(peer, packet); + break; + case packetTypes.BLOCK: + await this.handleBlock(peer, packet); + break; + case packetTypes.TX: + await this.handleTX(peer, packet); + break; + case packetTypes.REJECT: + await this.handleReject(peer, packet); + break; + case packetTypes.MEMPOOL: + await this.handleMempool(peer, packet); + break; + case packetTypes.FILTERLOAD: + await this.handleFilterLoad(peer, packet); + break; + case packetTypes.FILTERADD: + await this.handleFilterAdd(peer, packet); + break; + case packetTypes.FILTERCLEAR: + await this.handleFilterClear(peer, packet); + break; + case packetTypes.MERKLEBLOCK: + await this.handleMerkleBlock(peer, packet); + break; + case packetTypes.FEEFILTER: + await this.handleFeeFilter(peer, packet); + break; + case packetTypes.SENDCMPCT: + await this.handleSendCmpct(peer, packet); + break; + case packetTypes.CMPCTBLOCK: + await this.handleCmpctBlock(peer, packet); + break; + case packetTypes.GETBLOCKTXN: + await this.handleGetBlockTxn(peer, packet); + break; + case packetTypes.BLOCKTXN: + await this.handleBlockTxn(peer, packet); + break; + case packetTypes.ENCINIT: + await this.handleEncinit(peer, packet); + break; + case packetTypes.ENCACK: + await this.handleEncack(peer, packet); + break; + case packetTypes.AUTHCHALLENGE: + await this.handleAuthChallenge(peer, packet); + break; + case packetTypes.AUTHREPLY: + await this.handleAuthReply(peer, packet); + break; + case packetTypes.AUTHPROPOSE: + await this.handleAuthPropose(peer, packet); + break; + case packetTypes.UNKNOWN: + await this.handleUnknown(peer, packet); + break; + default: + assert(false, 'Bad packet type.'); + break; + } + + this.emit('packet', packet, peer); + } + + /** + * Handle peer connect event. + * @method + * @private + * @param {Peer} peer + */ + + async handleConnect(peer) { + this.logger.info('Connected to %s.', peer.hostname()); + + if (peer.outbound) + this.hosts.markSuccess(peer.hostname()); + + this.emit('peer connect', peer); + } + + /** + * Handle peer open event. + * @method + * @private + * @param {Peer} peer + */ + + async handleOpen(peer) { + // Advertise our address. + if (!this.options.selfish && this.options.listen) { + const addr = this.hosts.getLocal(peer.address); + if (addr) + peer.send(new packets.AddrPacket([addr])); + } + + // We want compact blocks! + if (this.options.compact) + peer.sendCompact(this.options.blockMode); + + // Find some more peers. + if (!this.hosts.isFull()) + peer.sendGetAddr(); + + // Relay our spv filter if we have one. + if (this.spvFilter) + peer.sendFilterLoad(this.spvFilter); + + // Announce our currently broadcasted items. + this.announceList(peer); + + // Set a fee rate filter. + if (this.options.feeRate !== -1) + peer.sendFeeRate(this.options.feeRate); + + // Start syncing the chain. + if (peer.outbound) + this.sendSync(peer); + + if (peer.outbound) { + this.hosts.markAck(peer.hostname(), peer.services); + + // If we don't have an ack'd + // loader yet consider it dead. + if (!peer.loader) { + if (this.peers.load && !this.peers.load.handshake) { + assert(this.peers.load.loader); + this.peers.load.loader = false; + this.peers.load = null; + } + } + + // If we do not have a loader, + // use this peer. + if (!this.peers.load) + this.setLoader(peer); + } + + this.emit('peer open', peer); + } + + /** + * Handle peer close event. + * @method + * @private + * @param {Peer} peer + * @param {Boolean} connected + */ + + async handleClose(peer, connected) { + const outbound = peer.outbound; + const loader = peer.loader; + const size = peer.blockMap.size; + + this.removePeer(peer); + + if (loader) { + this.logger.info('Removed loader peer (%s).', peer.hostname()); + if (this.checkpoints) + this.resetChain(); + } + + this.nonces.remove(peer.hostname()); + + this.emit('peer close', peer, connected); + + if (!this.opened) + return; + + if (this.disconnecting) + return; + + if (this.chain.synced && size > 0) { + this.logger.warning('Peer disconnected with requested blocks.'); + this.logger.warning('Resending sync...'); + this.forceSync(); + } + + if (!outbound) + return; + + this.refill(); + } + + /** + * Handle ban event. + * @method + * @private + * @param {Peer} peer + */ + + async handleBan(peer) { + this.ban(peer.address); + this.emit('ban', peer); + } + + /** + * Handle peer version event. + * @method + * @private + * @param {Peer} peer + * @param {VersionPacket} packet + */ + + async handleVersion(peer, packet) { + this.logger.info( + 'Received version (%s): version=%d height=%d services=%s agent=%s', + peer.hostname(), + packet.version, + packet.height, + packet.services.toString(2), + packet.agent); + + this.network.time.add(peer.hostname(), packet.time); + this.nonces.remove(peer.hostname()); + + if (!peer.outbound && packet.remote.isRoutable()) + this.hosts.markLocal(packet.remote); + } + + /** + * Handle `verack` packet. + * @method + * @private + * @param {Peer} peer + * @param {VerackPacket} packet + */ + + async handleVerack(peer, packet) { + ; + } + + /** + * Handle `ping` packet. + * @method + * @private + * @param {Peer} peer + * @param {PingPacket} packet + */ + + async handlePing(peer, packet) { + ; + } + + /** + * Handle `pong` packet. + * @method + * @private + * @param {Peer} peer + * @param {PongPacket} packet + */ + + async handlePong(peer, packet) { + ; + } + + /** + * Handle `getaddr` packet. + * @method + * @private + * @param {Peer} peer + * @param {GetAddrPacket} packet + */ + + async handleGetAddr(peer, packet) { + if (this.options.selfish) + return; + + if (peer.sentAddr) { + this.logger.debug( + 'Ignoring repeated getaddr (%s).', + peer.hostname()); + return; + } + + peer.sentAddr = true; + + const addrs = this.hosts.toArray(); + const items = []; + + for (const addr of addrs) { + if (!peer.addrFilter.added(addr.hostname, 'ascii')) + continue; + + items.push(addr); + + if (items.length === 1000) + break; + } + + if (items.length === 0) + return; + + this.logger.debug( + 'Sending %d addrs to peer (%s)', + items.length, + peer.hostname()); + + peer.send(new packets.AddrPacket(items)); + } + + /** + * Handle peer addr event. + * @method + * @private + * @param {Peer} peer + * @param {AddrPacket} packet + */ + + async handleAddr(peer, packet) { + const addrs = packet.items; + const now = this.network.now(); + const services = this.options.getRequiredServices(); + + for (const addr of addrs) { + peer.addrFilter.add(addr.hostname, 'ascii'); + + if (!addr.isRoutable()) + continue; + + if (!addr.hasServices(services)) + continue; + + if (addr.time <= 100000000 || addr.time > now + 10 * 60) + addr.time = now - 5 * 24 * 60 * 60; + + if (addr.port === 0) + continue; + + this.hosts.add(addr, peer.address); + } + + this.logger.info( + 'Received %d addrs (hosts=%d, peers=%d) (%s).', + addrs.length, + this.hosts.size(), + this.peers.size(), + peer.hostname()); + + this.fillOutbound(); + } + + /** + * Handle `inv` packet. + * @method + * @private + * @param {Peer} peer + * @param {InvPacket} packet + */ + + async handleInv(peer, packet) { + const unlock = await this.locker.lock(); + try { + return await this._handleInv(peer, packet); + } finally { + unlock(); + } + } + + /** + * Handle `inv` packet (without a lock). + * @method + * @private + * @param {Peer} peer + * @param {InvPacket} packet + */ + + async _handleInv(peer, packet) { + const items = packet.items; + + if (items.length > 50000) { + peer.increaseBan(100); + return; + } + + const blocks = []; + const txs = []; + let unknown = -1; + + for (const item of items) { + switch (item.type) { + case invTypes.BLOCK: + blocks.push(item.hash); + break; + case invTypes.TX: + txs.push(item.hash); + break; + default: + unknown = item.type; + continue; + } + peer.invFilter.add(item.hash, 'hex'); + } + + this.logger.spam( + 'Received inv packet with %d items: blocks=%d txs=%d (%s).', + items.length, blocks.length, txs.length, peer.hostname()); + + if (unknown !== -1) { + this.logger.warning( + 'Peer sent an unknown inv type: %d (%s).', + unknown, peer.hostname()); + } + + if (blocks.length > 0) + await this.handleBlockInv(peer, blocks); + + if (txs.length > 0) + await this.handleTXInv(peer, txs); + } + + /** + * Handle `inv` packet from peer (containing only BLOCK types). + * @method + * @private + * @param {Peer} peer + * @param {Hash[]} hashes + * @returns {Promise} + */ + + async handleBlockInv(peer, hashes) { + assert(hashes.length > 0); + + if (!this.syncing) + return; + + // Always keep track of the peer's best hash. + if (!peer.loader || this.chain.synced) { + const hash = hashes[hashes.length - 1]; + peer.bestHash = hash; + } + + // Ignore for now if we're still syncing + if (!this.chain.synced && !peer.loader) + return; + + if (this.options.hasWitness() && !peer.hasWitness()) + return; + + // Request headers instead. + if (this.checkpoints) + return; + + this.logger.debug( + 'Received %s block hashes from peer (%s).', + hashes.length, + peer.hostname()); + + const items = []; + + let exists = null; + + for (let i = 0; i < hashes.length; i++) { + const hash = hashes[i]; + + // Resolve orphan chain. + if (this.chain.hasOrphan(hash)) { + this.logger.debug('Received known orphan hash (%s).', peer.hostname()); + await this.resolveOrphan(peer, hash); + continue; + } + + // Request the block if we don't have it. + if (!await this.hasBlock(hash)) { + items.push(hash); + continue; + } + + exists = hash; + + // Normally we request the hashContinue. + // In the odd case where we already have + // it, we can do one of two things: either + // force re-downloading of the block to + // continue the sync, or do a getblocks + // from the last hash (this will reset + // the hashContinue on the remote node). + if (i === hashes.length - 1) { + this.logger.debug('Received existing hash (%s).', peer.hostname()); + await this.getBlocks(peer, hash); + } + } + + // Attempt to update the peer's best height + // with the last existing hash we know of. + if (exists && this.chain.synced) { + const height = await this.chain.getHeight(exists); + if (height !== -1) + peer.bestHeight = height; + } + + this.getBlock(peer, items); + } + + /** + * Handle peer inv packet (txs). + * @method + * @private + * @param {Peer} peer + * @param {Hash[]} hashes + */ + + async handleTXInv(peer, hashes) { + assert(hashes.length > 0); + + if (this.syncing && !this.chain.synced) + return; + + this.ensureTX(peer, hashes); + } + + /** + * Handle `getdata` packet. + * @method + * @private + * @param {Peer} peer + * @param {GetDataPacket} packet + */ + + async handleGetData(peer, packet) { + const items = packet.items; + + if (items.length > 50000) { + this.logger.warning('Peer sent inv with >50k items (%s).', peer.hostname()); + peer.increaseBan(100); + peer.destroy(); + return; + } + + const notFound = []; + + let txs = 0; + let blocks = 0; + let compact = 0; + let unknown = -1; + + for (const item of items) { + if (item.isTX()) { + const tx = await this.getItem(peer, item); + + if (!tx) { + notFound.push(item); + continue; + } + + // Coinbases are an insta-ban from any node. + // This should technically never happen, but + // it's worth keeping here just in case. A + // 24-hour ban from any node is rough. + if (tx.isCoinbase()) { + notFound.push(item); + this.logger.warning('Failsafe: tried to relay a coinbase.'); + continue; + } + + peer.send(new packets.TXPacket(tx, item.hasWitness())); + + txs += 1; + + continue; + } + + switch (item.type) { + case invTypes.BLOCK: + case invTypes.WITNESS_BLOCK: { + const result = await this.sendBlock(peer, item, item.hasWitness()); + if (!result) { + notFound.push(item); + continue; + } + blocks += 1; + break; + } + case invTypes.FILTERED_BLOCK: + case invTypes.WITNESS_FILTERED_BLOCK: { + if (!this.options.bip37) { + this.logger.debug( + 'Peer requested a merkleblock without bip37 enabled (%s).', + peer.hostname()); + peer.destroy(); + return; + } + + if (!peer.spvFilter) { + notFound.push(item); + continue; + } + + const block = await this.getItem(peer, item); + + if (!block) { + notFound.push(item); + continue; + } + + const merkle = block.toMerkle(peer.spvFilter); + + peer.send(new packets.MerkleBlockPacket(merkle)); + + for (const tx of merkle.txs) { + peer.send(new packets.TXPacket(tx, item.hasWitness())); + txs += 1; + } + + blocks += 1; + + break; + } + case invTypes.CMPCT_BLOCK: { + const height = await this.chain.getHeight(item.hash); + + // Fallback to full block. + if (height < this.chain.tip.height - 10) { + const result = await this.sendBlock(peer, item, peer.compactWitness); + if (!result) { + notFound.push(item); + continue; + } + blocks += 1; + break; + } + + const block = await this.getItem(peer, item); + + if (!block) { + notFound.push(item); + continue; + } + + peer.sendCompactBlock(block); + + blocks += 1; + compact += 1; + + break; + } + default: { + unknown = item.type; + notFound.push(item); + continue; + } + } + + if (item.hash === peer.hashContinue) { + peer.sendInv([new InvItem(invTypes.BLOCK, this.chain.tip.hash)]); + peer.hashContinue = null; + } + + // Wait for the peer to read + // before we pull more data + // out of the database. + await peer.drain(); + } + + if (notFound.length > 0) + peer.send(new packets.NotFoundPacket(notFound)); + + if (txs > 0) { + this.logger.debug( + 'Served %d txs with getdata (notfound=%d) (%s).', + txs, notFound.length, peer.hostname()); + } + + if (blocks > 0) { + this.logger.debug( + 'Served %d blocks with getdata (notfound=%d, cmpct=%d) (%s).', + blocks, notFound.length, compact, peer.hostname()); + } + + if (unknown !== -1) { + this.logger.warning( + 'Peer sent an unknown getdata type: %s (%d).', + unknown, peer.hostname()); + } + } + + /** + * Handle peer notfound packet. + * @method + * @private + * @param {Peer} peer + * @param {NotFoundPacket} packet + */ + + async handleNotFound(peer, packet) { + const items = packet.items; + + for (const item of items) { + if (!this.resolveItem(peer, item)) { + this.logger.warning( + 'Peer sent notfound for unrequested item: %s (%s).', + item.hash, peer.hostname()); + peer.destroy(); + return; + } + } + } + + /** + * Handle `getblocks` packet. + * @method + * @private + * @param {Peer} peer + * @param {GetBlocksPacket} packet + */ + + async handleGetBlocks(peer, packet) { + if (!this.chain.synced) + return; + + if (this.options.selfish) + return; + + if (this.chain.options.spv) + return; + + if (this.chain.options.prune) + return; + + let hash = await this.chain.findLocator(packet.locator); + + if (hash) + hash = await this.chain.getNextHash(hash); + + const blocks = []; + + while (hash) { + blocks.push(new InvItem(invTypes.BLOCK, hash)); + + if (hash === packet.stop) + break; + + if (blocks.length === 500) { + peer.hashContinue = hash; + break; + } + + hash = await this.chain.getNextHash(hash); + } + + peer.sendInv(blocks); + } + + /** + * Handle `getheaders` packet. + * @method + * @private + * @param {Peer} peer + * @param {GetHeadersPacket} packet + */ + + async handleGetHeaders(peer, packet) { + if (!this.chain.synced) + return; + + if (this.options.selfish) + return; + + if (this.chain.options.spv) + return; + + if (this.chain.options.prune) + return; + + let hash; + if (packet.locator.length > 0) { + hash = await this.chain.findLocator(packet.locator); + if (hash) + hash = await this.chain.getNextHash(hash); + } else { + hash = packet.stop; + } + + let entry; + if (hash) + entry = await this.chain.getEntry(hash); + + const headers = []; + + while (entry) { + headers.push(entry.toHeaders()); + + if (entry.hash === packet.stop) + break; + + if (headers.length === 2000) + break; + + entry = await this.chain.getNext(entry); + } + + peer.sendHeaders(headers); + } + + /** + * Handle `headers` packet from a given peer. + * @method + * @private + * @param {Peer} peer + * @param {HeadersPacket} packet + * @returns {Promise} + */ + + async handleHeaders(peer, packet) { + const unlock = await this.locker.lock(); + try { + return await this._handleHeaders(peer, packet); + } finally { + unlock(); + } + } + + /** + * Handle `headers` packet from + * a given peer without a lock. + * @method + * @private + * @param {Peer} peer + * @param {HeadersPacket} packet + * @returns {Promise} + */ + + async _handleHeaders(peer, packet) { + const headers = packet.items; + + if (!this.checkpoints) + return; + + if (!this.syncing) + return; + + if (!peer.loader) + return; + + if (headers.length === 0) + return; + + if (headers.length > 2000) { + peer.increaseBan(100); + return; + } + + assert(this.headerChain.size > 0); + + let checkpoint = false; + let node = null; + + for (const header of headers) { + const last = this.headerChain.tail; + const hash = header.hash('hex'); + const height = last.height + 1; + + if (!header.verify()) { + this.logger.warning( + 'Peer sent an invalid header (%s).', + peer.hostname()); + peer.increaseBan(100); + peer.destroy(); + return; + } + + if (header.prevBlock !== last.hash) { + this.logger.warning( + 'Peer sent a bad header chain (%s).', + peer.hostname()); + peer.destroy(); + return; + } + + node = new HeaderEntry(hash, height); + + if (node.height === this.headerTip.height) { + if (node.hash !== this.headerTip.hash) { + this.logger.warning( + 'Peer sent an invalid checkpoint (%s).', + peer.hostname()); + peer.destroy(); + return; + } + checkpoint = true; + } + + if (!this.headerNext) + this.headerNext = node; + + this.headerChain.push(node); + } + + this.logger.debug( + 'Received %s headers from peer (%s).', + headers.length, + peer.hostname()); + + // If we received a valid header + // chain, consider this a "block". + peer.blockTime = Date.now(); + + // Request the blocks we just added. + if (checkpoint) { + this.headerChain.shift(); + this.resolveHeaders(peer); + return; + } + + // Request more headers. + peer.sendGetHeaders([node.hash], this.headerTip.hash); + } + + /** + * Handle `sendheaders` packet. + * @method + * @private + * @param {Peer} peer + * @param {SendHeadersPacket} packet + * @returns {Promise} + */ + + async handleSendHeaders(peer, packet) { + ; + } + + /** + * Handle `block` packet. Attempt to add to chain. + * @method + * @private + * @param {Peer} peer + * @param {BlockPacket} packet + * @returns {Promise} + */ + + async handleBlock(peer, packet) { + const flags = chainCommon.flags.DEFAULT_FLAGS; + + if (this.options.spv) { + this.logger.warning( + 'Peer sent unsolicited block (%s).', + peer.hostname()); + return; + } + + await this.addBlock(peer, packet.block, flags); + } + + /** + * Attempt to add block to chain. + * @method + * @private + * @param {Peer} peer + * @param {Block} block + * @returns {Promise} + */ + + async addBlock(peer, block, flags) { + const hash = block.hash('hex'); + const unlock = await this.locker.lock(hash); + try { + return await this._addBlock(peer, block, flags); + } finally { + unlock(); + } + } + + /** + * Attempt to add block to chain (without a lock). + * @method + * @private + * @param {Peer} peer + * @param {Block} block + * @returns {Promise} + */ + + async _addBlock(peer, block, flags) { + if (!this.syncing) + return; + + const hash = block.hash('hex'); + + if (!this.resolveBlock(peer, hash)) { + this.logger.warning( + 'Received unrequested block: %s (%s).', + block.rhash(), peer.hostname()); + peer.destroy(); + return; + } + + peer.blockTime = Date.now(); + + let entry; + try { + entry = await this.chain.add(block, flags, peer.id); + } catch (err) { + if (err.type === 'VerifyError') { + peer.reject('block', err); + this.logger.warning(err); + return; + } + throw err; + } + + // Block was orphaned. + if (!entry) { + if (this.checkpoints) { + this.logger.warning( + 'Peer sent orphan block with getheaders (%s).', + peer.hostname()); + return; + } + + // During a getblocks sync, peers send + // their best tip frequently. We can grab + // the height commitment from the coinbase. + const height = block.getCoinbaseHeight(); + + if (height !== -1) { + peer.bestHash = hash; + peer.bestHeight = height; + this.resolveHeight(hash, height); + } + + this.logger.debug('Peer sent an orphan block. Resolving.'); + + await this.resolveOrphan(peer, hash); + + return; + } + + if (this.chain.synced) { + peer.bestHash = entry.hash; + peer.bestHeight = entry.height; + this.resolveHeight(entry.hash, entry.height); + } + + this.logStatus(block); + + await this.resolveChain(peer, hash); + } + + /** + * Resolve header chain. + * @method + * @private + * @param {Peer} peer + * @param {Hash} hash + * @returns {Promise} + */ + + async resolveChain(peer, hash) { + if (!this.checkpoints) + return; + + if (!peer.loader) + return; + + if (peer.destroyed) + throw new Error('Peer was destroyed (header chain resolution).'); + + const node = this.headerChain.head; + + assert(node); + + if (hash !== node.hash) { + this.logger.warning( + 'Header hash mismatch %s != %s (%s).', + encoding.revHex(hash), + encoding.revHex(node.hash), + peer.hostname()); + + peer.destroy(); + + return; + } + + if (node.height < this.network.lastCheckpoint) { + if (node.height === this.headerTip.height) { + this.logger.info( + 'Received checkpoint %s (%d).', + encoding.revHex(node.hash), node.height); + + this.headerTip = this.getNextTip(node.height); + + peer.sendGetHeaders([hash], this.headerTip.hash); + + return; + } + + this.headerChain.shift(); + this.resolveHeaders(peer); + + return; + } + + this.logger.info( + 'Switching to getblocks (%s).', + peer.hostname()); + + await this.switchSync(peer, hash); + } + + /** + * Switch to getblocks. + * @method + * @private + * @param {Peer} peer + * @param {Hash} hash + * @returns {Promise} + */ + + async switchSync(peer, hash) { + assert(this.checkpoints); + + this.checkpoints = false; + this.headerTip = null; + this.headerChain.reset(); + this.headerNext = null; + + await this.getBlocks(peer, hash); + } + + /** + * Handle bad orphan. + * @method + * @private + * @param {String} msg + * @param {VerifyError} err + * @param {Number} id + */ + + handleBadOrphan(msg, err, id) { + const peer = this.peers.find(id); + + if (!peer) { + this.logger.warning( + 'Could not find offending peer for orphan: %s (%d).', + encoding.revHex(err.hash), id); + return; + } + + this.logger.debug( + 'Punishing peer for sending a bad orphan (%s).', + peer.hostname()); + + // Punish the original peer who sent this. + peer.reject(msg, err); + } + + /** + * Log sync status. + * @private + * @param {Block} block + */ + + logStatus(block) { + if (this.chain.height % 20 === 0) { + this.logger.debug('Status:' + + ' time=%s height=%d progress=%s' + + ' orphans=%d active=%d' + + ' target=%s peers=%d', + util.date(block.time), + this.chain.height, + (this.chain.getProgress() * 100).toFixed(2) + '%', + this.chain.orphanMap.size, + this.blockMap.size, + block.bits, + this.peers.size()); + } + + if (this.chain.height % 2000 === 0) { + this.logger.info( + 'Received 2000 more blocks (height=%d, hash=%s).', + this.chain.height, + block.rhash()); + } + } + + /** + * Handle a transaction. Attempt to add to mempool. + * @method + * @private + * @param {Peer} peer + * @param {TXPacket} packet + * @returns {Promise} + */ + + async handleTX(peer, packet) { + const hash = packet.tx.hash('hex'); + const unlock = await this.locker.lock(hash); + try { + return await this._handleTX(peer, packet); + } finally { + unlock(); + } + } + + /** + * Handle a transaction. Attempt to add to mempool (without a lock). + * @method + * @private + * @param {Peer} peer + * @param {TXPacket} packet + * @returns {Promise} + */ + + async _handleTX(peer, packet) { + const tx = packet.tx; + const hash = tx.hash('hex'); + const flags = chainCommon.flags.VERIFY_NONE; + const block = peer.merkleBlock; + + if (block) { + assert(peer.merkleMatches > 0); + assert(peer.merkleMap); + + if (block.hasTX(hash)) { + if (peer.merkleMap.has(hash)) { + this.logger.warning( + 'Peer sent duplicate merkle tx: %s (%s).', + tx.txid(), peer.hostname()); + peer.increaseBan(100); + return; + } + + peer.merkleMap.add(hash); + + block.txs.push(tx); + + if (--peer.merkleMatches === 0) { + peer.merkleBlock = null; + peer.merkleTime = -1; + peer.merkleMatches = 0; + peer.merkleMap = null; + await this._addBlock(peer, block, flags); + } + + return; + } + } + + if (!this.resolveTX(peer, hash)) { + this.logger.warning( + 'Peer sent unrequested tx: %s (%s).', + tx.txid(), peer.hostname()); + peer.destroy(); + return; + } + + if (!this.mempool) { + this.emit('tx', tx); + return; + } + + let missing; + try { + missing = await this.mempool.addTX(tx, peer.id); + } catch (err) { + if (err.type === 'VerifyError') { + peer.reject('tx', err); + this.logger.info(err); + return; + } + throw err; + } + + if (missing && missing.length > 0) { + this.logger.debug( + 'Requesting %d missing transactions (%s).', + missing.length, peer.hostname()); + + this.ensureTX(peer, missing); + } + } + + /** + * Handle peer reject event. + * @method + * @private + * @param {Peer} peer + * @param {RejectPacket} packet + */ + + async handleReject(peer, packet) { + this.logger.warning( + 'Received reject (%s): msg=%s code=%s reason=%s hash=%s.', + peer.hostname(), + packet.message, + packet.getCode(), + packet.reason, + packet.rhash()); + + if (!packet.hash) + return; + + const entry = this.invMap.get(packet.hash); + + if (!entry) + return; + + entry.handleReject(peer); + } + + /** + * Handle `mempool` packet. + * @method + * @private + * @param {Peer} peer + * @param {MempoolPacket} packet + */ + + async handleMempool(peer, packet) { + if (!this.mempool) + return; + + if (!this.chain.synced) + return; + + if (this.options.selfish) + return; + + if (!this.options.bip37) { + this.logger.debug( + 'Peer requested mempool without bip37 enabled (%s).', + peer.hostname()); + peer.destroy(); + return; + } + + const items = []; + + for (const hash of this.mempool.map.keys()) + items.push(new InvItem(invTypes.TX, hash)); + + this.logger.debug( + 'Sending mempool snapshot (%s).', + peer.hostname()); + + peer.queueInv(items); + } + + /** + * Handle `filterload` packet. + * @method + * @private + * @param {Peer} peer + * @param {FilterLoadPacket} packet + */ + + async handleFilterLoad(peer, packet) { + ; + } + + /** + * Handle `filteradd` packet. + * @method + * @private + * @param {Peer} peer + * @param {FilterAddPacket} packet + */ + + async handleFilterAdd(peer, packet) { + ; + } + + /** + * Handle `filterclear` packet. + * @method + * @private + * @param {Peer} peer + * @param {FilterClearPacket} packet + */ + + async handleFilterClear(peer, packet) { + ; + } + + /** + * Handle `merkleblock` packet. + * @method + * @private + * @param {Peer} peer + * @param {MerkleBlockPacket} block + */ + + async handleMerkleBlock(peer, packet) { + const hash = packet.block.hash('hex'); + const unlock = await this.locker.lock(hash); + try { + return await this._handleMerkleBlock(peer, packet); + } finally { + unlock(); + } + } + + /** + * Handle `merkleblock` packet (without a lock). + * @method + * @private + * @param {Peer} peer + * @param {MerkleBlockPacket} block + */ + + async _handleMerkleBlock(peer, packet) { + if (!this.syncing) + return; + + // Potential DoS. + if (!this.options.spv) { + this.logger.warning( + 'Peer sent unsolicited merkleblock (%s).', + peer.hostname()); + peer.increaseBan(100); + return; + } + + const block = packet.block; + const hash = block.hash('hex'); + + if (!peer.blockMap.has(hash)) { + this.logger.warning( + 'Peer sent an unrequested merkleblock (%s).', + peer.hostname()); + peer.destroy(); + return; + } + + if (peer.merkleBlock) { + this.logger.warning( + 'Peer sent a merkleblock prematurely (%s).', + peer.hostname()); + peer.increaseBan(100); + return; + } + + if (!block.verify()) { + this.logger.warning( + 'Peer sent an invalid merkleblock (%s).', + peer.hostname()); + peer.increaseBan(100); + return; + } + + const tree = block.getTree(); + + if (tree.matches.length === 0) { + const flags = chainCommon.flags.VERIFY_NONE; + await this._addBlock(peer, block, flags); + return; + } + + peer.merkleBlock = block; + peer.merkleTime = Date.now(); + peer.merkleMatches = tree.matches.length; + peer.merkleMap = new Set(); + } + + /** + * Handle `sendcmpct` packet. + * @method + * @private + * @param {Peer} peer + * @param {FeeFilterPacket} packet + */ + + async handleFeeFilter(peer, packet) { + ; + } + + /** + * Handle `sendcmpct` packet. + * @method + * @private + * @param {Peer} peer + * @param {SendCmpctPacket} packet + */ + + async handleSendCmpct(peer, packet) { + ; + } + + /** + * Handle `cmpctblock` packet. + * @method + * @private + * @param {Peer} peer + * @param {CompactBlockPacket} packet + */ + + async handleCmpctBlock(peer, packet) { + const block = packet.block; + const hash = block.hash('hex'); + const witness = peer.compactWitness; + + if (!this.syncing) + return; + + if (!this.options.compact) { + this.logger.info( + 'Peer sent unsolicited cmpctblock (%s).', + peer.hostname()); + this.destroy(); + return; + } + + if (!peer.hasCompactSupport() || !peer.hasCompact()) { + this.logger.info( + 'Peer sent unsolicited cmpctblock (%s).', + peer.hostname()); + this.destroy(); + return; + } + + if (peer.compactBlocks.has(hash)) { + this.logger.debug( + 'Peer sent us a duplicate compact block (%s).', + peer.hostname()); + return; + } + + if (this.compactBlocks.has(hash)) { + this.logger.debug( + 'Already waiting for compact block %s (%s).', + hash, peer.hostname()); + return; + } + + if (!peer.blockMap.has(hash)) { + if (this.options.blockMode !== 1) { + this.logger.warning( + 'Peer sent us an unrequested compact block (%s).', + peer.hostname()); + peer.destroy(); + return; + } + peer.blockMap.set(hash, Date.now()); + assert(!this.blockMap.has(hash)); + this.blockMap.add(hash); + } + + if (!this.mempool) { + this.logger.warning('Requesting compact blocks without a mempool!'); + return; + } + + if (!block.verify()) { + this.logger.debug( + 'Peer sent an invalid compact block (%s).', + peer.hostname()); + peer.increaseBan(100); + return; + } + + let result; + try { + result = block.init(); + } catch (e) { + this.logger.debug( + 'Peer sent an invalid compact block (%s).', + peer.hostname()); + peer.increaseBan(100); + return; + } + + if (!result) { + this.logger.warning( + 'Siphash collision for %s. Requesting full block (%s).', + block.rhash(), peer.hostname()); + peer.getFullBlock(hash); + peer.increaseBan(10); + return; + } + + const full = block.fillMempool(witness, this.mempool); + + if (full) { + this.logger.debug( + 'Received full compact block %s (%s).', + block.rhash(), peer.hostname()); + const flags = chainCommon.flags.VERIFY_BODY; + await this.addBlock(peer, block.toBlock(), flags); + return; + } + + if (this.options.blockMode === 1) { + if (peer.compactBlocks.size >= 15) { + this.logger.warning('Compact block DoS attempt (%s).', peer.hostname()); + peer.destroy(); + return; + } + } + + block.now = Date.now(); + + assert(!peer.compactBlocks.has(hash)); + peer.compactBlocks.set(hash, block); + + this.compactBlocks.add(hash); + + this.logger.debug( + 'Received non-full compact block %s tx=%d/%d (%s).', + block.rhash(), block.count, block.totalTX, peer.hostname()); + + peer.send(new packets.GetBlockTxnPacket(block.toRequest())); + } + + /** + * Handle `getblocktxn` packet. + * @method + * @private + * @param {Peer} peer + * @param {GetBlockTxnPacket} packet + */ + + async handleGetBlockTxn(peer, packet) { + const req = packet.request; + + if (this.chain.options.spv) + return; + + if (this.chain.options.prune) + return; + + if (this.options.selfish) + return; + + const item = new InvItem(invTypes.BLOCK, req.hash); + + const block = await this.getItem(peer, item); + + if (!block) { + this.logger.debug( + 'Peer sent getblocktxn for non-existent block (%s).', + peer.hostname()); + peer.increaseBan(100); + return; + } + + const height = await this.chain.getHeight(req.hash); + + if (height < this.chain.tip.height - 15) { + this.logger.debug( + 'Peer sent a getblocktxn for a block > 15 deep (%s)', + peer.hostname()); + return; + } + + this.logger.debug( + 'Sending blocktxn for %s to peer (%s).', + block.rhash(), + peer.hostname()); + + const res = BIP152.TXResponse.fromBlock(block, req); + + peer.send(new packets.BlockTxnPacket(res, peer.compactWitness)); + } + + /** + * Handle `blocktxn` packet. + * @method + * @private + * @param {Peer} peer + * @param {BlockTxnPacket} packet + */ + + async handleBlockTxn(peer, packet) { + const res = packet.response; + const block = peer.compactBlocks.get(res.hash); + const flags = chainCommon.flags.VERIFY_BODY; + + if (!block) { + this.logger.debug( + 'Peer sent unsolicited blocktxn (%s).', + peer.hostname()); + return; + } + + peer.compactBlocks.delete(res.hash); + + assert(this.compactBlocks.has(res.hash)); + this.compactBlocks.delete(res.hash); + + if (!block.fillMissing(res)) { + this.logger.warning( + 'Peer sent non-full blocktxn for %s. Requesting full block (%s).', + block.rhash(), + peer.hostname()); + peer.getFullBlock(res.hash); + peer.increaseBan(10); + return; + } + + this.logger.debug( + 'Filled compact block %s (%s).', + block.rhash(), peer.hostname()); + + await this.addBlock(peer, block.toBlock(), flags); + } + + /** + * Handle `encinit` packet. + * @method + * @private + * @param {Peer} peer + * @param {EncinitPacket} packet + */ + + async handleEncinit(peer, packet) { + ; + } + + /** + * Handle `encack` packet. + * @method + * @private + * @param {Peer} peer + * @param {EncackPacket} packet + */ + + async handleEncack(peer, packet) { + ; + } + + /** + * Handle `authchallenge` packet. + * @method + * @private + * @param {Peer} peer + * @param {AuthChallengePacket} packet + */ + + async handleAuthChallenge(peer, packet) { + ; + } + + /** + * Handle `authreply` packet. + * @method + * @private + * @param {Peer} peer + * @param {AuthReplyPacket} packet + */ + + async handleAuthReply(peer, packet) { + ; + } + + /** + * Handle `authpropose` packet. + * @method + * @private + * @param {Peer} peer + * @param {AuthProposePacket} packet + */ + + async handleAuthPropose(peer, packet) { + ; + } + + /** + * Handle `unknown` packet. + * @method + * @private + * @param {Peer} peer + * @param {UnknownPacket} packet + */ + + async handleUnknown(peer, packet) { + this.logger.warning( + 'Unknown packet: %s (%s).', + packet.cmd, peer.hostname()); + } + + /** + * Create an inbound peer from an existing socket. + * @private + * @param {net.Socket} socket + */ + + addInbound(socket) { + if (!this.opened) { + socket.destroy(); + return; + } + + const peer = this.createInbound(socket); + + this.logger.info('Added inbound peer (%s).', peer.hostname()); + + this.peers.add(peer); + } + + /** + * Allocate a host from the host list. + * @returns {NetAddress} + */ + + getHost() { + for (const addr of this.hosts.nodes) { + if (this.peers.has(addr.hostname)) + continue; + + return addr; + } + + const services = this.options.getRequiredServices(); + const now = this.network.now(); + + for (let i = 0; i < 100; i++) { + const entry = this.hosts.getHost(); + + if (!entry) + break; + + const addr = entry.addr; + + if (this.peers.has(addr.hostname)) + continue; + + if (!addr.isValid()) + continue; + + if (!addr.hasServices(services)) + continue; + + if (!this.options.onion && addr.isOnion()) + continue; + + if (i < 30 && now - entry.lastAttempt < 600) + continue; + + if (i < 50 && addr.port !== this.network.port) + continue; + + if (i < 95 && this.hosts.isBanned(addr.host)) + continue; + + return entry.addr; + } + + return null; + } + + /** + * Create an outbound non-loader peer. These primarily + * exist for transaction relaying. + * @private + */ + + addOutbound() { + if (!this.opened) + return; + + if (this.peers.outbound >= this.options.maxOutbound) + return; + + // Hang back if we don't + // have a loader peer yet. + if (!this.peers.load) + return; + + const addr = this.getHost(); + + if (!addr) + return; + + const peer = this.createOutbound(addr); + + this.peers.add(peer); + + this.emit('peer', peer); + } + + /** + * Attempt to refill the pool with peers (no lock). + * @private + */ + + fillOutbound() { + const need = this.options.maxOutbound - this.peers.outbound; + + if (!this.peers.load) + this.addLoader(); + + if (need <= 0) + return; + + this.logger.debug('Refilling peers (%d/%d).', + this.peers.outbound, + this.options.maxOutbound); + + for (let i = 0; i < need; i++) + this.addOutbound(); + } + + /** + * Attempt to refill the pool with peers (no lock). + * @private + */ + + refill() { + if (this.pendingRefill != null) + return; + + this.pendingRefill = setTimeout(() => { + this.pendingRefill = null; + this.fillOutbound(); + }, 3000); + } + + /** + * Remove a peer from any list. Drop all load requests. + * @private + * @param {Peer} peer + */ + + removePeer(peer) { + this.peers.remove(peer); + + for (const hash of peer.blockMap.keys()) + this.resolveBlock(peer, hash); + + for (const hash of peer.txMap.keys()) + this.resolveTX(peer, hash); + + for (const hash of peer.compactBlocks.keys()) { + assert(this.compactBlocks.has(hash)); + this.compactBlocks.delete(hash); + } + + peer.compactBlocks.clear(); + } + + /** + * Ban peer. + * @param {NetAddress} addr + */ + + ban(addr) { + const peer = this.peers.get(addr.hostname); + + this.logger.debug('Banning peer (%s).', addr.hostname); + + this.hosts.ban(addr.host); + this.hosts.remove(addr.hostname); + + if (peer) + peer.destroy(); + } + + /** + * Unban peer. + * @param {NetAddress} addr + */ + + unban(addr) { + this.hosts.unban(addr.host); + } + + /** + * Set the spv filter. + * @param {BloomFilter} filter + * @param {String?} enc + */ + + setFilter(filter) { + if (!this.options.spv) + return; + + this.spvFilter = filter; + this.queueFilterLoad(); + } + + /** + * Watch a an address hash (filterload, SPV-only). + * @param {Buffer|Hash} data + * @param {String?} enc + */ + + watch(data, enc) { + if (!this.options.spv) + return; + + this.spvFilter.add(data, enc); + this.queueFilterLoad(); + } + + /** + * Reset the spv filter (filterload, SPV-only). + */ + + unwatch() { + if (!this.options.spv) + return; + + this.spvFilter.reset(); + this.queueFilterLoad(); + } + + /** + * Queue a resend of the bloom filter. + */ + + queueFilterLoad() { + if (!this.options.spv) + return; + + if (this.pendingFilter != null) + return; + + this.pendingFilter = setTimeout(() => { + this.pendingFilter = null; + this.sendFilterLoad(); + }, 100); + } + + /** + * Resend the bloom filter to peers. + */ + + sendFilterLoad() { + if (!this.options.spv) + return; + + assert(this.spvFilter); + + for (let peer = this.peers.head(); peer; peer = peer.next) + peer.sendFilterLoad(this.spvFilter); + } + + /** + * Add an address to the bloom filter (SPV-only). + * @param {Address|Base58Address} address + */ + + watchAddress(address) { + const hash = Address.getHash(address); + this.watch(hash); + } + + /** + * Add an outpoint to the bloom filter (SPV-only). + * @param {Outpoint} outpoint + */ + + watchOutpoint(outpoint) { + this.watch(outpoint.toRaw()); + } + + /** + * Send `getblocks` to peer after building + * locator and resolving orphan root. + * @method + * @param {Peer} peer + * @param {Hash} orphan - Orphan hash to resolve. + * @returns {Promise} + */ + + async resolveOrphan(peer, orphan) { + const locator = await this.chain.getLocator(); + const root = this.chain.getOrphanRoot(orphan); + + assert(root); + + peer.sendGetBlocks(locator, root); + } + + /** + * Send `getheaders` to peer after building locator. + * @method + * @param {Peer} peer + * @param {Hash} tip - Tip to build chain locator from. + * @param {Hash?} stop + * @returns {Promise} + */ + + async getHeaders(peer, tip, stop) { + const locator = await this.chain.getLocator(tip); + peer.sendGetHeaders(locator, stop); + } + + /** + * Send `getblocks` to peer after building locator. + * @method + * @param {Peer} peer + * @param {Hash} tip - Tip hash to build chain locator from. + * @param {Hash?} stop + * @returns {Promise} + */ + + async getBlocks(peer, tip, stop) { + const locator = await this.chain.getLocator(tip); + peer.sendGetBlocks(locator, stop); + } + + /** + * Queue a `getdata` request to be sent. + * @param {Peer} peer + * @param {Hash[]} hashes + */ + + getBlock(peer, hashes) { + if (!this.opened) + return; + + if (!peer.handshake) + throw new Error('Peer handshake not complete (getdata).'); + + if (peer.destroyed) + throw new Error('Peer is destroyed (getdata).'); + + let now = Date.now(); + const items = []; + + for (const hash of hashes) { + if (this.blockMap.has(hash)) + continue; + + this.blockMap.add(hash); + peer.blockMap.set(hash, now); + + if (this.chain.synced) + now += 100; + + items.push(hash); + } + + if (items.length === 0) + return; + + this.logger.debug( + 'Requesting %d/%d blocks from peer with getdata (%s).', + items.length, + this.blockMap.size, + peer.hostname()); + + peer.getBlock(items); + } + + /** + * Queue a `getdata` request to be sent. + * @param {Peer} peer + * @param {Hash[]} hashes + */ + + getTX(peer, hashes) { + if (!this.opened) + return; + + if (!peer.handshake) + throw new Error('Peer handshake not complete (getdata).'); + + if (peer.destroyed) + throw new Error('Peer is destroyed (getdata).'); + + let now = Date.now(); + + const items = []; + + for (const hash of hashes) { + if (this.txMap.has(hash)) + continue; + + this.txMap.add(hash); + peer.txMap.set(hash, now); + + now += 50; + + items.push(hash); + } + + if (items.length === 0) + return; + + this.logger.debug( + 'Requesting %d/%d txs from peer with getdata (%s).', + items.length, + this.txMap.size, + peer.hostname()); + + peer.getTX(items); + } + + /** + * Test whether the chain has or has seen an item. + * @method + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async hasBlock(hash) { + // Check the lock. + if (this.locker.has(hash)) + return true; + + // Check the chain. + if (await this.chain.has(hash)) + return true; + + return false; + } + + /** + * Test whether the mempool has or has seen an item. + * @param {Hash} hash + * @returns {Boolean} + */ + + hasTX(hash) { + // Check the lock queue. + if (this.locker.has(hash)) + return true; + + if (!this.mempool) { + // Check the TX filter if + // we don't have a mempool. + if (!this.txFilter.added(hash, 'hex')) + return true; + } else { + // Check the mempool. + if (this.mempool.has(hash)) + return true; + + // If we recently rejected this item. Ignore. + if (this.mempool.hasReject(hash)) { + this.logger.spam('Saw known reject of %s.', encoding.revHex(hash)); + return true; + } + } + + return false; + } + + /** + * Queue a `getdata` request to be sent. + * Check tx existence before requesting. + * @param {Peer} peer + * @param {Hash[]} hashes + */ + + ensureTX(peer, hashes) { + const items = []; + + for (const hash of hashes) { + if (this.hasTX(hash)) + continue; + + items.push(hash); + } + + this.getTX(peer, items); + } + + /** + * Fulfill a requested tx. + * @param {Peer} peer + * @param {Hash} hash + * @returns {Boolean} + */ + + resolveTX(peer, hash) { + if (!peer.txMap.has(hash)) + return false; + + peer.txMap.delete(hash); + + assert(this.txMap.has(hash)); + this.txMap.delete(hash); + + return true; + } + + /** + * Fulfill a requested block. + * @param {Peer} peer + * @param {Hash} hash + * @returns {Boolean} + */ + + resolveBlock(peer, hash) { + if (!peer.blockMap.has(hash)) + return false; + + peer.blockMap.delete(hash); + + assert(this.blockMap.has(hash)); + this.blockMap.delete(hash); + + return true; + } + + /** + * Fulfill a requested item. + * @param {Peer} peer + * @param {InvItem} item + * @returns {Boolean} + */ + + resolveItem(peer, item) { + if (item.isBlock()) + return this.resolveBlock(peer, item.hash); + + if (item.isTX()) + return this.resolveTX(peer, item.hash); + + return false; + } + + /** + * Broadcast a transaction or block. + * @param {TX|Block} msg + * @returns {Promise} + */ + + broadcast(msg) { + const hash = msg.hash('hex'); + + let item = this.invMap.get(hash); + + if (item) { + item.refresh(); + item.announce(); + } else { + item = new BroadcastItem(this, msg); + item.start(); + item.announce(); + } + + return new Promise((resolve, reject) => { + item.addJob(resolve, reject); + }); + } + + /** + * Announce a block to all peers. + * @param {Block} tx + */ + + announceBlock(msg) { + for (let peer = this.peers.head(); peer; peer = peer.next) + peer.announceBlock(msg); + } + + /** + * Announce a transaction to all peers. + * @param {TX} tx + */ + + announceTX(msg) { + for (let peer = this.peers.head(); peer; peer = peer.next) + peer.announceTX(msg); + } +} /** * Discovery interval for UPNP and DNS seeds. @@ -131,4276 +3573,833 @@ Object.setPrototypeOf(Pool.prototype, EventEmitter.prototype); Pool.DISCOVERY_INTERVAL = 120000; /** - * Initialize the pool. - * @private + * Pool Options + * @alias module:net.PoolOptions */ -Pool.prototype.init = function init() { - this.server.on('error', (err) => { - this.emit('error', err); - }); +class PoolOptions { + /** + * Create pool options. + * @constructor + */ - this.server.on('connection', (socket) => { - this.handleSocket(socket); - this.emit('connection', socket); - }); + constructor(options) { + this.network = Network.primary; + this.logger = null; + this.chain = null; + this.mempool = null; - this.server.on('listening', () => { - const data = this.server.address(); - this.logger.info( - 'Pool server listening on %s (port=%d).', - data.address, data.port); - this.emit('listening', data); - }); + this.nonces = new NonceList(); - this.chain.on('block', (block, entry) => { - this.emit('block', block, entry); - }); - - this.chain.on('reset', () => { - if (this.checkpoints) - this.resetChain(); - this.forceSync(); - }); - - this.chain.on('full', () => { - this.sync(); - this.emit('full'); - this.logger.info('Chain is fully synced (height=%d).', this.chain.height); - }); - - this.chain.on('bad orphan', (err, id) => { - this.handleBadOrphan('block', err, id); - }); - - if (this.mempool) { - this.mempool.on('tx', (tx) => { - this.emit('tx', tx); - }); - - this.mempool.on('bad orphan', (err, id) => { - this.handleBadOrphan('tx', err, id); - }); - } - - if (!this.options.selfish && !this.options.spv) { - if (this.mempool) { - this.mempool.on('tx', (tx) => { - this.announceTX(tx); - }); - } - - // Normally we would also broadcast - // competing chains, but we want to - // avoid getting banned if an evil - // miner sends us an invalid competing - // chain that we can't connect and - // verify yet. - this.chain.on('block', (block) => { - if (!this.chain.synced) - return; - this.announceBlock(block); - }); - } -}; - -/** - * Open the pool, wait for the chain to load. - * @returns {Promise} - */ - -Pool.prototype.open = async function open() { - assert(!this.opened, 'Pool is already open.'); - this.opened = true; - - this.logger.info('Pool loaded (maxpeers=%d).', this.options.maxOutbound); - - if (this.options.bip150) { - const key = secp256k1.publicKeyCreate(this.options.identityKey, true); - this.logger.info('Identity public key: %s.', key.toString('hex')); - this.logger.info('Identity address: %s.', BIP150.address(key)); - } - - this.resetChain(); -}; - -/** - * Close and destroy the pool. - * @method - * @alias Pool#close - * @returns {Promise} - */ - -Pool.prototype.close = async function close() { - assert(this.opened, 'Pool is not open.'); - this.opened = false; - return this.disconnect(); -}; - -/** - * Reset header chain. - */ - -Pool.prototype.resetChain = function resetChain() { - if (!this.options.checkpoints) - return; - - this.checkpoints = false; - this.headerTip = null; - this.headerChain.reset(); - this.headerNext = null; - - const tip = this.chain.tip; - - if (tip.height < this.network.lastCheckpoint) { + this.prefix = null; this.checkpoints = true; - this.headerTip = this.getNextTip(tip.height); - this.headerChain.push(new HeaderEntry(tip.hash, tip.height)); - this.logger.info( - 'Initialized header chain to height %d (checkpoint=%s).', - tip.height, encoding.revHex(this.headerTip.hash)); - } -}; + this.spv = false; + this.bip37 = false; + this.listen = false; + this.compact = true; + this.noRelay = false; + this.host = '0.0.0.0'; + this.port = this.network.port; + this.publicHost = '0.0.0.0'; + this.publicPort = this.network.port; + this.maxOutbound = 8; + this.maxInbound = 8; + this.createSocket = this._createSocket.bind(this); + this.createServer = tcp.createServer; + this.resolve = this._resolve.bind(this); + this.proxy = null; + this.onion = false; + this.upnp = false; + this.selfish = false; + this.version = common.PROTOCOL_VERSION; + this.agent = common.USER_AGENT; + this.bip151 = false; + this.bip150 = false; + this.authPeers = []; + this.knownPeers = {}; + this.identityKey = secp256k1.generatePrivateKey(); + this.banScore = common.BAN_SCORE; + this.banTime = common.BAN_TIME; + this.feeRate = -1; + this.seeds = this.network.seeds; + this.nodes = []; + this.invTimeout = 60000; + this.blockMode = 0; + this.services = common.LOCAL_SERVICES; + this.requiredServices = common.REQUIRED_SERVICES; + this.persistent = false; -/** - * Connect to the network. - * @method - * @returns {Promise} - */ - -Pool.prototype.connect = async function connect() { - const unlock = await this.locker.lock(); - try { - return await this._connect(); - } finally { - unlock(); - } -}; - -/** - * Connect to the network (no lock). - * @method - * @returns {Promise} - */ - -Pool.prototype._connect = async function _connect() { - assert(this.opened, 'Pool is not opened.'); - - if (this.connected) - return; - - await this.hosts.open(); - await this.authdb.open(); - - await this.discoverGateway(); - await this.discoverExternal(); - await this.discoverSeeds(); - - this.fillOutbound(); - - await this.listen(); - - this.startTimer(); - - this.connected = true; -}; - -/** - * Disconnect from the network. - * @method - * @returns {Promise} - */ - -Pool.prototype.disconnect = async function disconnect() { - const unlock = await this.locker.lock(); - try { - return await this._disconnect(); - } finally { - unlock(); - } -}; - -/** - * Disconnect from the network. - * @method - * @returns {Promise} - */ - -Pool.prototype._disconnect = async function _disconnect() { - if (!this.connected) - return; - - this.disconnecting = true; - - for (const item of this.invMap.values()) - item.resolve(); - - this.peers.destroy(); - - this.blockMap.clear(); - this.txMap.clear(); - - if (this.pendingFilter != null) { - clearTimeout(this.pendingFilter); - this.pendingFilter = null; + this.fromOptions(options); } - if (this.pendingRefill != null) { - clearTimeout(this.pendingRefill); - this.pendingRefill = null; - } + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {PoolOptions} + */ - this.checkpoints = false; - this.headerTip = null; - this.headerChain.reset(); - this.headerNext = null; + fromOptions(options) { + assert(options, 'Pool requires options.'); + assert(options.chain && typeof options.chain === 'object', + 'Pool options require a blockchain.'); - this.stopTimer(); + this.chain = options.chain; + this.network = options.chain.network; + this.logger = options.chain.logger; - await this.authdb.close(); - await this.hosts.close(); + this.port = this.network.port; + this.seeds = this.network.seeds; + this.port = this.network.port; + this.publicPort = this.network.port; - await this.unlisten(); - - this.disconnecting = false; - this.syncing = false; - this.connected = false; -}; - -/** - * Start listening on a server socket. - * @method - * @private - * @returns {Promise} - */ - -Pool.prototype.listen = async function listen() { - assert(this.server); - assert(!this.connected, 'Already listening.'); - - if (!this.options.listen) - return; - - this.server.maxConnections = this.options.maxInbound; - - await this.server.listen(this.options.port, this.options.host); -}; - -/** - * Stop listening on server socket. - * @method - * @private - * @returns {Promise} - */ - -Pool.prototype.unlisten = async function unlisten() { - assert(this.server); - assert(this.connected, 'Not listening.'); - - if (!this.options.listen) - return; - - await this.server.close(); -}; - -/** - * Start discovery timer. - * @private - */ - -Pool.prototype.startTimer = function startTimer() { - assert(this.timer == null, 'Timer already started.'); - this.timer = setInterval(() => this.discover(), Pool.DISCOVERY_INTERVAL); -}; - -/** - * Stop discovery timer. - * @private - */ - -Pool.prototype.stopTimer = function stopTimer() { - assert(this.timer != null, 'Timer already stopped.'); - clearInterval(this.timer); - this.timer = null; -}; - -/** - * Rediscover seeds and internet gateway. - * Attempt to add port mapping once again. - * @returns {Promise} - */ - -Pool.prototype.discover = async function discover() { - if (this.discovering) - return; - - try { - this.discovering = true; - await this.discoverGateway(); - await this.discoverSeeds(true); - } finally { - this.discovering = false; - } -}; - -/** - * Attempt to add port mapping (i.e. - * remote:8333->local:8333) via UPNP. - * @returns {Promise} - */ - -Pool.prototype.discoverGateway = async function discoverGateway() { - const src = this.options.publicPort; - const dest = this.options.port; - - // Pointless if we're not listening. - if (!this.options.listen) - return false; - - // UPNP is always optional, since - // it's likely to not work anyway. - if (!this.options.upnp) - return false; - - let wan; - try { - this.logger.debug('Discovering internet gateway (upnp).'); - wan = await UPNP.discover(); - } catch (e) { - this.logger.debug('Could not discover internet gateway (upnp).'); - this.logger.debug(e); - return false; - } - - let host; - try { - host = await wan.getExternalIP(); - } catch (e) { - this.logger.debug('Could not find external IP (upnp).'); - this.logger.debug(e); - return false; - } - - if (this.hosts.addLocal(host, src, scores.UPNP)) - this.logger.info('External IP found (upnp): %s.', host); - - this.logger.debug( - 'Adding port mapping %d->%d.', - src, dest); - - try { - await wan.addPortMapping(host, src, dest); - } catch (e) { - this.logger.debug('Could not add port mapping (upnp).'); - this.logger.debug(e); - return false; - } - - return true; -}; - -/** - * Attempt to resolve DNS seeds if necessary. - * @param {Boolean} checkPeers - * @returns {Promise} - */ - -Pool.prototype.discoverSeeds = async function discoverSeeds(checkPeers) { - if (this.hosts.dnsSeeds.length === 0) - return; - - const max = Math.min(2, this.options.maxOutbound); - const size = this.hosts.size(); - - let total = 0; - for (let peer = this.peers.head(); peer; peer = peer.next) { - if (!peer.outbound) - continue; - - if (peer.connected) { - if (++total > max) - break; + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; } - } - if (size === 0 || (checkPeers && total < max)) { - this.logger.warning('Could not find enough peers.'); - this.logger.warning('Hitting DNS seeds...'); - - await this.hosts.discoverSeeds(); - - this.logger.info( - 'Resolved %d hosts from DNS seeds.', - this.hosts.size() - size); - - this.refill(); - } -}; - -/** - * Attempt to discover external IP via HTTP. - * @returns {Promise} - */ - -Pool.prototype.discoverExternal = async function discoverExternal() { - const port = this.options.publicPort; - - // Pointless if we're not listening. - if (!this.options.listen) - return; - - // Never hit an HTTP server if - // we're using an outbound proxy. - if (this.options.proxy) - return; - - // Try not to hit this if we can avoid it. - if (this.hosts.local.size > 0) - return; - - let host4; - try { - host4 = await external.getIPv4(); - } catch (e) { - this.logger.debug('Could not find external IPv4 (http).'); - this.logger.debug(e); - } - - if (host4 && this.hosts.addLocal(host4, port, scores.HTTP)) - this.logger.info('External IPv4 found (http): %s.', host4); - - let host6; - try { - host6 = await external.getIPv6(); - } catch (e) { - this.logger.debug('Could not find external IPv6 (http).'); - this.logger.debug(e); - } - - if (host6 && this.hosts.addLocal(host6, port, scores.HTTP)) - this.logger.info('External IPv6 found (http): %s.', host6); -}; - -/** - * Handle incoming connection. - * @private - * @param {net.Socket} socket - */ - -Pool.prototype.handleSocket = function handleSocket(socket) { - if (!socket.remoteAddress) { - this.logger.debug('Ignoring disconnected peer.'); - socket.destroy(); - return; - } - - const ip = IP.normalize(socket.remoteAddress); - - if (this.peers.inbound >= this.options.maxInbound) { - this.logger.debug('Ignoring peer: too many inbound (%s).', ip); - socket.destroy(); - return; - } - - if (this.hosts.isBanned(ip)) { - this.logger.debug('Ignoring banned peer (%s).', ip); - socket.destroy(); - return; - } - - const host = IP.toHostname(ip, socket.remotePort); - - assert(!this.peers.map.has(host), 'Port collision.'); - - this.addInbound(socket); -}; - -/** - * Add a loader peer. Necessary for - * a sync to even begin. - * @private - */ - -Pool.prototype.addLoader = function addLoader() { - if (!this.opened) - return; - - assert(!this.peers.load); - - for (let peer = this.peers.head(); peer; peer = peer.next) { - if (!peer.outbound) - continue; - - this.logger.info( - 'Repurposing peer for loader (%s).', - peer.hostname()); - - this.setLoader(peer); - - return; - } - - const addr = this.getHost(); - - if (!addr) - return; - - const peer = this.createOutbound(addr); - - this.logger.info('Adding loader peer (%s).', peer.hostname()); - - this.peers.add(peer); - - this.setLoader(peer); -}; - -/** - * Add a loader peer. Necessary for - * a sync to even begin. - * @private - */ - -Pool.prototype.setLoader = function setLoader(peer) { - if (!this.opened) - return; - - assert(peer.outbound); - assert(!this.peers.load); - assert(!peer.loader); - - peer.loader = true; - this.peers.load = peer; - - this.sendSync(peer); - - this.emit('loader', peer); -}; - -/** - * Start the blockchain sync. - */ - -Pool.prototype.startSync = function startSync() { - if (!this.opened) - return; - - assert(this.connected, 'Pool is not connected!'); - - this.syncing = true; - this.resync(false); -}; - -/** - * Force sending of a sync to each peer. - */ - -Pool.prototype.forceSync = function forceSync() { - if (!this.opened) - return; - - assert(this.connected, 'Pool is not connected!'); - - this.resync(true); -}; - -/** - * Send a sync to each peer. - */ - -Pool.prototype.sync = function sync(force) { - this.resync(false); -}; - -/** - * Stop the sync. - * @private - */ - -Pool.prototype.stopSync = function stopSync() { - if (!this.syncing) - return; - - this.syncing = false; - - for (let peer = this.peers.head(); peer; peer = peer.next) { - if (!peer.outbound) - continue; - - if (!peer.syncing) - continue; - - peer.syncing = false; - peer.merkleBlock = null; - peer.merkleTime = -1; - peer.merkleMatches = 0; - peer.merkleMap = null; - peer.blockTime = -1; - peer.blockMap.clear(); - peer.compactBlocks.clear(); - } - - this.blockMap.clear(); - this.compactBlocks.clear(); -}; - -/** - * Send a sync to each peer. - * @private - * @param {Boolean?} force - * @returns {Promise} - */ - -Pool.prototype.resync = async function resync(force) { - if (!this.syncing) - return; - - let locator; - try { - locator = await this.chain.getLocator(); - } catch (e) { - this.emit('error', e); - return; - } - - for (let peer = this.peers.head(); peer; peer = peer.next) { - if (!peer.outbound) - continue; - - if (!force && peer.syncing) - continue; - - this.sendLocator(locator, peer); - } -}; - -/** - * Test whether a peer is sync-worthy. - * @param {Peer} peer - * @returns {Boolean} - */ - -Pool.prototype.isSyncable = function isSyncable(peer) { - if (!this.syncing) - return false; - - if (peer.destroyed) - return false; - - if (!peer.handshake) - return false; - - if (!(peer.services & services.NETWORK)) - return false; - - if (this.options.hasWitness() && !peer.hasWitness()) - return false; - - if (!peer.loader) { - if (!this.chain.synced) - return false; - } - - return true; -}; - -/** - * Start syncing from peer. - * @method - * @param {Peer} peer - * @returns {Promise} - */ - -Pool.prototype.sendSync = async function sendSync(peer) { - if (peer.syncing) - return false; - - if (!this.isSyncable(peer)) - return false; - - peer.syncing = true; - peer.blockTime = Date.now(); - - let locator; - try { - locator = await this.chain.getLocator(); - } catch (e) { - peer.syncing = false; - peer.blockTime = -1; - this.emit('error', e); - return false; - } - - return this.sendLocator(locator, peer); -}; - -/** - * Send a chain locator and start syncing from peer. - * @method - * @param {Hash[]} locator - * @param {Peer} peer - * @returns {Boolean} - */ - -Pool.prototype.sendLocator = function sendLocator(locator, peer) { - if (!this.isSyncable(peer)) - return false; - - // Ask for the mempool if we're synced. - if (this.network.requestMempool) { - if (peer.loader && this.chain.synced) - peer.sendMempool(); - } - - peer.syncing = true; - peer.blockTime = Date.now(); - - if (this.checkpoints) { - peer.sendGetHeaders(locator, this.headerTip.hash); - return true; - } - - peer.sendGetBlocks(locator); - - return true; -}; - -/** - * Send `mempool` to all peers. - */ - -Pool.prototype.sendMempool = function sendMempool() { - for (let peer = this.peers.head(); peer; peer = peer.next) - peer.sendMempool(); -}; - -/** - * Send `getaddr` to all peers. - */ - -Pool.prototype.sendGetAddr = function sendGetAddr() { - for (let peer = this.peers.head(); peer; peer = peer.next) - peer.sendGetAddr(); -}; - -/** - * Request current header chain blocks. - * @private - * @param {Peer} peer - */ - -Pool.prototype.resolveHeaders = function resolveHeaders(peer) { - const items = []; - - for (let node = this.headerNext; node; node = node.next) { - this.headerNext = node.next; - - items.push(node.hash); - - if (items.length === 50000) - break; - } - - this.getBlock(peer, items); -}; - -/** - * Update all peer heights by their best hash. - * @param {Hash} hash - * @param {Number} height - */ - -Pool.prototype.resolveHeight = function resolveHeight(hash, height) { - let total = 0; - - for (let peer = this.peers.head(); peer; peer = peer.next) { - if (peer.bestHash !== hash) - continue; - - if (peer.bestHeight !== height) { - peer.bestHeight = height; - total++; + if (options.mempool != null) { + assert(typeof options.mempool === 'object'); + this.mempool = options.mempool; } - } - if (total > 0) - this.logger.debug('Resolved height for %d peers.', total); -}; - -/** - * Find the next checkpoint. - * @private - * @param {Number} height - * @returns {Object} - */ - -Pool.prototype.getNextTip = function getNextTip(height) { - for (const next of this.network.checkpoints) { - if (next.height > height) - return new HeaderEntry(next.hash, next.height); - } - - throw new Error('Next checkpoint not found.'); -}; - -/** - * Announce broadcast list to peer. - * @param {Peer} peer - */ - -Pool.prototype.announceList = function announceList(peer) { - const blocks = []; - const txs = []; - - for (const item of this.invMap.values()) { - switch (item.type) { - case invTypes.BLOCK: - blocks.push(item.msg); - break; - case invTypes.TX: - txs.push(item.msg); - break; - default: - assert(false, 'Bad item type.'); - break; + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; } + + if (options.checkpoints != null) { + assert(typeof options.checkpoints === 'boolean'); + assert(options.checkpoints === this.chain.options.checkpoints); + this.checkpoints = options.checkpoints; + } else { + this.checkpoints = this.chain.options.checkpoints; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + assert(options.spv === this.chain.options.spv); + this.spv = options.spv; + } else { + this.spv = this.chain.options.spv; + } + + if (options.bip37 != null) { + assert(typeof options.bip37 === 'boolean'); + this.bip37 = options.bip37; + } + + if (options.listen != null) { + assert(typeof options.listen === 'boolean'); + this.listen = options.listen; + } + + if (options.compact != null) { + assert(typeof options.compact === 'boolean'); + this.compact = options.compact; + } + + if (options.noRelay != null) { + assert(typeof options.noRelay === 'boolean'); + this.noRelay = options.noRelay; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + const raw = IP.toBuffer(options.host); + this.host = IP.toString(raw); + if (IP.isRoutable(raw)) + this.publicHost = this.host; + } + + if (options.port != null) { + assert((options.port & 0xffff) === options.port); + this.port = options.port; + this.publicPort = options.port; + } + + if (options.publicHost != null) { + assert(typeof options.publicHost === 'string'); + this.publicHost = IP.normalize(options.publicHost); + } + + if (options.publicPort != null) { + assert((options.publicPort & 0xff) === options.publicPort); + this.publicPort = options.publicPort; + } + + if (options.maxOutbound != null) { + assert(typeof options.maxOutbound === 'number'); + assert(options.maxOutbound > 0); + this.maxOutbound = options.maxOutbound; + } + + if (options.maxInbound != null) { + assert(typeof options.maxInbound === 'number'); + this.maxInbound = options.maxInbound; + } + + if (options.createSocket) { + assert(typeof options.createSocket === 'function'); + this.createSocket = options.createSocket; + } + + if (options.createServer) { + assert(typeof options.createServer === 'function'); + this.createServer = options.createServer; + } + + if (options.resolve) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.proxy) { + assert(typeof options.proxy === 'string'); + this.proxy = options.proxy; + } + + if (options.onion != null) { + assert(typeof options.onion === 'boolean'); + this.onion = options.onion; + } + + if (options.upnp != null) { + assert(typeof options.upnp === 'boolean'); + this.upnp = options.upnp; + } + + if (options.selfish) { + assert(typeof options.selfish === 'boolean'); + this.selfish = options.selfish; + } + + if (options.version) { + assert(typeof options.version === 'number'); + this.version = options.version; + } + + if (options.agent) { + assert(typeof options.agent === 'string'); + assert(options.agent.length <= 255); + this.agent = options.agent; + } + + if (options.bip151 != null) { + assert(typeof options.bip151 === 'boolean'); + this.bip151 = options.bip151; + } + + if (options.bip150 != null) { + assert(typeof options.bip150 === 'boolean'); + assert(!options.bip150 || this.bip151, + 'Cannot enable bip150 without bip151.'); + + if (options.knownPeers) { + assert(typeof options.knownPeers === 'object'); + assert(!Array.isArray(options.knownPeers)); + this.knownPeers = options.knownPeers; + } + + if (options.authPeers) { + assert(Array.isArray(options.authPeers)); + this.authPeers = options.authPeers; + } + + if (options.identityKey) { + assert(Buffer.isBuffer(options.identityKey), + 'Identity key must be a buffer.'); + assert(secp256k1.privateKeyVerify(options.identityKey), + 'Invalid identity key.'); + this.identityKey = options.identityKey; + } + } + + if (options.banScore != null) { + assert(typeof this.options.banScore === 'number'); + this.banScore = this.options.banScore; + } + + if (options.banTime != null) { + assert(typeof this.options.banTime === 'number'); + this.banTime = this.options.banTime; + } + + if (options.feeRate != null) { + assert(typeof this.options.feeRate === 'number'); + this.feeRate = this.options.feeRate; + } + + if (options.seeds) { + assert(Array.isArray(options.seeds)); + this.seeds = options.seeds; + } + + if (options.nodes) { + assert(Array.isArray(options.nodes)); + this.nodes = options.nodes; + } + + if (options.only != null) { + assert(Array.isArray(options.only)); + if (options.only.length > 0) { + this.nodes = options.only; + this.maxOutbound = options.only.length; + } + } + + if (options.invTimeout != null) { + assert(typeof options.invTimeout === 'number'); + this.invTimeout = options.invTimeout; + } + + if (options.blockMode != null) { + assert(typeof options.blockMode === 'number'); + this.blockMode = options.blockMode; + } + + if (options.persistent != null) { + assert(typeof options.persistent === 'boolean'); + this.persistent = options.persistent; + } + + if (this.spv) { + this.requiredServices |= common.services.BLOOM; + this.services &= ~common.services.NETWORK; + this.noRelay = true; + this.checkpoints = true; + this.compact = false; + this.bip37 = false; + this.listen = false; + } + + if (this.selfish) { + this.services &= ~common.services.NETWORK; + this.bip37 = false; + } + + if (this.bip37) + this.services |= common.services.BLOOM; + + if (this.proxy) + this.listen = false; + + if (options.services != null) { + assert((options.services >>> 0) === options.services); + this.services = options.services; + } + + if (options.requiredServices != null) { + assert((options.requiredServices >>> 0) === options.requiredServices); + this.requiredServices = options.requiredServices; + } + + return this; } - if (blocks.length > 0) - peer.announceBlock(blocks); + /** + * Instantiate options from object. + * @param {Object} options + * @returns {PoolOptions} + */ - if (txs.length > 0) - peer.announceTX(txs); -}; - -/** - * Get a block/tx from the broadcast map. - * @private - * @param {Peer} peer - * @param {InvItem} item - * @returns {Promise} - */ - -Pool.prototype.getBroadcasted = function getBroadcasted(peer, item) { - const type = item.isTX() ? invTypes.TX : invTypes.BLOCK; - const entry = this.invMap.get(item.hash); - - if (!entry) - return null; - - if (type !== entry.type) { - this.logger.debug( - 'Peer requested item with the wrong type (%s).', - peer.hostname()); - return null; + static fromOptions(options) { + return new PoolOptions().fromOptions(options); } - this.logger.debug( - 'Peer requested %s %s as a %s packet (%s).', - item.isTX() ? 'tx' : 'block', - item.rhash(), - item.hasWitness() ? 'witness' : 'normal', - peer.hostname()); + /** + * Get the chain height. + * @private + * @returns {Number} + */ - entry.handleAck(peer); + getHeight() { + return this.chain.height; + } - return entry.msg; -}; + /** + * Test whether the chain is synced. + * @private + * @returns {Boolean} + */ -/** - * Get a block/tx either from the broadcast map, mempool, or blockchain. - * @method - * @private - * @param {Peer} peer - * @param {InvItem} item - * @returns {Promise} - */ + isFull() { + return this.chain.synced; + } -Pool.prototype.getItem = async function getItem(peer, item) { - const entry = this.getBroadcasted(peer, item); + /** + * Get required services for outbound peers. + * @private + * @returns {Number} + */ - if (entry) - return entry; + getRequiredServices() { + let services = this.requiredServices; + if (this.hasWitness()) + services |= common.services.WITNESS; + return services; + } - if (this.options.selfish) - return null; + /** + * Whether segwit is enabled. + * @private + * @returns {Boolean} + */ - if (item.isTX()) { + hasWitness() { + return this.chain.state.hasWitness(); + } + + /** + * Create a version packet nonce. + * @private + * @param {String} hostname + * @returns {Buffer} + */ + + createNonce(hostname) { + return this.nonces.alloc(hostname); + } + + /** + * Test whether version nonce is ours. + * @private + * @param {Buffer} nonce + * @returns {Boolean} + */ + + hasNonce(nonce) { + return this.nonces.has(nonce); + } + + /** + * Get fee rate for txid. + * @private + * @param {Hash} hash + * @returns {Rate} + */ + + getRate(hash) { if (!this.mempool) - return null; - return this.mempool.getTX(item.hash); - } + return -1; - if (this.chain.options.spv) - return null; - - if (this.chain.options.prune) - return null; - - return await this.chain.getBlock(item.hash); -}; - -/** - * Send a block from the broadcast list or chain. - * @method - * @private - * @param {Peer} peer - * @param {InvItem} item - * @returns {Boolean} - */ - -Pool.prototype.sendBlock = async function sendBlock(peer, item, witness) { - const broadcasted = this.getBroadcasted(peer, item); - - // Check for a broadcasted item first. - if (broadcasted) { - peer.send(new packets.BlockPacket(broadcasted, witness)); - return true; - } - - if (this.options.selfish - || this.chain.options.spv - || this.chain.options.prune) { - return false; - } - - // If we have the same serialization, we - // can write the raw binary to the socket. - if (witness || !this.options.hasWitness()) { - const block = await this.chain.getRawBlock(item.hash); - - if (block) { - peer.sendRaw('block', block); - return true; - } - - return false; - } - - const block = await this.chain.getBlock(item.hash); - - if (block) { - peer.send(new packets.BlockPacket(block, witness)); - return true; - } - - return false; -}; - -/** - * Create an outbound peer with no special purpose. - * @private - * @param {NetAddress} addr - * @returns {Peer} - */ - -Pool.prototype.createOutbound = function createOutbound(addr) { - const cipher = BIP151.ciphers.CHACHAPOLY; - const identity = this.options.identityKey; - const peer = Peer.fromOutbound(this.options, addr); - - this.hosts.markAttempt(addr.hostname); - - if (this.options.bip151) - peer.setCipher(cipher); - - if (this.options.bip150) - peer.setAuth(this.authdb, identity); - - this.bindPeer(peer); - - this.logger.debug('Connecting to %s.', peer.hostname()); - - peer.tryOpen(); - - return peer; -}; - -/** - * Accept an inbound socket. - * @private - * @param {net.Socket} socket - * @returns {Peer} - */ - -Pool.prototype.createInbound = function createInbound(socket) { - const cipher = BIP151.ciphers.CHACHAPOLY; - const identity = this.options.identityKey; - const peer = Peer.fromInbound(this.options, socket); - - if (this.options.bip151) - peer.setCipher(cipher); - - if (this.options.bip150) - peer.setAuth(this.authdb, identity); - - this.bindPeer(peer); - - peer.tryOpen(); - - return peer; -}; - -/** - * Allocate new peer id. - * @returns {Number} - */ - -Pool.prototype.uid = function uid() { - const MAX = Number.MAX_SAFE_INTEGER; - - if (this.id >= MAX - this.peers.size() - 1) - this.id = 0; - - // Once we overflow, there's a chance - // of collisions. Unlikely to happen - // unless we have tried to connect 9 - // quadrillion times, but still - // account for it. - do { - this.id += 1; - } while (this.peers.find(this.id)); - - return this.id; -}; - -/** - * Bind to peer events. - * @private - * @param {Peer} peer - */ - -Pool.prototype.bindPeer = function bindPeer(peer) { - peer.id = this.uid(); - - peer.onPacket = (packet) => { - return this.handlePacket(peer, packet); - }; - - peer.on('error', (err) => { - this.logger.debug(err); - }); - - peer.once('connect', () => { - this.handleConnect(peer); - }); - - peer.once('open', () => { - this.handleOpen(peer); - }); - - peer.once('close', (connected) => { - this.handleClose(peer, connected); - }); - - peer.once('ban', () => { - this.handleBan(peer); - }); -}; - -/** - * Handle peer packet event. - * @method - * @private - * @param {Peer} peer - * @param {Packet} packet - * @returns {Promise} - */ - -Pool.prototype.handlePacket = async function handlePacket(peer, packet) { - switch (packet.type) { - case packetTypes.VERSION: - await this.handleVersion(peer, packet); - break; - case packetTypes.VERACK: - await this.handleVerack(peer, packet); - break; - case packetTypes.PING: - await this.handlePing(peer, packet); - break; - case packetTypes.PONG: - await this.handlePong(peer, packet); - break; - case packetTypes.GETADDR: - await this.handleGetAddr(peer, packet); - break; - case packetTypes.ADDR: - await this.handleAddr(peer, packet); - break; - case packetTypes.INV: - await this.handleInv(peer, packet); - break; - case packetTypes.GETDATA: - await this.handleGetData(peer, packet); - break; - case packetTypes.NOTFOUND: - await this.handleNotFound(peer, packet); - break; - case packetTypes.GETBLOCKS: - await this.handleGetBlocks(peer, packet); - break; - case packetTypes.GETHEADERS: - await this.handleGetHeaders(peer, packet); - break; - case packetTypes.HEADERS: - await this.handleHeaders(peer, packet); - break; - case packetTypes.SENDHEADERS: - await this.handleSendHeaders(peer, packet); - break; - case packetTypes.BLOCK: - await this.handleBlock(peer, packet); - break; - case packetTypes.TX: - await this.handleTX(peer, packet); - break; - case packetTypes.REJECT: - await this.handleReject(peer, packet); - break; - case packetTypes.MEMPOOL: - await this.handleMempool(peer, packet); - break; - case packetTypes.FILTERLOAD: - await this.handleFilterLoad(peer, packet); - break; - case packetTypes.FILTERADD: - await this.handleFilterAdd(peer, packet); - break; - case packetTypes.FILTERCLEAR: - await this.handleFilterClear(peer, packet); - break; - case packetTypes.MERKLEBLOCK: - await this.handleMerkleBlock(peer, packet); - break; - case packetTypes.FEEFILTER: - await this.handleFeeFilter(peer, packet); - break; - case packetTypes.SENDCMPCT: - await this.handleSendCmpct(peer, packet); - break; - case packetTypes.CMPCTBLOCK: - await this.handleCmpctBlock(peer, packet); - break; - case packetTypes.GETBLOCKTXN: - await this.handleGetBlockTxn(peer, packet); - break; - case packetTypes.BLOCKTXN: - await this.handleBlockTxn(peer, packet); - break; - case packetTypes.ENCINIT: - await this.handleEncinit(peer, packet); - break; - case packetTypes.ENCACK: - await this.handleEncack(peer, packet); - break; - case packetTypes.AUTHCHALLENGE: - await this.handleAuthChallenge(peer, packet); - break; - case packetTypes.AUTHREPLY: - await this.handleAuthReply(peer, packet); - break; - case packetTypes.AUTHPROPOSE: - await this.handleAuthPropose(peer, packet); - break; - case packetTypes.UNKNOWN: - await this.handleUnknown(peer, packet); - break; - default: - assert(false, 'Bad packet type.'); - break; - } - - this.emit('packet', packet, peer); -}; - -/** - * Handle peer connect event. - * @method - * @private - * @param {Peer} peer - */ - -Pool.prototype.handleConnect = async function handleConnect(peer) { - this.logger.info('Connected to %s.', peer.hostname()); - - if (peer.outbound) - this.hosts.markSuccess(peer.hostname()); - - this.emit('peer connect', peer); -}; - -/** - * Handle peer open event. - * @method - * @private - * @param {Peer} peer - */ - -Pool.prototype.handleOpen = async function handleOpen(peer) { - // Advertise our address. - if (!this.options.selfish && this.options.listen) { - const addr = this.hosts.getLocal(peer.address); - if (addr) - peer.send(new packets.AddrPacket([addr])); - } - - // We want compact blocks! - if (this.options.compact) - peer.sendCompact(this.options.blockMode); - - // Find some more peers. - if (!this.hosts.isFull()) - peer.sendGetAddr(); - - // Relay our spv filter if we have one. - if (this.spvFilter) - peer.sendFilterLoad(this.spvFilter); - - // Announce our currently broadcasted items. - this.announceList(peer); - - // Set a fee rate filter. - if (this.options.feeRate !== -1) - peer.sendFeeRate(this.options.feeRate); - - // Start syncing the chain. - if (peer.outbound) - this.sendSync(peer); - - if (peer.outbound) { - this.hosts.markAck(peer.hostname(), peer.services); - - // If we don't have an ack'd - // loader yet consider it dead. - if (!peer.loader) { - if (this.peers.load && !this.peers.load.handshake) { - assert(this.peers.load.loader); - this.peers.load.loader = false; - this.peers.load = null; - } - } - - // If we do not have a loader, - // use this peer. - if (!this.peers.load) - this.setLoader(peer); - } - - this.emit('peer open', peer); -}; - -/** - * Handle peer close event. - * @method - * @private - * @param {Peer} peer - * @param {Boolean} connected - */ - -Pool.prototype.handleClose = async function handleClose(peer, connected) { - const outbound = peer.outbound; - const loader = peer.loader; - const size = peer.blockMap.size; - - this.removePeer(peer); - - if (loader) { - this.logger.info('Removed loader peer (%s).', peer.hostname()); - if (this.checkpoints) - this.resetChain(); - } - - this.nonces.remove(peer.hostname()); - - this.emit('peer close', peer, connected); - - if (!this.opened) - return; - - if (this.disconnecting) - return; - - if (this.chain.synced && size > 0) { - this.logger.warning('Peer disconnected with requested blocks.'); - this.logger.warning('Resending sync...'); - this.forceSync(); - } - - if (!outbound) - return; - - this.refill(); -}; - -/** - * Handle ban event. - * @method - * @private - * @param {Peer} peer - */ - -Pool.prototype.handleBan = async function handleBan(peer) { - this.ban(peer.address); - this.emit('ban', peer); -}; - -/** - * Handle peer version event. - * @method - * @private - * @param {Peer} peer - * @param {VersionPacket} packet - */ - -Pool.prototype.handleVersion = async function handleVersion(peer, packet) { - this.logger.info( - 'Received version (%s): version=%d height=%d services=%s agent=%s', - peer.hostname(), - packet.version, - packet.height, - packet.services.toString(2), - packet.agent); - - this.network.time.add(peer.hostname(), packet.time); - this.nonces.remove(peer.hostname()); - - if (!peer.outbound && packet.remote.isRoutable()) - this.hosts.markLocal(packet.remote); -}; - -/** - * Handle `verack` packet. - * @method - * @private - * @param {Peer} peer - * @param {VerackPacket} packet - */ - -Pool.prototype.handleVerack = async function handleVerack(peer, packet) { - ; -}; - -/** - * Handle `ping` packet. - * @method - * @private - * @param {Peer} peer - * @param {PingPacket} packet - */ - -Pool.prototype.handlePing = async function handlePing(peer, packet) { - ; -}; - -/** - * Handle `pong` packet. - * @method - * @private - * @param {Peer} peer - * @param {PongPacket} packet - */ - -Pool.prototype.handlePong = async function handlePong(peer, packet) { - ; -}; - -/** - * Handle `getaddr` packet. - * @method - * @private - * @param {Peer} peer - * @param {GetAddrPacket} packet - */ - -Pool.prototype.handleGetAddr = async function handleGetAddr(peer, packet) { - if (this.options.selfish) - return; - - if (peer.sentAddr) { - this.logger.debug( - 'Ignoring repeated getaddr (%s).', - peer.hostname()); - return; - } - - peer.sentAddr = true; - - const addrs = this.hosts.toArray(); - const items = []; - - for (const addr of addrs) { - if (!peer.addrFilter.added(addr.hostname, 'ascii')) - continue; - - items.push(addr); - - if (items.length === 1000) - break; - } - - if (items.length === 0) - return; - - this.logger.debug( - 'Sending %d addrs to peer (%s)', - items.length, - peer.hostname()); - - peer.send(new packets.AddrPacket(items)); -}; - -/** - * Handle peer addr event. - * @method - * @private - * @param {Peer} peer - * @param {AddrPacket} packet - */ - -Pool.prototype.handleAddr = async function handleAddr(peer, packet) { - const addrs = packet.items; - const now = this.network.now(); - const services = this.options.getRequiredServices(); - - for (const addr of addrs) { - peer.addrFilter.add(addr.hostname, 'ascii'); - - if (!addr.isRoutable()) - continue; - - if (!addr.hasServices(services)) - continue; - - if (addr.time <= 100000000 || addr.time > now + 10 * 60) - addr.time = now - 5 * 24 * 60 * 60; - - if (addr.port === 0) - continue; - - this.hosts.add(addr, peer.address); - } - - this.logger.info( - 'Received %d addrs (hosts=%d, peers=%d) (%s).', - addrs.length, - this.hosts.size(), - this.peers.size(), - peer.hostname()); - - this.fillOutbound(); -}; - -/** - * Handle `inv` packet. - * @method - * @private - * @param {Peer} peer - * @param {InvPacket} packet - */ - -Pool.prototype.handleInv = async function handleInv(peer, packet) { - const unlock = await this.locker.lock(); - try { - return await this._handleInv(peer, packet); - } finally { - unlock(); - } -}; - -/** - * Handle `inv` packet (without a lock). - * @method - * @private - * @param {Peer} peer - * @param {InvPacket} packet - */ - -Pool.prototype._handleInv = async function _handleInv(peer, packet) { - const items = packet.items; - - if (items.length > 50000) { - peer.increaseBan(100); - return; - } - - const blocks = []; - const txs = []; - let unknown = -1; - - for (const item of items) { - switch (item.type) { - case invTypes.BLOCK: - blocks.push(item.hash); - break; - case invTypes.TX: - txs.push(item.hash); - break; - default: - unknown = item.type; - continue; - } - peer.invFilter.add(item.hash, 'hex'); - } - - this.logger.spam( - 'Received inv packet with %d items: blocks=%d txs=%d (%s).', - items.length, blocks.length, txs.length, peer.hostname()); - - if (unknown !== -1) { - this.logger.warning( - 'Peer sent an unknown inv type: %d (%s).', - unknown, peer.hostname()); - } - - if (blocks.length > 0) - await this.handleBlockInv(peer, blocks); - - if (txs.length > 0) - await this.handleTXInv(peer, txs); -}; - -/** - * Handle `inv` packet from peer (containing only BLOCK types). - * @method - * @private - * @param {Peer} peer - * @param {Hash[]} hashes - * @returns {Promise} - */ - -Pool.prototype.handleBlockInv = async function handleBlockInv(peer, hashes) { - assert(hashes.length > 0); - - if (!this.syncing) - return; - - // Always keep track of the peer's best hash. - if (!peer.loader || this.chain.synced) { - const hash = hashes[hashes.length - 1]; - peer.bestHash = hash; - } - - // Ignore for now if we're still syncing - if (!this.chain.synced && !peer.loader) - return; - - if (this.options.hasWitness() && !peer.hasWitness()) - return; - - // Request headers instead. - if (this.checkpoints) - return; - - this.logger.debug( - 'Received %s block hashes from peer (%s).', - hashes.length, - peer.hostname()); - - const items = []; - - let exists = null; - - for (let i = 0; i < hashes.length; i++) { - const hash = hashes[i]; - - // Resolve orphan chain. - if (this.chain.hasOrphan(hash)) { - this.logger.debug('Received known orphan hash (%s).', peer.hostname()); - await this.resolveOrphan(peer, hash); - continue; - } - - // Request the block if we don't have it. - if (!await this.hasBlock(hash)) { - items.push(hash); - continue; - } - - exists = hash; - - // Normally we request the hashContinue. - // In the odd case where we already have - // it, we can do one of two things: either - // force re-downloading of the block to - // continue the sync, or do a getblocks - // from the last hash (this will reset - // the hashContinue on the remote node). - if (i === hashes.length - 1) { - this.logger.debug('Received existing hash (%s).', peer.hostname()); - await this.getBlocks(peer, hash); - } - } - - // Attempt to update the peer's best height - // with the last existing hash we know of. - if (exists && this.chain.synced) { - const height = await this.chain.getHeight(exists); - if (height !== -1) - peer.bestHeight = height; - } - - this.getBlock(peer, items); -}; - -/** - * Handle peer inv packet (txs). - * @method - * @private - * @param {Peer} peer - * @param {Hash[]} hashes - */ - -Pool.prototype.handleTXInv = async function handleTXInv(peer, hashes) { - assert(hashes.length > 0); - - if (this.syncing && !this.chain.synced) - return; - - this.ensureTX(peer, hashes); -}; - -/** - * Handle `getdata` packet. - * @method - * @private - * @param {Peer} peer - * @param {GetDataPacket} packet - */ - -Pool.prototype.handleGetData = async function handleGetData(peer, packet) { - const items = packet.items; - - if (items.length > 50000) { - this.logger.warning('Peer sent inv with >50k items (%s).', peer.hostname()); - peer.increaseBan(100); - peer.destroy(); - return; - } - - const notFound = []; - - let txs = 0; - let blocks = 0; - let compact = 0; - let unknown = -1; - - for (const item of items) { - if (item.isTX()) { - const tx = await this.getItem(peer, item); - - if (!tx) { - notFound.push(item); - continue; - } - - // Coinbases are an insta-ban from any node. - // This should technically never happen, but - // it's worth keeping here just in case. A - // 24-hour ban from any node is rough. - if (tx.isCoinbase()) { - notFound.push(item); - this.logger.warning('Failsafe: tried to relay a coinbase.'); - continue; - } - - peer.send(new packets.TXPacket(tx, item.hasWitness())); - - txs++; - - continue; - } - - switch (item.type) { - case invTypes.BLOCK: - case invTypes.WITNESS_BLOCK: { - const result = await this.sendBlock(peer, item, item.hasWitness()); - if (!result) { - notFound.push(item); - continue; - } - blocks++; - break; - } - case invTypes.FILTERED_BLOCK: - case invTypes.WITNESS_FILTERED_BLOCK: { - if (!this.options.bip37) { - this.logger.debug( - 'Peer requested a merkleblock without bip37 enabled (%s).', - peer.hostname()); - peer.destroy(); - return; - } - - if (!peer.spvFilter) { - notFound.push(item); - continue; - } - - const block = await this.getItem(peer, item); - - if (!block) { - notFound.push(item); - continue; - } - - const merkle = block.toMerkle(peer.spvFilter); - - peer.send(new packets.MerkleBlockPacket(merkle)); - - for (const tx of merkle.txs) { - peer.send(new packets.TXPacket(tx, item.hasWitness())); - txs++; - } - - blocks++; - - break; - } - case invTypes.CMPCT_BLOCK: { - const height = await this.chain.getHeight(item.hash); - - // Fallback to full block. - if (height < this.chain.tip.height - 10) { - const result = await this.sendBlock(peer, item, peer.compactWitness); - if (!result) { - notFound.push(item); - continue; - } - blocks++; - break; - } - - const block = await this.getItem(peer, item); - - if (!block) { - notFound.push(item); - continue; - } - - peer.sendCompactBlock(block); - - blocks++; - compact++; - - break; - } - default: { - unknown = item.type; - notFound.push(item); - continue; - } - } - - if (item.hash === peer.hashContinue) { - peer.sendInv([new InvItem(invTypes.BLOCK, this.chain.tip.hash)]); - peer.hashContinue = null; - } - - // Wait for the peer to read - // before we pull more data - // out of the database. - await peer.drain(); - } - - if (notFound.length > 0) - peer.send(new packets.NotFoundPacket(notFound)); - - if (txs > 0) { - this.logger.debug( - 'Served %d txs with getdata (notfound=%d) (%s).', - txs, notFound.length, peer.hostname()); - } - - if (blocks > 0) { - this.logger.debug( - 'Served %d blocks with getdata (notfound=%d, cmpct=%d) (%s).', - blocks, notFound.length, compact, peer.hostname()); - } - - if (unknown !== -1) { - this.logger.warning( - 'Peer sent an unknown getdata type: %s (%d).', - unknown, peer.hostname()); - } -}; - -/** - * Handle peer notfound packet. - * @method - * @private - * @param {Peer} peer - * @param {NotFoundPacket} packet - */ - -Pool.prototype.handleNotFound = async function handleNotFound(peer, packet) { - const items = packet.items; - - for (const item of items) { - if (!this.resolveItem(peer, item)) { - this.logger.warning( - 'Peer sent notfound for unrequested item: %s (%s).', - item.hash, peer.hostname()); - peer.destroy(); - return; - } - } -}; - -/** - * Handle `getblocks` packet. - * @method - * @private - * @param {Peer} peer - * @param {GetBlocksPacket} packet - */ - -Pool.prototype.handleGetBlocks = async function handleGetBlocks(peer, packet) { - if (!this.chain.synced) - return; - - if (this.options.selfish) - return; - - if (this.chain.options.spv) - return; - - if (this.chain.options.prune) - return; - - let hash = await this.chain.findLocator(packet.locator); - - if (hash) - hash = await this.chain.getNextHash(hash); - - const blocks = []; - - while (hash) { - blocks.push(new InvItem(invTypes.BLOCK, hash)); - - if (hash === packet.stop) - break; - - if (blocks.length === 500) { - peer.hashContinue = hash; - break; - } - - hash = await this.chain.getNextHash(hash); - } - - peer.sendInv(blocks); -}; - -/** - * Handle `getheaders` packet. - * @method - * @private - * @param {Peer} peer - * @param {GetHeadersPacket} packet - */ - -Pool.prototype.handleGetHeaders = async function handleGetHeaders(peer, packet) { - if (!this.chain.synced) - return; - - if (this.options.selfish) - return; - - if (this.chain.options.spv) - return; - - if (this.chain.options.prune) - return; - - let hash; - if (packet.locator.length > 0) { - hash = await this.chain.findLocator(packet.locator); - if (hash) - hash = await this.chain.getNextHash(hash); - } else { - hash = packet.stop; - } - - let entry; - if (hash) - entry = await this.chain.getEntry(hash); - - const headers = []; - - while (entry) { - headers.push(entry.toHeaders()); - - if (entry.hash === packet.stop) - break; - - if (headers.length === 2000) - break; - - entry = await this.chain.getNext(entry); - } - - peer.sendHeaders(headers); -}; - -/** - * Handle `headers` packet from a given peer. - * @method - * @private - * @param {Peer} peer - * @param {HeadersPacket} packet - * @returns {Promise} - */ - -Pool.prototype.handleHeaders = async function handleHeaders(peer, packet) { - const unlock = await this.locker.lock(); - try { - return await this._handleHeaders(peer, packet); - } finally { - unlock(); - } -}; - -/** - * Handle `headers` packet from - * a given peer without a lock. - * @method - * @private - * @param {Peer} peer - * @param {HeadersPacket} packet - * @returns {Promise} - */ - -Pool.prototype._handleHeaders = async function _handleHeaders(peer, packet) { - const headers = packet.items; - - if (!this.checkpoints) - return; - - if (!this.syncing) - return; - - if (!peer.loader) - return; - - if (headers.length === 0) - return; - - if (headers.length > 2000) { - peer.increaseBan(100); - return; - } - - assert(this.headerChain.size > 0); - - let checkpoint = false; - let node = null; - - for (const header of headers) { - const last = this.headerChain.tail; - const hash = header.hash('hex'); - const height = last.height + 1; - - if (!header.verify()) { - this.logger.warning( - 'Peer sent an invalid header (%s).', - peer.hostname()); - peer.increaseBan(100); - peer.destroy(); - return; - } - - if (header.prevBlock !== last.hash) { - this.logger.warning( - 'Peer sent a bad header chain (%s).', - peer.hostname()); - peer.destroy(); - return; - } - - node = new HeaderEntry(hash, height); - - if (node.height === this.headerTip.height) { - if (node.hash !== this.headerTip.hash) { - this.logger.warning( - 'Peer sent an invalid checkpoint (%s).', - peer.hostname()); - peer.destroy(); - return; - } - checkpoint = true; - } - - if (!this.headerNext) - this.headerNext = node; - - this.headerChain.push(node); - } - - this.logger.debug( - 'Received %s headers from peer (%s).', - headers.length, - peer.hostname()); - - // If we received a valid header - // chain, consider this a "block". - peer.blockTime = Date.now(); - - // Request the blocks we just added. - if (checkpoint) { - this.headerChain.shift(); - this.resolveHeaders(peer); - return; - } - - // Request more headers. - peer.sendGetHeaders([node.hash], this.headerTip.hash); -}; - -/** - * Handle `sendheaders` packet. - * @method - * @private - * @param {Peer} peer - * @param {SendHeadersPacket} packet - * @returns {Promise} - */ - -Pool.prototype.handleSendHeaders = async function handleSendHeaders(peer, packet) { - ; -}; - -/** - * Handle `block` packet. Attempt to add to chain. - * @method - * @private - * @param {Peer} peer - * @param {BlockPacket} packet - * @returns {Promise} - */ - -Pool.prototype.handleBlock = async function handleBlock(peer, packet) { - const flags = chainCommon.flags.DEFAULT_FLAGS; - - if (this.options.spv) { - this.logger.warning( - 'Peer sent unsolicited block (%s).', - peer.hostname()); - return; - } - - await this.addBlock(peer, packet.block, flags); -}; - -/** - * Attempt to add block to chain. - * @method - * @private - * @param {Peer} peer - * @param {Block} block - * @returns {Promise} - */ - -Pool.prototype.addBlock = async function addBlock(peer, block, flags) { - const hash = block.hash('hex'); - const unlock = await this.locker.lock(hash); - try { - return await this._addBlock(peer, block, flags); - } finally { - unlock(); - } -}; - -/** - * Attempt to add block to chain (without a lock). - * @method - * @private - * @param {Peer} peer - * @param {Block} block - * @returns {Promise} - */ - -Pool.prototype._addBlock = async function _addBlock(peer, block, flags) { - if (!this.syncing) - return; - - const hash = block.hash('hex'); - - if (!this.resolveBlock(peer, hash)) { - this.logger.warning( - 'Received unrequested block: %s (%s).', - block.rhash(), peer.hostname()); - peer.destroy(); - return; - } - - peer.blockTime = Date.now(); - - let entry; - try { - entry = await this.chain.add(block, flags, peer.id); - } catch (err) { - if (err.type === 'VerifyError') { - peer.reject('block', err); - this.logger.warning(err); - return; - } - throw err; - } - - // Block was orphaned. - if (!entry) { - if (this.checkpoints) { - this.logger.warning( - 'Peer sent orphan block with getheaders (%s).', - peer.hostname()); - return; - } - - // During a getblocks sync, peers send - // their best tip frequently. We can grab - // the height commitment from the coinbase. - const height = block.getCoinbaseHeight(); - - if (height !== -1) { - peer.bestHash = hash; - peer.bestHeight = height; - this.resolveHeight(hash, height); - } - - this.logger.debug('Peer sent an orphan block. Resolving.'); - - await this.resolveOrphan(peer, hash); - - return; - } - - if (this.chain.synced) { - peer.bestHash = entry.hash; - peer.bestHeight = entry.height; - this.resolveHeight(entry.hash, entry.height); - } - - this.logStatus(block); - - await this.resolveChain(peer, hash); -}; - -/** - * Resolve header chain. - * @method - * @private - * @param {Peer} peer - * @param {Hash} hash - * @returns {Promise} - */ - -Pool.prototype.resolveChain = async function resolveChain(peer, hash) { - if (!this.checkpoints) - return; - - if (!peer.loader) - return; - - if (peer.destroyed) - throw new Error('Peer was destroyed (header chain resolution).'); - - const node = this.headerChain.head; - - assert(node); - - if (hash !== node.hash) { - this.logger.warning( - 'Header hash mismatch %s != %s (%s).', - encoding.revHex(hash), - encoding.revHex(node.hash), - peer.hostname()); - - peer.destroy(); - - return; - } - - if (node.height < this.network.lastCheckpoint) { - if (node.height === this.headerTip.height) { - this.logger.info( - 'Received checkpoint %s (%d).', - encoding.revHex(node.hash), node.height); - - this.headerTip = this.getNextTip(node.height); - - peer.sendGetHeaders([hash], this.headerTip.hash); - - return; - } - - this.headerChain.shift(); - this.resolveHeaders(peer); - - return; - } - - this.logger.info( - 'Switching to getblocks (%s).', - peer.hostname()); - - await this.switchSync(peer, hash); -}; - -/** - * Switch to getblocks. - * @method - * @private - * @param {Peer} peer - * @param {Hash} hash - * @returns {Promise} - */ - -Pool.prototype.switchSync = async function switchSync(peer, hash) { - assert(this.checkpoints); - - this.checkpoints = false; - this.headerTip = null; - this.headerChain.reset(); - this.headerNext = null; - - await this.getBlocks(peer, hash); -}; - -/** - * Handle bad orphan. - * @method - * @private - * @param {String} msg - * @param {VerifyError} err - * @param {Number} id - */ - -Pool.prototype.handleBadOrphan = function handleBadOrphan(msg, err, id) { - const peer = this.peers.find(id); - - if (!peer) { - this.logger.warning( - 'Could not find offending peer for orphan: %s (%d).', - encoding.revHex(err.hash), id); - return; - } - - this.logger.debug( - 'Punishing peer for sending a bad orphan (%s).', - peer.hostname()); - - // Punish the original peer who sent this. - peer.reject(msg, err); -}; - -/** - * Log sync status. - * @private - * @param {Block} block - */ - -Pool.prototype.logStatus = function logStatus(block) { - if (this.chain.height % 20 === 0) { - this.logger.debug('Status:' - + ' time=%s height=%d progress=%s' - + ' orphans=%d active=%d' - + ' target=%s peers=%d', - util.date(block.time), - this.chain.height, - (this.chain.getProgress() * 100).toFixed(2) + '%', - this.chain.orphanMap.size, - this.blockMap.size, - block.bits, - this.peers.size()); - } - - if (this.chain.height % 2000 === 0) { - this.logger.info( - 'Received 2000 more blocks (height=%d, hash=%s).', - this.chain.height, - block.rhash()); - } -}; - -/** - * Handle a transaction. Attempt to add to mempool. - * @method - * @private - * @param {Peer} peer - * @param {TXPacket} packet - * @returns {Promise} - */ - -Pool.prototype.handleTX = async function handleTX(peer, packet) { - const hash = packet.tx.hash('hex'); - const unlock = await this.locker.lock(hash); - try { - return await this._handleTX(peer, packet); - } finally { - unlock(); - } -}; - -/** - * Handle a transaction. Attempt to add to mempool (without a lock). - * @method - * @private - * @param {Peer} peer - * @param {TXPacket} packet - * @returns {Promise} - */ - -Pool.prototype._handleTX = async function _handleTX(peer, packet) { - const tx = packet.tx; - const hash = tx.hash('hex'); - const flags = chainCommon.flags.VERIFY_NONE; - const block = peer.merkleBlock; - - if (block) { - assert(peer.merkleMatches > 0); - assert(peer.merkleMap); - - if (block.hasTX(hash)) { - if (peer.merkleMap.has(hash)) { - this.logger.warning( - 'Peer sent duplicate merkle tx: %s (%s).', - tx.txid(), peer.hostname()); - peer.increaseBan(100); - return; - } - - peer.merkleMap.add(hash); - - block.txs.push(tx); - - if (--peer.merkleMatches === 0) { - peer.merkleBlock = null; - peer.merkleTime = -1; - peer.merkleMatches = 0; - peer.merkleMap = null; - await this._addBlock(peer, block, flags); - } - - return; - } - } - - if (!this.resolveTX(peer, hash)) { - this.logger.warning( - 'Peer sent unrequested tx: %s (%s).', - tx.txid(), peer.hostname()); - peer.destroy(); - return; - } - - if (!this.mempool) { - this.emit('tx', tx); - return; - } - - let missing; - try { - missing = await this.mempool.addTX(tx, peer.id); - } catch (err) { - if (err.type === 'VerifyError') { - peer.reject('tx', err); - this.logger.info(err); - return; - } - throw err; - } - - if (missing && missing.length > 0) { - this.logger.debug( - 'Requesting %d missing transactions (%s).', - missing.length, peer.hostname()); - - this.ensureTX(peer, missing); - } -}; - -/** - * Handle peer reject event. - * @method - * @private - * @param {Peer} peer - * @param {RejectPacket} packet - */ - -Pool.prototype.handleReject = async function handleReject(peer, packet) { - this.logger.warning( - 'Received reject (%s): msg=%s code=%s reason=%s hash=%s.', - peer.hostname(), - packet.message, - packet.getCode(), - packet.reason, - packet.rhash()); - - if (!packet.hash) - return; - - const entry = this.invMap.get(packet.hash); - - if (!entry) - return; - - entry.handleReject(peer); -}; - -/** - * Handle `mempool` packet. - * @method - * @private - * @param {Peer} peer - * @param {MempoolPacket} packet - */ - -Pool.prototype.handleMempool = async function handleMempool(peer, packet) { - if (!this.mempool) - return; - - if (!this.chain.synced) - return; - - if (this.options.selfish) - return; - - if (!this.options.bip37) { - this.logger.debug( - 'Peer requested mempool without bip37 enabled (%s).', - peer.hostname()); - peer.destroy(); - return; - } - - const items = []; - - for (const hash of this.mempool.map.keys()) - items.push(new InvItem(invTypes.TX, hash)); - - this.logger.debug( - 'Sending mempool snapshot (%s).', - peer.hostname()); - - peer.queueInv(items); -}; - -/** - * Handle `filterload` packet. - * @method - * @private - * @param {Peer} peer - * @param {FilterLoadPacket} packet - */ - -Pool.prototype.handleFilterLoad = async function handleFilterLoad(peer, packet) { - ; -}; - -/** - * Handle `filteradd` packet. - * @method - * @private - * @param {Peer} peer - * @param {FilterAddPacket} packet - */ - -Pool.prototype.handleFilterAdd = async function handleFilterAdd(peer, packet) { - ; -}; - -/** - * Handle `filterclear` packet. - * @method - * @private - * @param {Peer} peer - * @param {FilterClearPacket} packet - */ - -Pool.prototype.handleFilterClear = async function handleFilterClear(peer, packet) { - ; -}; - -/** - * Handle `merkleblock` packet. - * @method - * @private - * @param {Peer} peer - * @param {MerkleBlockPacket} block - */ - -Pool.prototype.handleMerkleBlock = async function handleMerkleBlock(peer, packet) { - const hash = packet.block.hash('hex'); - const unlock = await this.locker.lock(hash); - try { - return await this._handleMerkleBlock(peer, packet); - } finally { - unlock(); - } -}; - -/** - * Handle `merkleblock` packet (without a lock). - * @method - * @private - * @param {Peer} peer - * @param {MerkleBlockPacket} block - */ - -Pool.prototype._handleMerkleBlock = async function _handleMerkleBlock(peer, packet) { - if (!this.syncing) - return; - - // Potential DoS. - if (!this.options.spv) { - this.logger.warning( - 'Peer sent unsolicited merkleblock (%s).', - peer.hostname()); - peer.increaseBan(100); - return; - } - - const block = packet.block; - const hash = block.hash('hex'); - - if (!peer.blockMap.has(hash)) { - this.logger.warning( - 'Peer sent an unrequested merkleblock (%s).', - peer.hostname()); - peer.destroy(); - return; - } - - if (peer.merkleBlock) { - this.logger.warning( - 'Peer sent a merkleblock prematurely (%s).', - peer.hostname()); - peer.increaseBan(100); - return; - } - - if (!block.verify()) { - this.logger.warning( - 'Peer sent an invalid merkleblock (%s).', - peer.hostname()); - peer.increaseBan(100); - return; - } - - const tree = block.getTree(); - - if (tree.matches.length === 0) { - const flags = chainCommon.flags.VERIFY_NONE; - await this._addBlock(peer, block, flags); - return; - } - - peer.merkleBlock = block; - peer.merkleTime = Date.now(); - peer.merkleMatches = tree.matches.length; - peer.merkleMap = new Set(); -}; - -/** - * Handle `sendcmpct` packet. - * @method - * @private - * @param {Peer} peer - * @param {FeeFilterPacket} packet - */ - -Pool.prototype.handleFeeFilter = async function handleFeeFilter(peer, packet) { - ; -}; - -/** - * Handle `sendcmpct` packet. - * @method - * @private - * @param {Peer} peer - * @param {SendCmpctPacket} packet - */ - -Pool.prototype.handleSendCmpct = async function handleSendCmpct(peer, packet) { - ; -}; - -/** - * Handle `cmpctblock` packet. - * @method - * @private - * @param {Peer} peer - * @param {CompactBlockPacket} packet - */ - -Pool.prototype.handleCmpctBlock = async function handleCmpctBlock(peer, packet) { - const block = packet.block; - const hash = block.hash('hex'); - const witness = peer.compactWitness; - - if (!this.syncing) - return; - - if (!this.options.compact) { - this.logger.info( - 'Peer sent unsolicited cmpctblock (%s).', - peer.hostname()); - this.destroy(); - return; - } - - if (!peer.hasCompactSupport() || !peer.hasCompact()) { - this.logger.info( - 'Peer sent unsolicited cmpctblock (%s).', - peer.hostname()); - this.destroy(); - return; - } - - if (peer.compactBlocks.has(hash)) { - this.logger.debug( - 'Peer sent us a duplicate compact block (%s).', - peer.hostname()); - return; - } - - if (this.compactBlocks.has(hash)) { - this.logger.debug( - 'Already waiting for compact block %s (%s).', - hash, peer.hostname()); - return; - } - - if (!peer.blockMap.has(hash)) { - if (this.options.blockMode !== 1) { - this.logger.warning( - 'Peer sent us an unrequested compact block (%s).', - peer.hostname()); - peer.destroy(); - return; - } - peer.blockMap.set(hash, Date.now()); - assert(!this.blockMap.has(hash)); - this.blockMap.add(hash); - } - - if (!this.mempool) { - this.logger.warning('Requesting compact blocks without a mempool!'); - return; - } - - if (!block.verify()) { - this.logger.debug( - 'Peer sent an invalid compact block (%s).', - peer.hostname()); - peer.increaseBan(100); - return; - } - - let result; - try { - result = block.init(); - } catch (e) { - this.logger.debug( - 'Peer sent an invalid compact block (%s).', - peer.hostname()); - peer.increaseBan(100); - return; - } - - if (!result) { - this.logger.warning( - 'Siphash collision for %s. Requesting full block (%s).', - block.rhash(), peer.hostname()); - peer.getFullBlock(hash); - peer.increaseBan(10); - return; - } - - const full = block.fillMempool(witness, this.mempool); - - if (full) { - this.logger.debug( - 'Received full compact block %s (%s).', - block.rhash(), peer.hostname()); - const flags = chainCommon.flags.VERIFY_BODY; - await this.addBlock(peer, block.toBlock(), flags); - return; - } - - if (this.options.blockMode === 1) { - if (peer.compactBlocks.size >= 15) { - this.logger.warning('Compact block DoS attempt (%s).', peer.hostname()); - peer.destroy(); - return; - } - } - - block.now = Date.now(); - - assert(!peer.compactBlocks.has(hash)); - peer.compactBlocks.set(hash, block); - - this.compactBlocks.add(hash); - - this.logger.debug( - 'Received non-full compact block %s tx=%d/%d (%s).', - block.rhash(), block.count, block.totalTX, peer.hostname()); - - peer.send(new packets.GetBlockTxnPacket(block.toRequest())); -}; - -/** - * Handle `getblocktxn` packet. - * @method - * @private - * @param {Peer} peer - * @param {GetBlockTxnPacket} packet - */ - -Pool.prototype.handleGetBlockTxn = async function handleGetBlockTxn(peer, packet) { - const req = packet.request; - - if (this.chain.options.spv) - return; - - if (this.chain.options.prune) - return; - - if (this.options.selfish) - return; - - const item = new InvItem(invTypes.BLOCK, req.hash); - - const block = await this.getItem(peer, item); - - if (!block) { - this.logger.debug( - 'Peer sent getblocktxn for non-existent block (%s).', - peer.hostname()); - peer.increaseBan(100); - return; - } - - const height = await this.chain.getHeight(req.hash); - - if (height < this.chain.tip.height - 15) { - this.logger.debug( - 'Peer sent a getblocktxn for a block > 15 deep (%s)', - peer.hostname()); - return; - } - - this.logger.debug( - 'Sending blocktxn for %s to peer (%s).', - block.rhash(), - peer.hostname()); - - const res = BIP152.TXResponse.fromBlock(block, req); - - peer.send(new packets.BlockTxnPacket(res, peer.compactWitness)); -}; - -/** - * Handle `blocktxn` packet. - * @method - * @private - * @param {Peer} peer - * @param {BlockTxnPacket} packet - */ - -Pool.prototype.handleBlockTxn = async function handleBlockTxn(peer, packet) { - const res = packet.response; - const block = peer.compactBlocks.get(res.hash); - const flags = chainCommon.flags.VERIFY_BODY; - - if (!block) { - this.logger.debug( - 'Peer sent unsolicited blocktxn (%s).', - peer.hostname()); - return; - } - - peer.compactBlocks.delete(res.hash); - - assert(this.compactBlocks.has(res.hash)); - this.compactBlocks.delete(res.hash); - - if (!block.fillMissing(res)) { - this.logger.warning( - 'Peer sent non-full blocktxn for %s. Requesting full block (%s).', - block.rhash(), - peer.hostname()); - peer.getFullBlock(res.hash); - peer.increaseBan(10); - return; - } - - this.logger.debug( - 'Filled compact block %s (%s).', - block.rhash(), peer.hostname()); - - await this.addBlock(peer, block.toBlock(), flags); -}; - -/** - * Handle `encinit` packet. - * @method - * @private - * @param {Peer} peer - * @param {EncinitPacket} packet - */ - -Pool.prototype.handleEncinit = async function handleEncinit(peer, packet) { - ; -}; - -/** - * Handle `encack` packet. - * @method - * @private - * @param {Peer} peer - * @param {EncackPacket} packet - */ - -Pool.prototype.handleEncack = async function handleEncack(peer, packet) { - ; -}; - -/** - * Handle `authchallenge` packet. - * @method - * @private - * @param {Peer} peer - * @param {AuthChallengePacket} packet - */ - -Pool.prototype.handleAuthChallenge = async function handleAuthChallenge(peer, packet) { - ; -}; - -/** - * Handle `authreply` packet. - * @method - * @private - * @param {Peer} peer - * @param {AuthReplyPacket} packet - */ - -Pool.prototype.handleAuthReply = async function handleAuthReply(peer, packet) { - ; -}; - -/** - * Handle `authpropose` packet. - * @method - * @private - * @param {Peer} peer - * @param {AuthProposePacket} packet - */ - -Pool.prototype.handleAuthPropose = async function handleAuthPropose(peer, packet) { - ; -}; - -/** - * Handle `unknown` packet. - * @method - * @private - * @param {Peer} peer - * @param {UnknownPacket} packet - */ - -Pool.prototype.handleUnknown = async function handleUnknown(peer, packet) { - this.logger.warning( - 'Unknown packet: %s (%s).', - packet.cmd, peer.hostname()); -}; - -/** - * Create an inbound peer from an existing socket. - * @private - * @param {net.Socket} socket - */ - -Pool.prototype.addInbound = function addInbound(socket) { - if (!this.opened) { - socket.destroy(); - return; - } - - const peer = this.createInbound(socket); - - this.logger.info('Added inbound peer (%s).', peer.hostname()); - - this.peers.add(peer); -}; - -/** - * Allocate a host from the host list. - * @returns {NetAddress} - */ - -Pool.prototype.getHost = function getHost() { - for (const addr of this.hosts.nodes) { - if (this.peers.has(addr.hostname)) - continue; - - return addr; - } - - const services = this.options.getRequiredServices(); - const now = this.network.now(); - - for (let i = 0; i < 100; i++) { - const entry = this.hosts.getHost(); + const entry = this.mempool.getEntry(hash); if (!entry) - break; + return -1; - const addr = entry.addr; - - if (this.peers.has(addr.hostname)) - continue; - - if (!addr.isValid()) - continue; - - if (!addr.hasServices(services)) - continue; - - if (!this.options.onion && addr.isOnion()) - continue; - - if (i < 30 && now - entry.lastAttempt < 600) - continue; - - if (i < 50 && addr.port !== this.network.port) - continue; - - if (i < 95 && this.hosts.isBanned(addr.host)) - continue; - - return entry.addr; + return entry.getRate(); } - return null; -}; + /** + * Default createSocket call. + * @private + * @param {Number} port + * @param {String} host + * @returns {net.Socket} + */ -/** - * Create an outbound non-loader peer. These primarily - * exist for transaction relaying. - * @private - */ + _createSocket(port, host) { + if (this.proxy) + return socks.connect(this.proxy, port, host); -Pool.prototype.addOutbound = function addOutbound() { - if (!this.opened) - return; - - if (this.peers.outbound >= this.options.maxOutbound) - return; - - // Hang back if we don't - // have a loader peer yet. - if (!this.peers.load) - return; - - const addr = this.getHost(); - - if (!addr) - return; - - const peer = this.createOutbound(addr); - - this.peers.add(peer); - - this.emit('peer', peer); -}; - -/** - * Attempt to refill the pool with peers (no lock). - * @private - */ - -Pool.prototype.fillOutbound = function fillOutbound() { - const need = this.options.maxOutbound - this.peers.outbound; - - if (!this.peers.load) - this.addLoader(); - - if (need <= 0) - return; - - this.logger.debug('Refilling peers (%d/%d).', - this.peers.outbound, - this.options.maxOutbound); - - for (let i = 0; i < need; i++) - this.addOutbound(); -}; - -/** - * Attempt to refill the pool with peers (no lock). - * @private - */ - -Pool.prototype.refill = function refill() { - if (this.pendingRefill != null) - return; - - this.pendingRefill = setTimeout(() => { - this.pendingRefill = null; - this.fillOutbound(); - }, 3000); -}; - -/** - * Remove a peer from any list. Drop all load requests. - * @private - * @param {Peer} peer - */ - -Pool.prototype.removePeer = function removePeer(peer) { - this.peers.remove(peer); - - for (const hash of peer.blockMap.keys()) - this.resolveBlock(peer, hash); - - for (const hash of peer.txMap.keys()) - this.resolveTX(peer, hash); - - for (const hash of peer.compactBlocks.keys()) { - assert(this.compactBlocks.has(hash)); - this.compactBlocks.delete(hash); + return tcp.createSocket(port, host); } - peer.compactBlocks.clear(); -}; + /** + * Default resolve call. + * @private + * @param {String} name + * @returns {String[]} + */ -/** - * Ban peer. - * @param {NetAddress} addr - */ + _resolve(name) { + if (this.onion) + return socks.resolve(this.proxy, name); -Pool.prototype.ban = function ban(addr) { - const peer = this.peers.get(addr.hostname); - - this.logger.debug('Banning peer (%s).', addr.hostname); - - this.hosts.ban(addr.host); - this.hosts.remove(addr.hostname); - - if (peer) - peer.destroy(); -}; - -/** - * Unban peer. - * @param {NetAddress} addr - */ - -Pool.prototype.unban = function unban(addr) { - this.hosts.unban(addr.host); -}; - -/** - * Set the spv filter. - * @param {BloomFilter} filter - * @param {String?} enc - */ - -Pool.prototype.setFilter = function setFilter(filter) { - if (!this.options.spv) - return; - - this.spvFilter = filter; - this.queueFilterLoad(); -}; - -/** - * Watch a an address hash (filterload, SPV-only). - * @param {Buffer|Hash} data - * @param {String?} enc - */ - -Pool.prototype.watch = function watch(data, enc) { - if (!this.options.spv) - return; - - this.spvFilter.add(data, enc); - this.queueFilterLoad(); -}; - -/** - * Reset the spv filter (filterload, SPV-only). - */ - -Pool.prototype.unwatch = function unwatch() { - if (!this.options.spv) - return; - - this.spvFilter.reset(); - this.queueFilterLoad(); -}; - -/** - * Queue a resend of the bloom filter. - */ - -Pool.prototype.queueFilterLoad = function queueFilterLoad() { - if (!this.options.spv) - return; - - if (this.pendingFilter != null) - return; - - this.pendingFilter = setTimeout(() => { - this.pendingFilter = null; - this.sendFilterLoad(); - }, 100); -}; - -/** - * Resend the bloom filter to peers. - */ - -Pool.prototype.sendFilterLoad = function sendFilterLoad() { - if (!this.options.spv) - return; - - assert(this.spvFilter); - - for (let peer = this.peers.head(); peer; peer = peer.next) - peer.sendFilterLoad(this.spvFilter); -}; - -/** - * Add an address to the bloom filter (SPV-only). - * @param {Address|Base58Address} address - */ - -Pool.prototype.watchAddress = function watchAddress(address) { - const hash = Address.getHash(address); - this.watch(hash); -}; - -/** - * Add an outpoint to the bloom filter (SPV-only). - * @param {Outpoint} outpoint - */ - -Pool.prototype.watchOutpoint = function watchOutpoint(outpoint) { - this.watch(outpoint.toRaw()); -}; - -/** - * Send `getblocks` to peer after building - * locator and resolving orphan root. - * @method - * @param {Peer} peer - * @param {Hash} orphan - Orphan hash to resolve. - * @returns {Promise} - */ - -Pool.prototype.resolveOrphan = async function resolveOrphan(peer, orphan) { - const locator = await this.chain.getLocator(); - const root = this.chain.getOrphanRoot(orphan); - - assert(root); - - peer.sendGetBlocks(locator, root); -}; - -/** - * Send `getheaders` to peer after building locator. - * @method - * @param {Peer} peer - * @param {Hash} tip - Tip to build chain locator from. - * @param {Hash?} stop - * @returns {Promise} - */ - -Pool.prototype.getHeaders = async function getHeaders(peer, tip, stop) { - const locator = await this.chain.getLocator(tip); - peer.sendGetHeaders(locator, stop); -}; - -/** - * Send `getblocks` to peer after building locator. - * @method - * @param {Peer} peer - * @param {Hash} tip - Tip hash to build chain locator from. - * @param {Hash?} stop - * @returns {Promise} - */ - -Pool.prototype.getBlocks = async function getBlocks(peer, tip, stop) { - const locator = await this.chain.getLocator(tip); - peer.sendGetBlocks(locator, stop); -}; - -/** - * Queue a `getdata` request to be sent. - * @param {Peer} peer - * @param {Hash[]} hashes - */ - -Pool.prototype.getBlock = function getBlock(peer, hashes) { - if (!this.opened) - return; - - if (!peer.handshake) - throw new Error('Peer handshake not complete (getdata).'); - - if (peer.destroyed) - throw new Error('Peer is destroyed (getdata).'); - - let now = Date.now(); - const items = []; - - for (const hash of hashes) { - if (this.blockMap.has(hash)) - continue; - - this.blockMap.add(hash); - peer.blockMap.set(hash, now); - - if (this.chain.synced) - now += 100; - - items.push(hash); + return dns.lookup(name); } - - if (items.length === 0) - return; - - this.logger.debug( - 'Requesting %d/%d blocks from peer with getdata (%s).', - items.length, - this.blockMap.size, - peer.hostname()); - - peer.getBlock(items); -}; - -/** - * Queue a `getdata` request to be sent. - * @param {Peer} peer - * @param {Hash[]} hashes - */ - -Pool.prototype.getTX = function getTX(peer, hashes) { - if (!this.opened) - return; - - if (!peer.handshake) - throw new Error('Peer handshake not complete (getdata).'); - - if (peer.destroyed) - throw new Error('Peer is destroyed (getdata).'); - - let now = Date.now(); - - const items = []; - - for (const hash of hashes) { - if (this.txMap.has(hash)) - continue; - - this.txMap.add(hash); - peer.txMap.set(hash, now); - - now += 50; - - items.push(hash); - } - - if (items.length === 0) - return; - - this.logger.debug( - 'Requesting %d/%d txs from peer with getdata (%s).', - items.length, - this.txMap.size, - peer.hostname()); - - peer.getTX(items); -}; - -/** - * Test whether the chain has or has seen an item. - * @method - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -Pool.prototype.hasBlock = async function hasBlock(hash) { - // Check the lock. - if (this.locker.has(hash)) - return true; - - // Check the chain. - if (await this.chain.has(hash)) - return true; - - return false; -}; - -/** - * Test whether the mempool has or has seen an item. - * @param {Hash} hash - * @returns {Boolean} - */ - -Pool.prototype.hasTX = function hasTX(hash) { - // Check the lock queue. - if (this.locker.has(hash)) - return true; - - if (!this.mempool) { - // Check the TX filter if - // we don't have a mempool. - if (!this.txFilter.added(hash, 'hex')) - return true; - } else { - // Check the mempool. - if (this.mempool.has(hash)) - return true; - - // If we recently rejected this item. Ignore. - if (this.mempool.hasReject(hash)) { - this.logger.spam('Saw known reject of %s.', encoding.revHex(hash)); - return true; - } - } - - return false; -}; - -/** - * Queue a `getdata` request to be sent. - * Check tx existence before requesting. - * @param {Peer} peer - * @param {Hash[]} hashes - */ - -Pool.prototype.ensureTX = function ensureTX(peer, hashes) { - const items = []; - - for (const hash of hashes) { - if (this.hasTX(hash)) - continue; - - items.push(hash); - } - - this.getTX(peer, items); -}; - -/** - * Fulfill a requested tx. - * @param {Peer} peer - * @param {Hash} hash - * @returns {Boolean} - */ - -Pool.prototype.resolveTX = function resolveTX(peer, hash) { - if (!peer.txMap.has(hash)) - return false; - - peer.txMap.delete(hash); - - assert(this.txMap.has(hash)); - this.txMap.delete(hash); - - return true; -}; - -/** - * Fulfill a requested block. - * @param {Peer} peer - * @param {Hash} hash - * @returns {Boolean} - */ - -Pool.prototype.resolveBlock = function resolveBlock(peer, hash) { - if (!peer.blockMap.has(hash)) - return false; - - peer.blockMap.delete(hash); - - assert(this.blockMap.has(hash)); - this.blockMap.delete(hash); - - return true; -}; - -/** - * Fulfill a requested item. - * @param {Peer} peer - * @param {InvItem} item - * @returns {Boolean} - */ - -Pool.prototype.resolveItem = function resolveItem(peer, item) { - if (item.isBlock()) - return this.resolveBlock(peer, item.hash); - - if (item.isTX()) - return this.resolveTX(peer, item.hash); - - return false; -}; - -/** - * Broadcast a transaction or block. - * @param {TX|Block} msg - * @returns {Promise} - */ - -Pool.prototype.broadcast = function broadcast(msg) { - const hash = msg.hash('hex'); - - let item = this.invMap.get(hash); - - if (item) { - item.refresh(); - item.announce(); - } else { - item = new BroadcastItem(this, msg); - item.start(); - item.announce(); - } - - return new Promise((resolve, reject) => { - item.addJob(resolve, reject); - }); -}; - -/** - * Announce a block to all peers. - * @param {Block} tx - */ - -Pool.prototype.announceBlock = function announceBlock(msg) { - for (let peer = this.peers.head(); peer; peer = peer.next) - peer.announceBlock(msg); -}; - -/** - * Announce a transaction to all peers. - * @param {TX} tx - */ - -Pool.prototype.announceTX = function announceTX(msg) { - for (let peer = this.peers.head(); peer; peer = peer.next) - peer.announceTX(msg); -}; - -/** - * PoolOptions - * @alias module:net.PoolOptions - * @constructor - */ - -function PoolOptions(options) { - if (!(this instanceof PoolOptions)) - return new PoolOptions(options); - - this.network = Network.primary; - this.logger = null; - this.chain = null; - this.mempool = null; - - this.nonces = new NonceList(); - - this.prefix = null; - this.checkpoints = true; - this.spv = false; - this.bip37 = false; - this.listen = false; - this.compact = true; - this.noRelay = false; - this.host = '0.0.0.0'; - this.port = this.network.port; - this.publicHost = '0.0.0.0'; - this.publicPort = this.network.port; - this.maxOutbound = 8; - this.maxInbound = 8; - this.createSocket = this._createSocket.bind(this); - this.createServer = tcp.createServer; - this.resolve = this._resolve.bind(this); - this.proxy = null; - this.onion = false; - this.upnp = false; - this.selfish = false; - this.version = common.PROTOCOL_VERSION; - this.agent = common.USER_AGENT; - this.bip151 = false; - this.bip150 = false; - this.authPeers = []; - this.knownPeers = {}; - this.identityKey = secp256k1.generatePrivateKey(); - this.banScore = common.BAN_SCORE; - this.banTime = common.BAN_TIME; - this.feeRate = -1; - this.seeds = this.network.seeds; - this.nodes = []; - this.invTimeout = 60000; - this.blockMode = 0; - this.services = common.LOCAL_SERVICES; - this.requiredServices = common.REQUIRED_SERVICES; - this.persistent = false; - - this.fromOptions(options); } -/** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {PoolOptions} - */ - -PoolOptions.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Pool requires options.'); - assert(options.chain && typeof options.chain === 'object', - 'Pool options require a blockchain.'); - - this.chain = options.chain; - this.network = options.chain.network; - this.logger = options.chain.logger; - - this.port = this.network.port; - this.seeds = this.network.seeds; - this.port = this.network.port; - this.publicPort = this.network.port; - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.mempool != null) { - assert(typeof options.mempool === 'object'); - this.mempool = options.mempool; - } - - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - } - - if (options.checkpoints != null) { - assert(typeof options.checkpoints === 'boolean'); - assert(options.checkpoints === this.chain.options.checkpoints); - this.checkpoints = options.checkpoints; - } else { - this.checkpoints = this.chain.options.checkpoints; - } - - if (options.spv != null) { - assert(typeof options.spv === 'boolean'); - assert(options.spv === this.chain.options.spv); - this.spv = options.spv; - } else { - this.spv = this.chain.options.spv; - } - - if (options.bip37 != null) { - assert(typeof options.bip37 === 'boolean'); - this.bip37 = options.bip37; - } - - if (options.listen != null) { - assert(typeof options.listen === 'boolean'); - this.listen = options.listen; - } - - if (options.compact != null) { - assert(typeof options.compact === 'boolean'); - this.compact = options.compact; - } - - if (options.noRelay != null) { - assert(typeof options.noRelay === 'boolean'); - this.noRelay = options.noRelay; - } - - if (options.host != null) { - assert(typeof options.host === 'string'); - const raw = IP.toBuffer(options.host); - this.host = IP.toString(raw); - if (IP.isRoutable(raw)) - this.publicHost = this.host; - } - - if (options.port != null) { - assert((options.port & 0xffff) === options.port); - this.port = options.port; - this.publicPort = options.port; - } - - if (options.publicHost != null) { - assert(typeof options.publicHost === 'string'); - this.publicHost = IP.normalize(options.publicHost); - } - - if (options.publicPort != null) { - assert((options.publicPort & 0xff) === options.publicPort); - this.publicPort = options.publicPort; - } - - if (options.maxOutbound != null) { - assert(typeof options.maxOutbound === 'number'); - assert(options.maxOutbound > 0); - this.maxOutbound = options.maxOutbound; - } - - if (options.maxInbound != null) { - assert(typeof options.maxInbound === 'number'); - this.maxInbound = options.maxInbound; - } - - if (options.createSocket) { - assert(typeof options.createSocket === 'function'); - this.createSocket = options.createSocket; - } - - if (options.createServer) { - assert(typeof options.createServer === 'function'); - this.createServer = options.createServer; - } - - if (options.resolve) { - assert(typeof options.resolve === 'function'); - this.resolve = options.resolve; - } - - if (options.proxy) { - assert(typeof options.proxy === 'string'); - this.proxy = options.proxy; - } - - if (options.onion != null) { - assert(typeof options.onion === 'boolean'); - this.onion = options.onion; - } - - if (options.upnp != null) { - assert(typeof options.upnp === 'boolean'); - this.upnp = options.upnp; - } - - if (options.selfish) { - assert(typeof options.selfish === 'boolean'); - this.selfish = options.selfish; - } - - if (options.version) { - assert(typeof options.version === 'number'); - this.version = options.version; - } - - if (options.agent) { - assert(typeof options.agent === 'string'); - assert(options.agent.length <= 255); - this.agent = options.agent; - } - - if (options.bip151 != null) { - assert(typeof options.bip151 === 'boolean'); - this.bip151 = options.bip151; - } - - if (options.bip150 != null) { - assert(typeof options.bip150 === 'boolean'); - assert(!options.bip150 || this.bip151, - 'Cannot enable bip150 without bip151.'); - - if (options.knownPeers) { - assert(typeof options.knownPeers === 'object'); - assert(!Array.isArray(options.knownPeers)); - this.knownPeers = options.knownPeers; - } - - if (options.authPeers) { - assert(Array.isArray(options.authPeers)); - this.authPeers = options.authPeers; - } - - if (options.identityKey) { - assert(Buffer.isBuffer(options.identityKey), - 'Identity key must be a buffer.'); - assert(secp256k1.privateKeyVerify(options.identityKey), - 'Invalid identity key.'); - this.identityKey = options.identityKey; - } - } - - if (options.banScore != null) { - assert(typeof this.options.banScore === 'number'); - this.banScore = this.options.banScore; - } - - if (options.banTime != null) { - assert(typeof this.options.banTime === 'number'); - this.banTime = this.options.banTime; - } - - if (options.feeRate != null) { - assert(typeof this.options.feeRate === 'number'); - this.feeRate = this.options.feeRate; - } - - if (options.seeds) { - assert(Array.isArray(options.seeds)); - this.seeds = options.seeds; - } - - if (options.nodes) { - assert(Array.isArray(options.nodes)); - this.nodes = options.nodes; - } - - if (options.only != null) { - assert(Array.isArray(options.only)); - if (options.only.length > 0) { - this.nodes = options.only; - this.maxOutbound = options.only.length; - } - } - - if (options.invTimeout != null) { - assert(typeof options.invTimeout === 'number'); - this.invTimeout = options.invTimeout; - } - - if (options.blockMode != null) { - assert(typeof options.blockMode === 'number'); - this.blockMode = options.blockMode; - } - - if (options.persistent != null) { - assert(typeof options.persistent === 'boolean'); - this.persistent = options.persistent; - } - - if (this.spv) { - this.requiredServices |= common.services.BLOOM; - this.services &= ~common.services.NETWORK; - this.noRelay = true; - this.checkpoints = true; - this.compact = false; - this.bip37 = false; - this.listen = false; - } - - if (this.selfish) { - this.services &= ~common.services.NETWORK; - this.bip37 = false; - } - - if (this.bip37) - this.services |= common.services.BLOOM; - - if (this.proxy) - this.listen = false; - - if (options.services != null) { - assert((options.services >>> 0) === options.services); - this.services = options.services; - } - - if (options.requiredServices != null) { - assert((options.requiredServices >>> 0) === options.requiredServices); - this.requiredServices = options.requiredServices; - } - - return this; -}; - -/** - * Instantiate options from object. - * @param {Object} options - * @returns {PoolOptions} - */ - -PoolOptions.fromOptions = function fromOptions(options) { - return new PoolOptions().fromOptions(options); -}; - -/** - * Get the chain height. - * @private - * @returns {Number} - */ - -PoolOptions.prototype.getHeight = function getHeight() { - return this.chain.height; -}; - -/** - * Test whether the chain is synced. - * @private - * @returns {Boolean} - */ - -PoolOptions.prototype.isFull = function isFull() { - return this.chain.synced; -}; - -/** - * Get required services for outbound peers. - * @private - * @returns {Number} - */ - -PoolOptions.prototype.getRequiredServices = function getRequiredServices() { - let services = this.requiredServices; - if (this.hasWitness()) - services |= common.services.WITNESS; - return services; -}; - -/** - * Whether segwit is enabled. - * @private - * @returns {Boolean} - */ - -PoolOptions.prototype.hasWitness = function hasWitness() { - return this.chain.state.hasWitness(); -}; - -/** - * Create a version packet nonce. - * @private - * @param {String} hostname - * @returns {Buffer} - */ - -PoolOptions.prototype.createNonce = function createNonce(hostname) { - return this.nonces.alloc(hostname); -}; - -/** - * Test whether version nonce is ours. - * @private - * @param {Buffer} nonce - * @returns {Boolean} - */ - -PoolOptions.prototype.hasNonce = function hasNonce(nonce) { - return this.nonces.has(nonce); -}; - -/** - * Get fee rate for txid. - * @private - * @param {Hash} hash - * @returns {Rate} - */ - -PoolOptions.prototype.getRate = function getRate(hash) { - if (!this.mempool) - return -1; - - const entry = this.mempool.getEntry(hash); - - if (!entry) - return -1; - - return entry.getRate(); -}; - -/** - * Default createSocket call. - * @private - * @param {Number} port - * @param {String} host - * @returns {net.Socket} - */ - -PoolOptions.prototype._createSocket = function _createSocket(port, host) { - if (this.proxy) - return socks.connect(this.proxy, port, host); - - return tcp.createSocket(port, host); -}; - -/** - * Default resolve call. - * @private - * @param {String} name - * @returns {String[]} - */ - -PoolOptions.prototype._resolve = function _resolve(name) { - if (this.onion) - return socks.resolve(this.proxy, name); - - return dns.lookup(name); -}; - /** * Peer List * @alias module:net.PeerList - * @constructor - * @param {Object} options */ -function PeerList() { - this.map = new Map(); - this.ids = new Map(); - this.list = new List(); - this.load = null; - this.inbound = 0; - this.outbound = 0; +class PeerList { + /** + * Create peer list. + * @constructor + * @param {Object} options + */ + + constructor() { + this.map = new Map(); + this.ids = new Map(); + this.list = new List(); + this.load = null; + this.inbound = 0; + this.outbound = 0; + } + + /** + * Get the list head. + * @returns {Peer} + */ + + head() { + return this.list.head; + } + + /** + * Get the list tail. + * @returns {Peer} + */ + + tail() { + return this.list.tail; + } + + /** + * Get list size. + * @returns {Number} + */ + + size() { + return this.list.size; + } + + /** + * Add peer to list. + * @param {Peer} peer + */ + + add(peer) { + assert(this.list.push(peer)); + + assert(!this.map.has(peer.hostname())); + this.map.set(peer.hostname(), peer); + + assert(!this.ids.has(peer.id)); + this.ids.set(peer.id, peer); + + if (peer.outbound) + this.outbound += 1; + else + this.inbound += 1; + } + + /** + * Remove peer from list. + * @param {Peer} peer + */ + + remove(peer) { + assert(this.list.remove(peer)); + + assert(this.ids.has(peer.id)); + this.ids.delete(peer.id); + + assert(this.map.has(peer.hostname())); + this.map.delete(peer.hostname()); + + if (peer === this.load) { + assert(peer.loader); + peer.loader = false; + this.load = null; + } + + if (peer.outbound) + this.outbound -= 1; + else + this.inbound -= 1; + } + + /** + * Get peer by hostname. + * @param {String} hostname + * @returns {Peer} + */ + + get(hostname) { + return this.map.get(hostname); + } + + /** + * Test whether a peer exists. + * @param {String} hostname + * @returns {Boolean} + */ + + has(hostname) { + return this.map.has(hostname); + } + + /** + * Get peer by ID. + * @param {Number} id + * @returns {Peer} + */ + + find(id) { + return this.ids.get(id); + } + + /** + * Destroy peer list (kills peers). + */ + + destroy() { + let next; + + for (let peer = this.list.head; peer; peer = next) { + next = peer.next; + peer.destroy(); + } + } } /** - * 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; -}; - -/** - * Add peer to list. - * @param {Peer} peer - */ - -PeerList.prototype.add = function add(peer) { - assert(this.list.push(peer)); - - assert(!this.map.has(peer.hostname())); - this.map.set(peer.hostname(), peer); - - assert(!this.ids.has(peer.id)); - this.ids.set(peer.id, peer); - - if (peer.outbound) - this.outbound++; - else - this.inbound++; -}; - -/** - * Remove peer from list. - * @param {Peer} peer - */ - -PeerList.prototype.remove = function remove(peer) { - assert(this.list.remove(peer)); - - assert(this.ids.has(peer.id)); - this.ids.delete(peer.id); - - assert(this.map.has(peer.hostname())); - this.map.delete(peer.hostname()); - - if (peer === this.load) { - assert(peer.loader); - peer.loader = false; - this.load = null; - } - - if (peer.outbound) - this.outbound--; - else - this.inbound--; -}; - -/** - * Get peer by hostname. - * @param {String} hostname - * @returns {Peer} - */ - -PeerList.prototype.get = function get(hostname) { - return this.map.get(hostname); -}; - -/** - * Test whether a peer exists. - * @param {String} hostname - * @returns {Boolean} - */ - -PeerList.prototype.has = function has(hostname) { - return this.map.has(hostname); -}; - -/** - * Get peer by ID. - * @param {Number} id - * @returns {Peer} - */ - -PeerList.prototype.find = function find(id) { - return this.ids.get(id); -}; - -/** - * Destroy peer list (kills peers). - */ - -PeerList.prototype.destroy = function destroy() { - let next; - - for (let peer = this.list.head; peer; peer = next) { - next = peer.next; - peer.destroy(); - } -}; - -/** + * Broadcast Item * Represents an item that is broadcasted via an inv/getdata cycle. * @alias module:net.BroadcastItem - * @constructor * @private - * @param {Pool} pool - * @param {TX|Block} msg * @emits BroadcastItem#ack * @emits BroadcastItem#reject * @emits BroadcastItem#timeout */ -function BroadcastItem(pool, msg) { - if (!(this instanceof BroadcastItem)) - return new BroadcastItem(pool, msg); +class BroadcastItem extends EventEmitter { + /** + * Create broadcast item. + * @constructor + * @param {Pool} pool + * @param {TX|Block} msg + */ - assert(!msg.mutable, 'Cannot broadcast mutable item.'); + constructor(pool, msg) { + assert(!msg.mutable, 'Cannot broadcast mutable item.'); - const item = msg.toInv(); + const item = msg.toInv(); - this.pool = pool; - this.hash = item.hash; - this.type = item.type; - this.msg = msg; - this.jobs = []; -} + this.pool = pool; + this.hash = item.hash; + this.type = item.type; + this.msg = msg; + this.jobs = []; + } -Object.setPrototypeOf(BroadcastItem.prototype, EventEmitter.prototype); + /** + * Add a job to be executed on ack, timeout, or reject. + * @returns {Promise} + */ -/** - * Add a job to be executed on ack, timeout, or reject. - * @returns {Promise} - */ + addJob(resolve, reject) { + this.jobs.push({ resolve, reject }); + } -BroadcastItem.prototype.addJob = function addJob(resolve, reject) { - this.jobs.push({ resolve, reject }); -}; + /** + * Start the broadcast. + */ -/** - * Start the broadcast. - */ + start() { + assert(!this.timeout, 'Already started.'); + assert(!this.pool.invMap.has(this.hash), 'Already started.'); -BroadcastItem.prototype.start = function start() { - assert(!this.timeout, 'Already started.'); - assert(!this.pool.invMap.has(this.hash), 'Already started.'); + this.pool.invMap.set(this.hash, this); - this.pool.invMap.set(this.hash, this); + this.refresh(); - this.refresh(); + return this; + } - return this; -}; + /** + * Refresh the timeout on the broadcast. + */ -/** - * Refresh the timeout on the broadcast. - */ + refresh() { + if (this.timeout != null) { + clearTimeout(this.timeout); + this.timeout = null; + } + + this.timeout = setTimeout(() => { + this.emit('timeout'); + this.reject(new Error('Timed out.')); + }, this.pool.options.invTimeout); + } + + /** + * Announce the item. + */ + + announce() { + switch (this.type) { + case invTypes.TX: + this.pool.announceTX(this.msg); + break; + case invTypes.BLOCK: + this.pool.announceBlock(this.msg); + break; + default: + assert(false, 'Bad type.'); + break; + } + } + + /** + * Finish the broadcast. + */ + + cleanup() { + assert(this.timeout != null, 'Already finished.'); + assert(this.pool.invMap.has(this.hash), 'Already finished.'); -BroadcastItem.prototype.refresh = function refresh() { - if (this.timeout != null) { clearTimeout(this.timeout); this.timeout = null; + + this.pool.invMap.delete(this.hash); } - this.timeout = setTimeout(() => { - this.emit('timeout'); - this.reject(new Error('Timed out.')); - }, this.pool.options.invTimeout); -}; + /** + * Finish the broadcast, return with an error. + * @param {Error} err + */ -/** - * Announce the item. - */ - -BroadcastItem.prototype.announce = function announce() { - switch (this.type) { - case invTypes.TX: - this.pool.announceTX(this.msg); - break; - case invTypes.BLOCK: - this.pool.announceBlock(this.msg); - break; - default: - assert(false, 'Bad type.'); - break; - } -}; - -/** - * Finish the broadcast. - */ - -BroadcastItem.prototype.cleanup = function cleanup() { - assert(this.timeout != null, 'Already finished.'); - assert(this.pool.invMap.has(this.hash), 'Already finished.'); - - clearTimeout(this.timeout); - this.timeout = null; - - this.pool.invMap.delete(this.hash); -}; - -/** - * Finish the broadcast, return with an error. - * @param {Error} err - */ - -BroadcastItem.prototype.reject = function reject(err) { - this.cleanup(); - - for (const job of this.jobs) - job.reject(err); - - this.jobs.length = 0; -}; - -/** - * Finish the broadcast successfully. - */ - -BroadcastItem.prototype.resolve = function resolve() { - this.cleanup(); - - for (const job of this.jobs) - job.resolve(false); - - this.jobs.length = 0; -}; - -/** - * Handle an ack from a peer. - * @param {Peer} peer - */ - -BroadcastItem.prototype.handleAck = function handleAck(peer) { - setTimeout(() => { - this.emit('ack', peer); + reject(err) { + this.cleanup(); for (const job of this.jobs) - job.resolve(true); + job.reject(err); this.jobs.length = 0; - }, 1000); -}; + } -/** - * Handle a reject from a peer. - * @param {Peer} peer - */ + /** + * Finish the broadcast successfully. + */ -BroadcastItem.prototype.handleReject = function handleReject(peer) { - this.emit('reject', peer); + resolve() { + this.cleanup(); - for (const job of this.jobs) - job.resolve(false); + for (const job of this.jobs) + job.resolve(false); - this.jobs.length = 0; -}; + this.jobs.length = 0; + } -/** - * Inspect the broadcast item. - * @returns {String} - */ + /** + * Handle an ack from a peer. + * @param {Peer} peer + */ -BroadcastItem.prototype.inspect = function inspect() { - const type = this.type === invTypes.TX ? 'tx' : 'block'; - const hash = encoding.revHex(this.hash); - return ``; -}; + handleAck(peer) { + setTimeout(() => { + this.emit('ack', peer); -/** - * NonceList - * @constructor - * @ignore - */ + for (const job of this.jobs) + job.resolve(true); -function NonceList() { - this.map = new Map(); - this.hosts = new Map(); + this.jobs.length = 0; + }, 1000); + } + + /** + * Handle a reject from a peer. + * @param {Peer} peer + */ + + handleReject(peer) { + this.emit('reject', peer); + + for (const job of this.jobs) + job.resolve(false); + + this.jobs.length = 0; + } + + /** + * Inspect the broadcast item. + * @returns {String} + */ + + inspect() { + const type = this.type === invTypes.TX ? 'tx' : 'block'; + const hash = encoding.revHex(this.hash); + return ``; + } } -NonceList.prototype.alloc = function alloc(hostname) { - for (;;) { - const nonce = common.nonce(); - const key = nonce.toString('hex'); - - if (this.map.has(key)) - continue; - - this.map.set(key, hostname); - - assert(!this.hosts.has(hostname)); - this.hosts.set(hostname, key); - - return nonce; - } -}; - -NonceList.prototype.has = function has(nonce) { - const key = nonce.toString('hex'); - return this.map.has(key); -}; - -NonceList.prototype.remove = function remove(hostname) { - const key = this.hosts.get(hostname); - - if (!key) - return false; - - this.hosts.delete(hostname); - - assert(this.map.has(key)); - this.map.delete(key); - - return true; -}; - /** - * HeaderEntry - * @constructor + * Nonce List * @ignore */ -function HeaderEntry(hash, height) { - this.hash = hash; - this.height = height; - this.prev = null; - this.next = null; +class NonceList { + /** + * Create nonce list. + * @constructor + */ + + constructor() { + this.map = new Map(); + this.hosts = new Map(); + } + + alloc(hostname) { + for (;;) { + const nonce = common.nonce(); + const key = nonce.toString('hex'); + + if (this.map.has(key)) + continue; + + this.map.set(key, hostname); + + assert(!this.hosts.has(hostname)); + this.hosts.set(hostname, key); + + return nonce; + } + } + + has(nonce) { + const key = nonce.toString('hex'); + return this.map.has(key); + } + + remove(hostname) { + const key = this.hosts.get(hostname); + + if (!key) + return false; + + this.hosts.delete(hostname); + + assert(this.map.has(key)); + this.map.delete(key); + + return true; + } +} + +/** + * Header Entry + * @ignore + */ + +class HeaderEntry { + /** + * Create header entry. + * @constructor + */ + + constructor(hash, height) { + this.hash = hash; + this.height = height; + this.prev = null; + this.next = null; + } } /*