/*! * peer.js - peer object for bcoin * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('bsert'); const EventEmitter = require('events'); const {Lock} = require('bmutex'); const {format} = require('util'); const tcp = require('btcp'); const dns = require('bdns'); const Logger = require('blgr'); const {RollingFilter} = require('bfilter'); const {BufferMap} = require('buffer-map'); const Parser = require('./parser'); const Framer = require('./framer'); const packets = require('./packets'); const consensus = require('../protocol/consensus'); const common = require('./common'); const InvItem = require('../primitives/invitem'); const BIP152 = require('./bip152'); const Block = require('../primitives/block'); const TX = require('../primitives/tx'); const NetAddress = require('./netaddress'); const Network = require('../protocol/network'); const services = common.services; const invTypes = InvItem.types; const packetTypes = packets.types; const {inspectSymbol} = require('../utils'); /** * Represents a network peer. * @alias module:net.Peer * @extends EventEmitter * @property {net.Socket} socket * @property {NetAddress} address * @property {Parser} parser * @property {Framer} framer * @property {Number} version * @property {Boolean} destroyed * @property {Boolean} ack - Whether verack has been received. * @property {Boolean} connected * @property {Number} time * @property {Boolean} preferHeaders - Whether the peer has * requested getheaders. * @property {Hash?} hashContinue - The block hash at which to continue * the sync for the peer. * @property {Bloom?} spvFilter - The _peer's_ bloom spvFilter. * @property {Boolean} noRelay - Whether to relay transactions * immediately to the peer. * @property {BN} challenge - Local nonce. * @property {Number} lastPong - Timestamp for last `pong` * received (unix time). * @property {Number} lastPing - Timestamp for last `ping` * sent (unix time). * @property {Number} minPing - Lowest ping time seen. * @property {Number} banScore */ class Peer extends EventEmitter { /** * Create a peer. * @alias module:net.Peer * @constructor * @param {PeerOptions} options */ constructor(options) { super(); 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.id = -1; this.socket = null; this.opened = false; this.outbound = false; this.loader = false; this.address = new NetAddress(); this.local = new NetAddress(); this.name = null; 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.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.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.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.blockMap = new BufferMap(); this.txMap = new BufferMap(); this.responseMap = new Map(); this.compactBlocks = new BufferMap(); 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) { return this.framer.packet(cmd, payload, checksum); } /** * Feed data to the parser. * @param {Buffer} data */ feedParser(data) { return this.parser.feed(data); } /** * 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; } /** * Do a reverse dns lookup on peer's addr. * @returns {Promise} */ async getName() { try { if (!this.name) { const {host, port} = this.address; const {hostname} = await dns.lookupService(host, port); this.name = hostname; } } catch (e) { ; } return this.name; } /** * 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.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 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(); 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)) 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); 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.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); if (this.responseMap.size >= common.MAX_REQUEST) { this.destroy(); return null; } } 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) => { const entry = this.request(type); if (!entry) { reject(new Error('Peer is destroyed (request).')); return; } 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.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.'); 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; } 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(common.ZERO_NONCE)) { 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; } /** * 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 = packet.locator[0]; let end = null; if (stop) end = stop; this.logger.debug( 'Requesting headers packet from peer with getheaders (%s).', this.hostname()); this.logger.debug( 'Sending getheaders (hash=%h, stop=%h).', 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 = packet.locator[0]; let end = null; if (stop) end = stop; this.logger.debug( 'Requesting inv packet from peer with getblocks (%s).', this.hostname()); this.logger.debug( 'Sending getblocks (hash=%h, stop=%h).', 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 %h (%s): code=%s reason=%s.', msg, 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} */ [inspectSymbol]() { return ''; } } /** * Max output bytes buffered before * invoking stall behavior for peer. * @const {Number} * @default */ Peer.DRAIN_MAX = 10 << 20; /** * Interval to check for drainage * and required responses from peer. * @const {Number} * @default */ Peer.STALL_INTERVAL = 5000; /** * Interval for pinging peers. * @const {Number} * @default */ Peer.PING_INTERVAL = 30000; /** * Interval to flush invs. * Higher means more invs (usually * txs) will be accumulated before * flushing. * @const {Number} * @default */ Peer.INV_INTERVAL = 5000; /** * Required time for peers to * respond to messages (i.e. * getblocks/getdata). * @const {Number} * @default */ Peer.RESPONSE_TIMEOUT = 30000; /** * Required time for loader to * respond with block/merkleblock. * @const {Number} * @default */ Peer.BLOCK_TIMEOUT = 120000; /** * Required time for loader to * respond with a tx. * @const {Number} * @default */ Peer.TX_TIMEOUT = 120000; /** * Generic timeout interval. * @const {Number} * @default */ Peer.TIMEOUT_INTERVAL = 20 * 60000; /** * Peer Options * @alias module:net.PeerOptions */ class PeerOptions { /** * Create peer options. * @constructor */ 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.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); } /** * 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; } } /** * Request Entry * @ignore */ 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; } } /* * Expose */ module.exports = Peer;