UNPKG

hsd

Version:
2,318 lines (1,855 loc) 110 kB
/*! * pool.js - peer management for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const EventEmitter = require('events'); const {Lock} = require('bmutex'); const IP = require('binet'); const tcp = require('btcp'); const UPNP = require('bupnp'); const socks = require('bsocks'); const List = require('blst'); const base32 = require('bcrypto/lib/encoding/base32'); const {BufferMap, BufferSet} = require('buffer-map'); const blake2b = require('bcrypto/lib/blake2b'); const {BloomFilter, RollingFilter} = require('@handshake-org/bfilter'); const rng = require('bcrypto/lib/random'); const secp256k1 = require('bcrypto/lib/secp256k1'); const {siphash} = require('bcrypto/lib/siphash'); const {lookup} = require('./lookup'); 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 consensus = require('../protocol/consensus'); const NameState = require('../covenants/namestate'); 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.brontide = 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.claimMap = new BufferSet(); this.airdropMap = new BufferSet(); this.compactBlocks = new BufferSet(); this.invMap = new BufferMap(); this.nameMap = new BufferMap(); this.pendingFilter = null; this.refillTimer = null; this.discoverTimer = null; this.connectedGroups = new BufferSet(); 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) => { try { this.handleSocket(socket, false); } catch (e) { this.emit('error', e); return; } 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.brontide.on('error', (err) => { this.emit('error', err); }); this.brontide.on('connection', (socket) => { try { this.handleSocket(socket, true); } catch (e) { this.emit('error', e); return; } this.emit('connection', socket); }); this.brontide.on('listening', () => { const data = this.brontide.address(); this.logger.info( 'Brontide 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', () => { try { if (this.checkpoints) this.resetChain(); this.forceSync(); } catch (e) { this.emit('error', e); } }); this.chain.on('full', () => { try { this.sync(); } catch (e) { this.emit('error', e); return; } this.emit('full'); this.logger.info('Chain is fully synced (height=%d).', this.chain.height); }); this.chain.on('bad orphan', (err, id) => { try { this.handleBadOrphan(packets.types.BLOCK, err, id); } catch (e) { this.emit('error', e); } }); if (this.mempool) { this.mempool.on('tx', (tx) => { this.emit('tx', tx); }); this.mempool.on('claim', (claim) => { this.emit('claim', claim); }); this.mempool.on('airdrop', (proof) => { this.emit('airdrop', proof); }); this.mempool.on('bad orphan', (err, id) => { try { this.handleBadOrphan(packets.types.TX, err, id); } catch (e) { this.emit('error', e); } }); } if (!this.options.spv) { if (this.mempool) { this.mempool.on('tx', (tx) => { try { this.announceTX(tx); } catch (e) { this.emit('error', e); } }); this.mempool.on('claim', (claim) => { try { this.announceClaim(claim); } catch (e) { this.emit('error', e); } }); this.mempool.on('airdrop', (proof) => { try { this.announceAirdrop(proof); } catch (e) { this.emit('error', e); } }); } // 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; try { this.announceBlock(block); } catch (e) { this.emit('error', e); } }); } } /** * 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.logger.info('Pool identity key: %s.', base32.encode(this.hosts.brontide.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=%x).', 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.discoverSeeds(); await this.listen(); this.fillOutbound(); 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(); this.claimMap.clear(); this.airdropMap.clear(); if (this.pendingFilter != null) { clearTimeout(this.pendingFilter); this.pendingFilter = 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.brontide); assert(!this.connected, 'Already listening.'); if (!this.options.listen) return; this.server.maxConnections = this.options.maxInbound; this.brontide.maxConnections = this.options.maxInbound; await this.server.listen(this.options.port, this.options.host); await this.brontide.listen(this.options.brontidePort, this.options.host); } /** * Stop listening on server socket. * @method * @private * @returns {Promise} */ async unlisten() { assert(this.server); assert(this.brontide); assert(this.connected, 'Not listening.'); if (!this.options.listen) return; await this.server.close(); await this.brontide.close(); } /** * Start discovery timer. * @private */ startTimer() { assert(this.refillTimer == null, 'Refill timer already started.'); assert(this.discoverTimer == null, 'Discover timer already started.'); this.refillTimer = setInterval(() => this.refill(), Pool.REFILL_INTERVAL); this.discoverTimer = setInterval(() => this.discover(), Pool.DISCOVERY_INTERVAL); } /** * Stop discovery timer. * @private */ stopTimer() { assert(this.refillTimer != null, 'Refill timer already stopped.'); assert(this.discoverTimer != null, 'Discover timer already stopped.'); clearInterval(this.refillTimer); this.refillTimer = null; clearInterval(this.discoverTimer); this.discoverTimer = 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; } 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; } if (this.hosts.addLocal(host, src, scores.UPNP)) this.logger.info('External IP found (upnp): %s.', host); 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); } } /** * Handle incoming connection. * @private * @param {net.Socket} socket */ handleSocket(socket, encrypted) { 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, encrypted); } /** * 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.connectedGroups.add(addr.getGroup()); 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; if (force) { peer.lastTip = consensus.ZERO_HASH; peer.lastStop = consensus.ZERO_HASH; } 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, consensus.ZERO_HASH); 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 === common.MAX_INV) 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.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 = []; const claims = []; const proofs = []; 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; case invTypes.CLAIM: claims.push(item.msg); break; case invTypes.AIRDROP: proofs.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); if (claims.length > 0) peer.announceClaim(claims); if (proofs.length > 0) peer.announceAirdrop(proofs); } /** * Get a block/tx from the broadcast map. * @private * @param {Peer} peer * @param {InvItem} item * @returns {Promise} */ getBroadcasted(peer, item) { let name = ''; let type = 0; if (item.isTX()) { name = 'tx'; type = invTypes.TX; } else if (item.isBlock()) { name = 'block'; type = invTypes.BLOCK; } else if (item.isClaim()) { name = 'claim'; type = invTypes.CLAIM; } else if (item.isAirdrop()) { name = 'airdrop'; type = invTypes.AIRDROP; } 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 %x (%s).', name, item.hash, 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 (item.isTX()) { if (!this.mempool) return null; return this.mempool.getTX(item.hash); } if (item.isClaim()) { if (!this.mempool) return null; return this.mempool.getClaim(item.hash); } if (item.isAirdrop()) { if (!this.mempool) return null; return this.mempool.getAirdrop(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.chain.options.spv || this.chain.options.prune) { return false; } const block = await this.chain.getRawBlock(item.hash); peer.sendRaw(packetTypes.BLOCK, block); return true; } /** * 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, encrypted) { const peer = Peer.fromInbound(this.options, socket, encrypted); 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', async () => { try { await this.handleConnect(peer); } catch (e) { this.emit('error', e); } }); peer.once('open', async () => { try { await this.handleOpen(peer); } catch (e) { this.emit('error', e); } }); peer.once('close', async (connected) => { try { await this.handleClose(peer, connected); } catch (e) { this.emit('error', e); } }); peer.once('ban', async () => { try { await this.handleBan(peer); } catch (e) { this.emit('error', e); } }); } /** * 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.GETPROOF: await this.handleGetProof(peer, packet); break; case packetTypes.PROOF: await this.handleProof(peer, packet); break; case packetTypes.CLAIM: await this.handleClaim(peer, packet); break; case packetTypes.AIRDROP: await this.handleAirdrop(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 (peer.outbound) { if (this.options.listen) { const addr = this.hosts.getLocal(peer.address); if (addr) peer.send(new packets.AddrPacket([addr])); } // Find some more peers. if (peer.version >= 3) { peer.sendGetAddr(); peer.gettingAddr = true; } } // We want compact blocks! if (this.options.compact) peer.sendCompact(this.options.blockMode); // 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); if (peer.outbound) { // Start syncing the chain. this.sendSync(peer); // Mark success. 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 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(); } } /** * 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 (peer.outbound) { this.logger.debug( 'Ignoring getaddr from outbound node (%s).', peer.hostname()); 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 (addr.hasKey()) continue; if (!peer.addrFilter.added(addr.hostname, 'ascii')) continue; items.push(addr); } if (items.length === 0) return; this.logger.debug( 'Sending %d addrs to peer (%s)', items.length, peer.hostname()); for (let i = 0; i < 1000; i += 1000) { const out = items.slice(i, i + 1000); peer.send(new packets.AddrPacket(out)); } } /** * 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 since = now - 10 * 60; const services = this.options.getRequiredServices(); const relay = []; if (addrs.length > 1000) { peer.increaseBan(100); return; } if (peer.version < 3) return; for (const addr of addrs) { peer.addrFilter.add(addr.hostname, 'ascii'); if (!addr.isRoutable()) continue; if (!addr.hasServices(services)) continue; if (addr.port === 0) continue; if (addr.hasKey()) continue; if (this.hosts.isBanned(addr.host)) continue; if (addr.time <= 100000000 || addr.time > now + 10 * 60) addr.time = now - 5 * 24 * 60 * 60; if (!peer.gettingAddr && addrs.length < 10) { if (addr.time > since) relay.push(addr); } this.hosts.add(addr, peer.address); } if (addrs.length < 1000) peer.gettingAddr = false; this.logger.info( 'Received %d addrs (hosts=%d, peers=%d) (%s).', addrs.length, this.hosts.size(), this.peers.size(), peer.hostname()); if (relay.length > 0) { const peers = []; this.logger.debug('Relaying %d addrs to random peers.', relay.length); for (let peer = this.peers.head(); peer; peer = peer.next) { if (peer.handshake) peers.push(peer); } if (peers.length > 0) { for (const addr of relay) { const [hi, lo] = siphash(addr.raw, this.hosts.key); const peer1 = peers[(hi >>> 0) % peers.length]; const peer2 = peers[(lo >>> 0) % peers.length]; const key = Buffer.from(addr.hostname, 'binary'); const msg = new packets.AddrPacket([addr]); if (peer1.addrFilter.added(key)) peer1.send(msg); if (peer2.addrFilter.added(key)) peer2.send(msg); } } } } /** * 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 = []; const claims = []; const proofs = []; 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; case invTypes.CLAIM: claims.push(item.hash); break; case invTypes.AIRDROP: proofs.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 claims=%d (%s).', items.length, blocks.length, txs.length, claims.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); if (claims.length > 0) await this.handleClaimInv(peer, claims); if (proofs.length > 0) await this.handleAirdropInv(peer, proofs); } /** * 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. if (i === hashes.length - 1) { this.logger.debug('Received existing hash (%s).', peer.hostname()); await this.getBlocks(peer, hash, consensus.ZERO_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 peer inv packet (claims). * @method * @private * @param {Peer} peer * @param {Hash[]} hashes */ async handleClaimInv(peer, hashes) { assert(hashes.length > 0); if (this.syncing && !this.chain.synced) return; this.ensureClaim(peer, hashes); } /** * Handle peer inv packet (airdrops). * @method * @private * @param {Peer} peer * @param {Hash[]} hashes */ async handleAirdropInv(peer, hashes) { assert(hashes.length > 0); if (this.syncing && !this.chain.synced) return; this.ensureAirdrop(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 claims = 0; let proofs = 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; } if (item.isClaim()) { const claim = await this.getItem(peer, item); if (!claim) { notFound.push(item); continue; } peer.send(new packets.ClaimPacket(claim)); claims += 1; continue; } if (item.isAirdrop()) { const proof = await this.getItem(peer, item); if (!proof) { notFound.push(item); continue; } peer.send(new packets.AirdropPacket(proof)); proofs += 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 (item.hash.equals(peer.hashContinue)) { peer.sendInv([new InvItem(invTypes.BLOCK, this.chain.tip.hash)]); peer.hashContinue = consensus.ZERO_HASH; } // 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 (claims > 0) { this.logger.debug( 'Served %d claims with getdata (notfound=%d) (%s).', claims, notFound.length, peer.hostname()); } if (proofs > 0) { this.logger.debug( 'Served %d airdrops with getdata (notfound=%d) (%s).', proofs, notFound.length, peer.hostname()); } if (unknown !== -1) { this.logger.warning( 'Peer sent an unknown getdata type: %d (%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: %x (%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.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 (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.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.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()) { 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 he