UNPKG

mxbit

Version:
2,362 lines (1,879 loc) 88.3 kB
/*! * pool.js - peer management 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 IP = require('binet'); const dns = require('bdns'); const tcp = require('btcp'); const UPNP = require('bupnp'); const socks = require('bsocks'); const List = require('blst'); const {BloomFilter, RollingFilter} = require('bfilter'); const {BufferMap, BufferSet} = require('buffer-map'); const util = require('../utils/util'); const common = require('./common'); const chainCommon = require('../blockchain/common'); const Address = require('../primitives/address'); const BIP152 = require('./bip152'); const Network = require('../protocol/network'); const Peer = require('./peer'); const HostList = require('./hostlist'); const InvItem = require('../primitives/invitem'); const packets = require('./packets'); const services = common.services; const invTypes = InvItem.types; const packetTypes = packets.types; const scores = HostList.scores; /** * Pool * A pool of peers for handling all network activity. * @alias module:net.Pool * @extends EventEmitter */ class Pool extends EventEmitter { /** * Create a pool. * @constructor * @param {Object} options */ constructor(options) { super(); 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.locker = new Lock(true, BufferMap); this.connected = false; this.disconnecting = false; this.syncing = false; this.discovering = false; this.spvFilter = null; this.txFilter = null; this.blockMap = new BufferSet(); this.txMap = new BufferSet(); this.compactBlocks = new BufferSet(); this.invMap = new BufferMap(); this.pendingFilter = null; this.pendingRefill = null; this.checkpoints = false; this.headerChain = new List(); this.headerNext = null; this.headerTip = null; this.peers = new PeerList(); 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.mempool) this.txFilter = new RollingFilter(50000, 0.000001); this.init(); } /** * 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); 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=%h).', tip.height, 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.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() { for (const item of this.invMap.values()) item.resolve(); if (!this.connected) return; this.disconnecting = true; 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.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 DNS. * @returns {Promise} */ async discoverExternal() { const port = this.options.publicPort; // Pointless if we're not listening. if (!this.options.listen) return; // Never hit a DNS 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 = null; try { host4 = await dns.getIPv4(2000); } catch (e) { this.logger.debug('Could not find external IPv4 (dns).'); this.logger.debug(e); } if (host4 && this.hosts.addLocal(host4, port, scores.DNS)) this.logger.info('External IPv4 found (dns): %s.', host4); let host6 = null; /* try { host6 = await dns.getIPv6(2000); } catch (e) { this.logger.debug('Could not find external IPv6 (dns).'); this.logger.debug(e); } if (host6 && this.hosts.addLocal(host6, port, scores.DNS)) this.logger.info('External IPv6 found (dns): %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 (!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); // Don't send oversized getdata request if (items.length === 27000) 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 || !peer.bestHash.equals(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 %h as a %s packet (%s).', item.isTX() ? 'tx' : 'block', item.hash, '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) { const broadcasted = this.getBroadcasted(peer, item); // Check for a broadcasted item first. if (broadcasted) { peer.send(new packets.BlockPacket(broadcasted)); return true; } if (this.options.selfish || this.chain.options.spv || this.chain.options.prune) { return false; } // We have have the same serialization, we // can write the raw binary to the socket. const block = await this.chain.getRawBlock(item.hash); if (block) { peer.sendRaw('block', block); return true; } return false; } /** * Create an outbound peer with no special purpose. * @private * @param {NetAddress} addr * @returns {Peer} */ createOutbound(addr) { const peer = Peer.fromOutbound(this.options, addr); this.hosts.markAttempt(addr.hostname); 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 peer = Peer.fromInbound(this.options, socket); 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.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 > common.MAX_INV) { 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); } 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; // Request headers instead. if (this.checkpoints) return; this.logger.debug( 'Received %d 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 > common.MAX_INV) { 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)); txs += 1; continue; } switch (item.type) { case invTypes.BLOCK: { const result = await this.sendBlock(peer, item); if (!result) { notFound.push(item); continue; } blocks += 1; break; } case invTypes.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)); 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); 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 (peer.hashContinue && item.hash.equals(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: %d (%s).', 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: %h (%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) { if (packet.stop && hash.equals(packet.stop)) break; blocks.push(new InvItem(invTypes.BLOCK, hash)); 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 (packet.stop && entry.hash.equals(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(); const height = last.height + 1; if (!header.verify(height)) { this.logger.warning( 'Peer sent an invalid header (%s).', peer.hostname()); peer.increaseBan(100); peer.destroy(); return; } if (!header.prevBlock.equals(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.equals(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 %d 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(); 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(); if (!this.resolveBlock(peer, hash)) { this.logger.warning( 'Received unrequested block: %h (%s).', block.hash(), 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.equals(node.hash)) { this.logger.warning( 'Header hash mismatch %h != %h (%s).', hash, node.hash, peer.hostname()); peer.destroy(); return; } if (node.height < this.network.lastCheckpoint) { if (node.height === this.headerTip.height) { this.logger.info( 'Received checkpoint %h (%d).', 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: %h (%d).', 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=%h).', this.chain.height, block.hash()); } } /** * 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(); 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 * @pri