hsd
Version:
Cryptocurrency bike-shed
2,318 lines (1,855 loc) • 110 kB
JavaScript
/*!
* 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