/*!
* peer.js - peer object 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 {format} = require('util');
const tcp = require('btcp');
const dns = require('bdns');
const Logger = require('blgr');
const {RollingFilter} = require('bfilter');
const {BufferMap} = require('buffer-map');
const Parser = require('./parser');
const Framer = require('./framer');
const packets = require('./packets');
const consensus = require('../protocol/consensus');
const common = require('./common');
const InvItem = require('../primitives/invitem');
const BIP152 = require('./bip152');
const Block = require('../primitives/block');
const TX = require('../primitives/tx');
const Claim = require('../primitives/claim');
const NetAddress = require('./netaddress');
const Network = require('../protocol/network');
const {BrontideStream} = require('./brontide');
const AirdropProof = require('../primitives/airdropproof');
const SlidingWindow = require('./slidingwindow');
const services = common.services;
const invTypes = InvItem.types;
const packetTypes = packets.types;
/**
* Represents a network peer.
* @alias module:net.Peer
* @extends EventEmitter
*/
class Peer extends EventEmitter {
/**
* Create a peer.
* @alias module:net.Peer
* @constructor
* @param {PeerOptions} options
*/
constructor(options) {
super();
this.options = new PeerOptions(options);
this.network = this.options.network;
this.logger = this.options.logger.context('peer');
this.locker = new Lock();
this.parser = new Parser(this.network);
this.framer = new Framer(this.network);
this.id = -1;
this.stream = null;
this.socket = null;
this.brontide = new BrontideStream();
this.encrypted = false;
this.identityKey = this.options.identityKey;
this.opened = false;
this.outbound = false;
this.loader = false;
this.address = new NetAddress();
this.local = new NetAddress();
this.name = null;
this.connected = false;
this.destroyed = false;
this.ack = false;
this.handshake = false;
this.time = 0;
this.lastSend = 0;
this.lastRecv = 0;
this.drainSize = 0;
this.drainQueue = [];
this.banScore = 0;
this.invQueue = [];
this.onPacket = null;
this.next = null;
this.prev = null;
this.version = -1;
this.services = 0;
this.height = -1;
this.agent = null;
this.noRelay = false;
this.preferHeaders = false;
this.hashContinue = consensus.ZERO_HASH;
this.spvFilter = null;
this.feeRate = -1;
this.compactMode = -1;
this.merkleBlock = null;
this.merkleTime = -1;
this.merkleMatches = 0;
this.merkleMap = null;
this.syncing = false;
this.sentAddr = false;
this.gettingAddr = false;
this.sentGetAddr = false;
this.challenge = null;
this.lastPong = -1;
this.lastPing = -1;
this.minPing = -1;
this.blockTime = -1;
this.bestHash = consensus.ZERO_HASH;
this.bestHeight = -1;
this.lastTip = consensus.ZERO_HASH;
this.lastStop = consensus.ZERO_HASH;
this.connectTimeout = null;
this.pingTimer = null;
this.invTimer = null;
this.stallTimer = null;
this.addrFilter = new RollingFilter(5000, 0.001);
this.invFilter = new RollingFilter(50000, 0.000001);
this.blockMap = new BufferMap();
this.txMap = new BufferMap();
this.claimMap = new BufferMap();
this.airdropMap = new BufferMap();
this.responseMap = new Map();
this.compactBlocks = new BufferMap();
this.nameMap = new BufferMap();
this.totalProofs = 0;
this.proofWindow = null;
this.init();
}
/**
* Create inbound peer from socket.
* @param {PeerOptions} options
* @param {net.Socket} socket
* @returns {Peer}
*/
static fromInbound(options, socket, encrypted) {
const peer = new this(options);
peer.accept(socket, encrypted);
return peer;
}
/**
* Create outbound peer from net address.
* @param {PeerOptions} options
* @param {NetAddress} addr
* @returns {Peer}
*/
static fromOutbound(options, addr) {
const peer = new this(options);
peer.connect(addr);
return peer;
}
/**
* Begin peer initialization.
* @private
*/
init() {
this.parser.on('packet', async (packet) => {
try {
await this.readPacket(packet);
} catch (e) {
this.error(e);
this.destroy();
}
});
this.parser.on('error', (err) => {
if (this.destroyed)
return;
this.error(err);
try {
this.sendReject('malformed', 'error parsing message');
this.increaseBan(10);
} catch (e) {
this.error(e);
}
});
}
/**
* Getter to retrieve hostname.
* @returns {String}
*/
hostname() {
return this.address.hostname;
}
/**
* Frame a payload with a header.
* @param {String} cmd - Packet type.
* @param {Buffer} payload
* @returns {Buffer} Payload with header prepended.
*/
framePacket(cmd, payload) {
return this.framer.packet(cmd, payload);
}
/**
* Feed data to the parser.
* @param {Buffer} data
*/
feedParser(data) {
return this.parser.feed(data);
}
/**
* Bind to socket.
* @param {net.Socket} socket
*/
_bind(socket, encrypted) {
assert(!this.socket);
this.socket = socket;
this.encrypted = encrypted;
this.stream = encrypted ? this.brontide : this.socket;
this.brontide.on('error', (err) => {
this.error(err);
this.destroy();
});
this.socket.on('error', (err) => {
if (!this.connected)
return;
this.error(err);
this.destroy();
});
this.socket.once('close', () => {
this.error('Socket hangup.');
this.destroy();
});
this.socket.on('drain', () => {
this.handleDrain();
});
this.stream.on('data', (chunk) => {
try {
this.lastRecv = Date.now();
this.feedParser(chunk);
} catch (e) {
this.error(e);
this.destroy();
}
});
this.socket.setNoDelay(true);
}
/**
* Accept an inbound socket.
* @param {net.Socket} socket
* @returns {net.Socket}
*/
accept(socket, encrypted) {
assert(!this.socket);
this.address = NetAddress.fromSocket(socket, this.network);
this.address.services = 0;
this.outbound = false;
this._bind(socket, encrypted);
if (encrypted) {
this.connected = false;
this.brontide.accept(socket, this.identityKey);
} else {
this.time = Date.now();
this.connected = true;
}
return socket;
}
/**
* Create the socket and begin connecting. This method
* will use `options.createSocket` if provided.
* @param {NetAddress} addr
* @returns {net.Socket}
*/
connect(addr) {
assert(!this.socket);
const socket = this.options.createSocket(addr.port, addr.host);
this.address = addr;
this.outbound = true;
this.connected = false;
this._bind(socket, addr.hasKey());
if (addr.hasKey())
this.brontide.connect(socket, this.identityKey, addr.key);
return socket;
}
/**
* Do a reverse dns lookup on peer's addr.
* @returns {Promise}
*/
async getName() {
try {
if (!this.name) {
const {host, port} = this.address;
const {hostname} = await dns.lookupService(host, port);
this.name = hostname;
}
} catch (e) {
;
}
return this.name;
}
/**
* Open and perform initial handshake (without rejection).
* @method
* @returns {Promise}
*/
async tryOpen() {
try {
await this.open();
} catch (e) {
;
}
}
/**
* Open and perform initial handshake.
* @method
* @returns {Promise}
*/
async open() {
try {
await this._open();
} catch (e) {
this.error(e);
this.destroy();
throw e;
}
}
/**
* Open and perform initial handshake.
* @method
* @returns {Promise}
*/
async _open() {
this.opened = true;
// Connect to peer.
await this.initConnect();
await this.initStall();
await this.initVersion();
await this.finalize();
assert(!this.destroyed);
// Finally we can let the pool know
// that this peer is ready to go.
this.emit('open');
}
/**
* Wait for connection.
* @private
* @returns {Promise}
*/
async initConnect() {
if (this.connected) {
assert(!this.outbound);
return Promise.resolve();
}
assert(this.stream);
assert(this.socket);
return new Promise((resolve, reject) => {
const cleanup = () => {
if (this.connectTimeout != null) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
if (this.socket) {
// eslint-disable-next-line no-use-before-define
this.socket.removeListener('error', onError);
}
// eslint-disable-next-line no-use-before-define
this.brontide.removeListener('error', onError);
};
const onError = (err) => {
cleanup();
reject(err);
};
this.stream.once('connect', () => {
this.time = Date.now();
this.connected = true;
this.emit('connect');
cleanup();
resolve();
});
this.socket.once('error', onError);
this.brontide.once('error', onError);
this.connectTimeout = setTimeout(() => {
this.connectTimeout = null;
cleanup();
reject(new Error('Connection timed out.'));
}, Peer.CONNECT_TIMEOUT);
});
}
/**
* Setup stall timer.
* @private
* @returns {Promise}
*/
initStall() {
assert(!this.stallTimer);
assert(!this.destroyed);
this.stallTimer = setInterval(() => {
this.maybeTimeout();
}, Peer.STALL_INTERVAL);
return Promise.resolve();
}
/**
* Handle post handshake.
* @method
* @private
* @returns {Promise}
*/
async initVersion() {
assert(!this.destroyed);
if (this.outbound) {
if (this.version !== -1)
throw new Error('Peer prematurely introduced themselves (outbound).');
if (this.ack)
throw new Error('Peer prematurely acknowledged us (outbound).');
// Say hello.
this.sendVersion();
await this.wait(packetTypes.VERACK, Peer.HANDSHAKE_TIMEOUT);
assert(this.ack);
if (this.version === -1)
await this.wait(packetTypes.VERSION, Peer.HANDSHAKE_TIMEOUT);
assert(this.version !== -1);
} else {
// We're shy. Wait for an introduction.
if (this.version === -1)
await this.wait(packetTypes.VERSION, Peer.HANDSHAKE_TIMEOUT);
assert(this.version !== -1);
if (this.ack)
throw new Error('Peer prematurely acknowledged us (inbound).');
this.sendVersion();
await this.wait(packetTypes.VERACK, Peer.HANDSHAKE_TIMEOUT);
}
if (this.destroyed)
throw new Error('Peer was destroyed during handshake.');
this.handshake = true;
this.logger.debug('Version handshake complete (%s).', this.hostname());
}
/**
* Finalize peer after handshake.
* @method
* @private
* @returns {Promise}
*/
async finalize() {
assert(!this.destroyed);
// Setup the ping interval.
this.pingTimer = setInterval(() => {
this.sendPing();
}, Peer.PING_INTERVAL);
// Setup the inv flusher.
this.invTimer = setInterval(() => {
this.flushInv();
}, Peer.INV_INTERVAL);
this.proofWindow = new SlidingWindow({
limit: this.options.maxProofRPS
});
this.proofWindow.start();
}
/**
* Broadcast blocks to peer.
* @param {Block[]} blocks
*/
announceBlock(blocks) {
if (!this.handshake)
return;
if (this.destroyed)
return;
if (!Array.isArray(blocks))
blocks = [blocks];
const inv = [];
for (const block of blocks) {
assert(block instanceof Block);
// Don't send if they already have it.
if (this.invFilter.test(block.hash()))
continue;
// Send them the block immediately if
// they're using compact block mode 1.
if (this.compactMode === 1) {
this.invFilter.add(block.hash());
this.sendCompactBlock(block);
continue;
}
// Convert item to block headers
// for peers that request it.
if (this.preferHeaders) {
inv.push(block.toHeaders());
continue;
}
inv.push(block.toInv());
}
if (this.preferHeaders) {
this.sendHeaders(inv);
return;
}
this.queueInv(inv);
}
/**
* Broadcast transactions to peer.
* @param {TX[]} txs
*/
announceTX(txs) {
if (!this.handshake)
return;
if (this.destroyed)
return;
// Do not send txs to spv clients
// that have relay unset.
if (this.noRelay)
return;
if (!Array.isArray(txs))
txs = [txs];
const inv = [];
for (const tx of txs) {
assert(tx instanceof TX);
// Don't send if they already have it.
if (this.invFilter.test(tx.hash()))
continue;
// Check the peer's bloom
// filter if they're using spv.
if (this.spvFilter) {
if (!tx.test(this.spvFilter))
continue;
}
// Check the fee filter.
if (this.feeRate !== -1) {
const rate = this.options.getRate(tx.hash());
if (rate !== -1 && rate < this.feeRate)
continue;
}
inv.push(tx.toInv());
}
this.queueInv(inv);
}
/**
* Broadcast transactions to peer.
* @param {Claim[]} claims
*/
announceClaim(claims) {
if (!this.handshake)
return;
if (this.destroyed)
return;
// Do not send claims to spv clients
// that have relay unset.
if (this.noRelay)
return;
if (!Array.isArray(claims))
claims = [claims];
const inv = [];
for (const claim of claims) {
assert(claim instanceof Claim);
// Don't send if they already have it.
if (this.invFilter.test(claim.hash()))
continue;
inv.push(claim.toInv());
}
this.queueInv(inv);
}
/**
* Broadcast transactions to peer.
* @param {AirdropProof[]} proofs
*/
announceAirdrop(proofs) {
if (!this.handshake)
return;
if (this.destroyed)
return;
// Do not send proofs to spv clients
// that have relay unset.
if (this.noRelay)
return;
if (!Array.isArray(proofs))
proofs = [proofs];
const inv = [];
for (const proof of proofs) {
assert(proof instanceof AirdropProof);
// Don't send if they already have it.
if (this.invFilter.test(proof.hash()))
continue;
inv.push(proof.toInv(InvItem));
}
this.queueInv(inv);
}
/**
* Send inv to a peer.
* @param {InvItem[]} items
*/
queueInv(items) {
if (!this.handshake)
return;
if (this.destroyed)
return;
if (!Array.isArray(items))
items = [items];
let hasBlock = false;
for (const item of items) {
if (item.type === invTypes.BLOCK)
hasBlock = true;
this.invQueue.push(item);
}
if (this.invQueue.length >= 500 || hasBlock)
this.flushInv();
}
/**
* Flush inv queue.
* @private
*/
flushInv() {
if (this.destroyed)
return;
const queue = this.invQueue;
if (queue.length === 0)
return;
this.invQueue = [];
this.logger.spam('Serving %d inv items to %s.',
queue.length, this.hostname());
const items = [];
for (const item of queue) {
if (!this.invFilter.added(item.hash))
continue;
items.push(item);
}
for (let i = 0; i < items.length; i += 1000) {
const chunk = items.slice(i, i + 1000);
this.send(new packets.InvPacket(chunk));
}
}
/**
* Force send an inv (no filter check).
* @param {InvItem[]} items
*/
sendInv(items) {
if (!this.handshake)
return;
if (this.destroyed)
return;
if (!Array.isArray(items))
items = [items];
for (const item of items)
this.invFilter.add(item.hash);
if (items.length === 0)
return;
this.logger.spam('Serving %d inv items to %s.',
items.length, this.hostname());
for (let i = 0; i < items.length; i += 1000) {
const chunk = items.slice(i, i + 1000);
this.send(new packets.InvPacket(chunk));
}
}
/**
* Send headers to a peer.
* @param {Headers[]} items
*/
sendHeaders(items) {
if (!this.handshake)
return;
if (this.destroyed)
return;
if (!Array.isArray(items))
items = [items];
for (const item of items)
this.invFilter.add(item.hash());
if (items.length === 0)
return;
this.logger.spam('Serving %d headers to %s.',
items.length, this.hostname());
for (let i = 0; i < items.length; i += 2000) {
const chunk = items.slice(i, i + 2000);
this.send(new packets.HeadersPacket(chunk));
}
}
/**
* Send a compact block.
* @private
* @param {Block} block
* @returns {Boolean}
*/
sendCompactBlock(block) {
const compact = BIP152.CompactBlock.fromBlock(block);
this.send(new packets.CmpctBlockPacket(compact));
}
/**
* Send a `version` packet.
*/
sendVersion() {
const packet = new packets.VersionPacket();
packet.version = this.options.version;
packet.services = this.options.services;
packet.time = this.network.now();
packet.remote = this.address;
packet.nonce = this.options.createNonce(this.hostname());
packet.agent = this.options.agent;
packet.height = this.options.getHeight();
packet.noRelay = this.options.noRelay;
this.send(packet);
}
/**
* Send a `getaddr` packet.
*/
sendGetAddr() {
if (this.sentGetAddr)
return;
this.sentGetAddr = true;
this.send(new packets.GetAddrPacket());
}
/**
* Send a `ping` packet.
*/
sendPing() {
if (!this.handshake)
return;
if (this.challenge) {
this.logger.debug(
'Peer has not responded to ping (%s).',
this.hostname());
return;
}
this.lastPing = Date.now();
this.challenge = common.nonce();
this.send(new packets.PingPacket(this.challenge));
}
/**
* Send `filterload` to update the local bloom filter.
*/
sendFilterLoad(filter) {
if (!this.handshake)
return;
if (!this.options.spv)
return;
if (!(this.services & services.BLOOM))
return;
this.send(new packets.FilterLoadPacket(filter));
}
/**
* Set a fee rate filter for the peer.
* @param {Rate} rate
*/
sendFeeRate(rate) {
if (!this.handshake)
return;
this.send(new packets.FeeFilterPacket(rate));
}
/**
* Disconnect from and destroy the peer.
*/
destroy() {
const connected = this.connected;
if (this.destroyed)
return;
this.destroyed = true;
this.connected = false;
this.socket.destroy();
this.socket = null;
if (this.pingTimer != null) {
clearInterval(this.pingTimer);
this.pingTimer = null;
}
if (this.invTimer != null) {
clearInterval(this.invTimer);
this.invTimer = null;
}
if (this.proofWindow != null) {
this.proofWindow.stop();
}
if (this.stallTimer != null) {
clearInterval(this.stallTimer);
this.stallTimer = null;
}
if (this.connectTimeout != null) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
const jobs = this.drainQueue;
this.drainSize = 0;
this.drainQueue = [];
for (const job of jobs)
job.reject(new Error('Peer was destroyed.'));
for (const [cmd, entry] of this.responseMap) {
this.responseMap.delete(cmd);
entry.reject(new Error('Peer was destroyed.'));
}
this.locker.destroy();
this.emit('close', connected);
}
/**
* Write data to the peer's socket.
* @param {Buffer} data
*/
write(data) {
if (this.destroyed)
throw new Error('Peer is destroyed (write).');
this.lastSend = Date.now();
if (this.stream.write(data) === false)
this.needsDrain(data.length);
}
/**
* Send a packet.
* @param {Packet} packet
*/
send(packet) {
if (this.destroyed)
throw new Error('Peer is destroyed (send).');
this.sendRaw(packet.rawType, packet.encode());
this.addTimeout(packet);
}
/**
* Send a packet.
* @param {Packet} packet
*/
sendRaw(type, body) {
const payload = this.framePacket(type, body);
this.write(payload);
}
/**
* Wait for a drain event.
* @returns {Promise}
*/
drain() {
if (this.destroyed)
return Promise.reject(new Error('Peer is destroyed.'));
if (this.drainSize === 0)
return Promise.resolve();
return new Promise((resolve, reject) => {
this.drainQueue.push({ resolve, reject });
});
}
/**
* Handle drain event.
* @private
*/
handleDrain() {
const jobs = this.drainQueue;
this.drainSize = 0;
if (jobs.length === 0)
return;
this.drainQueue = [];
for (const job of jobs)
job.resolve();
}
/**
* Add to drain counter.
* @private
* @param {Number} size
*/
needsDrain(size) {
this.drainSize += size;
if (this.drainSize >= Peer.DRAIN_MAX) {
this.logger.warning(
'Peer is not reading: %dmb buffered (%s).',
this.drainSize / (1 << 20),
this.hostname());
this.error('Peer stalled (drain).');
this.destroy();
}
}
/**
* Potentially add response timeout.
* @private
* @param {Packet} packet
*/
addTimeout(packet) {
const timeout = Peer.RESPONSE_TIMEOUT;
if (!this.outbound)
return;
switch (packet.type) {
case packetTypes.GETBLOCKS:
if (!this.options.isFull())
this.request(packetTypes.INV, timeout);
break;
case packetTypes.GETHEADERS:
this.request(packetTypes.HEADERS, timeout * 2);
break;
case packetTypes.GETDATA:
this.request(packetTypes.DATA, timeout * 2);
break;
case packetTypes.GETBLOCKTXN:
this.request(packetTypes.BLOCKTXN, timeout);
break;
case packetTypes.GETPROOF:
this.request(packetTypes.PROOF, timeout);
break;
}
}
/**
* Potentially finish response timeout.
* @private
* @param {Packet} packet
*/
fulfill(packet) {
switch (packet.type) {
case packetTypes.BLOCK:
case packetTypes.CMPCTBLOCK:
case packetTypes.MERKLEBLOCK:
case packetTypes.TX:
case packetTypes.CLAIM:
case packetTypes.AIRDROP:
case packetTypes.NOTFOUND: {
const entry = this.response(packetTypes.DATA, packet);
assert(!entry || entry.jobs.length === 0);
break;
}
}
return this.response(packet.type, packet);
}
/**
* Potentially timeout peer if it hasn't responded.
* @private
*/
maybeTimeout() {
const now = Date.now();
for (const [key, entry] of this.responseMap) {
if (now > entry.timeout) {
const name = packets.typesByVal[key];
this.error('Peer is stalling (%s).', name.toLowerCase());
this.destroy();
return;
}
}
if (this.merkleBlock) {
assert(this.merkleTime !== -1);
if (now > this.merkleTime + Peer.BLOCK_TIMEOUT) {
this.error('Peer is stalling (merkleblock).');
this.destroy();
return;
}
}
if (this.syncing && this.loader && !this.options.isFull()) {
if (now > this.blockTime + Peer.BLOCK_TIMEOUT) {
this.error('Peer is stalling (block).');
this.destroy();
return;
}
}
if (this.options.isFull() || !this.syncing) {
for (const time of this.blockMap.values()) {
if (now > time + Peer.BLOCK_TIMEOUT) {
this.error('Peer is stalling (block).');
this.destroy();
return;
}
}
for (const time of this.txMap.values()) {
if (now > time + Peer.TX_TIMEOUT) {
this.error('Peer is stalling (tx).');
this.destroy();
return;
}
}
for (const time of this.claimMap.values()) {
if (now > time + Peer.TX_TIMEOUT) {
this.error('Peer is stalling (claim).');
this.destroy();
return;
}
}
for (const time of this.airdropMap.values()) {
if (now > time + Peer.TX_TIMEOUT) {
this.error('Peer is stalling (airdrop).');
this.destroy();
return;
}
}
for (const block of this.compactBlocks.values()) {
if (now > block.now + Peer.RESPONSE_TIMEOUT) {
this.error('Peer is stalling (blocktxn).');
this.destroy();
return;
}
}
}
for (const time of this.nameMap.values()) {
if (now > time + Peer.NAME_TIMEOUT) {
this.error('Peer is stalling (name).');
this.destroy();
return;
}
}
if (now > this.time + 60000) {
assert(this.time !== 0);
if (this.lastRecv === 0 || this.lastSend === 0) {
this.error('Peer is stalling (no message).');
this.destroy();
return;
}
if (now > this.lastSend + Peer.TIMEOUT_INTERVAL) {
this.error('Peer is stalling (send).');
this.destroy();
return;
}
if (now > this.lastRecv + Peer.TIMEOUT_INTERVAL) {
this.error('Peer is stalling (recv).');
this.destroy();
return;
}
if (this.challenge && now > this.lastPing + Peer.TIMEOUT_INTERVAL) {
this.error('Peer is stalling (ping).');
this.destroy();
return;
}
}
}
/**
* Wait for a packet to be received from peer.
* @private
* @param {Number} type - Packet type.
* @param {Number} timeout
* @returns {RequestEntry}
*/
request(type, timeout) {
if (this.destroyed)
return null;
let entry = this.responseMap.get(type);
if (!entry) {
entry = new RequestEntry();
this.responseMap.set(type, entry);
if (this.responseMap.size >= common.MAX_REQUEST) {
this.destroy();
return null;
}
}
entry.setTimeout(timeout);
return entry;
}
/**
* Fulfill awaiting requests created with {@link Peer#request}.
* @private
* @param {Number} type - Packet type.
* @param {Object} payload
*/
response(type, payload) {
const entry = this.responseMap.get(type);
if (!entry)
return null;
this.responseMap.delete(type);
return entry;
}
/**
* Wait for a packet to be received from peer.
* @private
* @param {Number} type - Packet type.
* @returns {Promise} - Returns Object(payload).
* Executed on timeout or once packet is received.
*/
wait(type, timeout) {
return new Promise((resolve, reject) => {
const entry = this.request(type);
if (!entry) {
reject(new Error('Peer is destroyed (request).'));
return;
}
entry.setTimeout(timeout);
entry.addJob(resolve, reject);
});
}
/**
* Emit an error and destroy the peer.
* @private
* @param {...String|Error} err
*/
error(err) {
if (this.destroyed)
return;
if (typeof err === 'string') {
const msg = format.apply(null, arguments);
err = new Error(msg);
}
if (typeof err.code === 'string' && err.code[0] === 'E') {
const msg = err.code;
err = new Error(msg);
err.code = msg;
err.message = `Socket Error: ${msg}`;
}
err.message += ` (${this.hostname()})`;
this.emit('error', err);
}
/**
* Calculate peer block inv type (filtered,
* compact, witness, or non-witness).
* @returns {Number}
*/
blockType() {
if (this.options.spv)
return invTypes.FILTERED_BLOCK;
if (this.options.compact && this.hasCompact())
return invTypes.CMPCT_BLOCK;
return invTypes.BLOCK;
}
/**
* Calculate peer tx inv type (witness or non-witness).
* @returns {Number}
*/
txType() {
return invTypes.TX;
}
/**
* Send `getdata` to peer.
* @param {InvItem[]} items
*/
getData(items) {
this.send(new packets.GetDataPacket(items));
}
/**
* Send batched `getdata` to peer.
* @param {InvType} type
* @param {Hash[]} hashes
*/
getItems(type, hashes) {
const items = [];
for (const hash of hashes)
items.push(new InvItem(type, hash));
if (items.length === 0)
return;
this.getData(items);
}
/**
* Send batched `getdata` to peer (blocks).
* @param {Hash[]} hashes
*/
getBlock(hashes) {
this.getItems(this.blockType(), hashes);
}
/**
* Send batched `getdata` to peer (txs).
* @param {Hash[]} hashes
*/
getTX(hashes) {
this.getItems(this.txType(), hashes);
}
/**
* Send batched `getdata` to peer (claims).
* @param {Hash[]} hashes
*/
getClaim(hashes) {
this.getItems(invTypes.CLAIM, hashes);
}
/**
* Send batched `getdata` to peer (airdrops).
* @param {Hash[]} hashes
*/
getAirdrop(hashes) {
this.getItems(invTypes.AIRDROP, hashes);
}
/**
* Send `getdata` to peer for a single block.
* @param {Hash} hash
*/
getFullBlock(hash) {
assert(!this.options.spv);
this.getItems(invTypes.BLOCK, [hash]);
}
/**
* Handle a packet payload.
* @method
* @private
* @param {Packet} packet
*/
async readPacket(packet) {
if (this.destroyed)
return;
// The "pre-handshake" packets get
// to bypass the lock, since they
// are meant to change the way input
// is handled at a low level. They
// must be handled immediately.
switch (packet.type) {
case packetTypes.PONG: {
try {
this.socket.pause();
await this.handlePacket(packet);
} finally {
if (!this.destroyed && this.socket) {
try {
this.socket.resume();
} catch (e) {
;
}
}
}
break;
}
default: {
const unlock = await this.locker.lock();
try {
this.socket.pause();
await this.handlePacket(packet);
} finally {
if (!this.destroyed && this.socket) {
try {
this.socket.resume();
} catch (e) {
;
}
}
unlock();
}
break;
}
}
}
/**
* Handle a packet payload without a lock.
* @method
* @private
* @param {Packet} packet
*/
async handlePacket(packet) {
if (this.destroyed)
throw new Error('Destroyed peer sent a packet.');
const entry = this.fulfill(packet);
switch (packet.type) {
case packetTypes.VERSION:
await this.handleVersion(packet);
break;
case packetTypes.VERACK:
await this.handleVerack(packet);
break;
case packetTypes.PING:
await this.handlePing(packet);
break;
case packetTypes.PONG:
await this.handlePong(packet);
break;
case packetTypes.SENDHEADERS:
await this.handleSendHeaders(packet);
break;
case packetTypes.FILTERLOAD:
await this.handleFilterLoad(packet);
break;
case packetTypes.FILTERADD:
await this.handleFilterAdd(packet);
break;
case packetTypes.FILTERCLEAR:
await this.handleFilterClear(packet);
break;
case packetTypes.FEEFILTER:
await this.handleFeeFilter(packet);
break;
case packetTypes.SENDCMPCT:
await this.handleSendCmpct(packet);
break;
}
if (this.onPacket)
await this.onPacket(packet);
this.emit('packet', packet);
if (entry)
entry.resolve(packet);
}
/**
* Handle `version` packet.
* @method
* @private
* @param {VersionPacket} packet
*/
async handleVersion(packet) {
if (this.version !== -1)
throw new Error('Peer sent a duplicate version.');
this.version = packet.version;
this.services = packet.services;
this.height = packet.height;
this.agent = packet.agent;
this.noRelay = packet.noRelay;
this.local = packet.remote;
if (!this.network.selfConnect) {
if (this.options.hasNonce(packet.nonce))
throw new Error('We connected to ourself. Oops.');
}
if (this.version < common.MIN_VERSION)
throw new Error('Peer does not support required protocol version.');
if (this.outbound) {
if (!(this.services & services.NETWORK))
throw new Error('Peer does not support network services.');
if (this.options.spv) {
if (!(this.services & services.BLOOM))
throw new Error('Peer does not support BIP37.');
}
}
this.send(new packets.VerackPacket());
}
/**
* Handle `verack` packet.
* @method
* @private
* @param {VerackPacket} packet
*/
async handleVerack(packet) {
if (this.ack) {
this.logger.debug('Peer sent duplicate ack (%s).', this.hostname());
return;
}
this.ack = true;
this.logger.debug('Received verack (%s).', this.hostname());
}
/**
* Handle `ping` packet.
* @method
* @private
* @param {PingPacket} packet
*/
async handlePing(packet) {
if (!packet.nonce)
return;
this.send(new packets.PongPacket(packet.nonce));
}
/**
* Handle `pong` packet.
* @method
* @private
* @param {PongPacket} packet
*/
async handlePong(packet) {
const nonce = packet.nonce;
const now = Date.now();
if (!this.challenge) {
this.logger.debug('Peer sent an unsolicited pong (%s).', this.hostname());
return;
}
if (!nonce.equals(this.challenge)) {
if (nonce.equals(common.ZERO_NONCE)) {
this.logger.debug('Peer sent a zero nonce (%s).', this.hostname());
this.challenge = null;
return;
}
this.logger.debug('Peer sent the wrong nonce (%s).', this.hostname());
return;
}
if (now >= this.lastPing) {
this.lastPong = now;
if (this.minPing === -1)
this.minPing = now - this.lastPing;
this.minPing = Math.min(this.minPing, now - this.lastPing);
} else {
this.logger.debug('Timing mismatch (what?) (%s).', this.hostname());
}
this.challenge = null;
}
/**
* Handle `sendheaders` packet.
* @method
* @private
* @param {SendHeadersPacket} packet
*/
async handleSendHeaders(packet) {
if (this.preferHeaders) {
this.logger.debug(
'Peer sent a duplicate sendheaders (%s).',
this.hostname());
return;
}
this.preferHeaders = true;
}
/**
* Handle `filterload` packet.
* @method
* @private
* @param {FilterLoadPacket} packet
*/
async handleFilterLoad(packet) {
if (!packet.isWithinConstraints()) {
this.increaseBan(100);
return;
}
this.spvFilter = packet.filter;
this.noRelay = false;
}
/**
* Handle `filteradd` packet.
* @method
* @private
* @param {FilterAddPacket} packet
*/
async handleFilterAdd(packet) {
const data = packet.data;
if (data.length > consensus.MAX_SCRIPT_PUSH) {
this.increaseBan(100);
return;
}
if (this.spvFilter)
this.spvFilter.add(data);
this.noRelay = false;
}
/**
* Handle `filterclear` packet.
* @method
* @private
* @param {FilterClearPacket} packet
*/
async handleFilterClear(packet) {
if (this.spvFilter)
this.spvFilter.reset();
this.noRelay = false;
}
/**
* Handle `feefilter` packet.
* @method
* @private
* @param {FeeFilterPacket} packet
*/
async handleFeeFilter(packet) {
const rate = packet.rate;
if (rate < 0 || rate > consensus.MAX_MONEY) {
this.increaseBan(100);
return;
}
this.feeRate = rate;
}
/**
* Handle `sendcmpct` packet.
* @method
* @private
* @param {SendCmpctPacket}
*/
async handleSendCmpct(packet) {
if (this.compactMode !== -1) {
this.logger.debug(
'Peer sent a duplicate sendcmpct (%s).',
this.hostname());
return;
}
if (packet.version > 1) {
// Ignore
this.logger.info(
'Peer request compact blocks version %d (%s).',
packet.version, this.hostname());
return;
}
if (packet.mode > 1) {
this.logger.info(
'Peer request compact blocks mode %d (%s).',
packet.mode, this.hostname());
return;
}
this.logger.info(
'Peer initialized compact blocks (mode=%d, version=%d) (%s).',
packet.mode, packet.version, this.hostname());
this.compactMode = packet.mode;
}
/**
* Send `getheaders` to peer. Note that unlike
* `getblocks`, `getheaders` can have a null locator.
* @param {Hash[]} locator - Chain locator.
* @param {Hash} stop - Hash to stop at.
*/
sendGetHeaders(locator, stop) {
const packet = new packets.GetHeadersPacket(locator, stop);
let hash = consensus.ZERO_HASH;
if (packet.locator.length > 0)
hash = packet.locator[0];
this.logger.debug(
'Requesting headers packet from peer with getheaders (%s).',
this.hostname());
this.logger.debug(
'Sending getheaders (hash=%x, stop=%x).',
hash, stop);
this.send(packet);
}
/**
* Send `getblocks` to peer.
* @param {Hash[]} locator - Chain locator.
* @param {Hash} stop - Hash to stop at.
*/
sendGetBlocks(locator, stop) {
const packet = new packets.GetBlocksPacket(locator, stop);
let hash = consensus.ZERO_HASH;
if (packet.locator.length > 0)
hash = packet.locator[0];
if (hash.equals(this.lastTip) && stop.equals(this.lastStop))
return;
this.lastTip = hash;
this.lastStop = stop;
this.logger.debug(
'Requesting inv packet from peer with getblocks (%s).',
this.hostname());
this.logger.debug(
'Sending getblocks (hash=%x, stop=%x).',
hash, stop);
this.send(packet);
}
/**
* Send `mempool` to peer.
*/
sendMempool() {
if (!this.handshake)
return;
if (!(this.services & services.BLOOM)) {
this.logger.debug(
'Cannot request mempool for non-bloom peer (%s).',
this.hostname());
return;
}
this.logger.debug(
'Requesting inv packet from peer with mempool (%s).',
this.hostname());
this.send(new packets.MempoolPacket());
}
/**
* Send `reject` to peer.
* @param {Number} code
* @param {String} reason
* @param {Number} msg
* @param {Hash} hash
*/
sendReject(code, reason, msg, hash) {
const reject = packets.RejectPacket.fromReason(code, reason, msg, hash);
if (msg != null) {
this.logger.debug('Rejecting %s %x (%s): code=%s reason=%s.',
packets.typesByVal[msg] || 'UNKNOWN',
hash, this.hostname(), code, reason);
} else {
this.logger.debug('Rejecting packet from %s: code=%s reason=%s.',
this.hostname(), code, reason);
}
this.logger.debug(
'Sending reject packet to peer (%s).',
this.hostname());
this.send(reject);
}
/**
* Send a `sendcmpct` packet.
* @param {Number} mode
*/
sendCompact(mode) {
this.logger.info(
'Initializing normal compact blocks (%s).',
this.hostname());
this.send(new packets.SendCmpctPacket(mode, 1));
}
/**
* Send a `getproof` packet.
* @param {Buffer} root
* @param {Buffer} key
*/
sendGetProof(root, key) {
this.logger.info(
'Sending proof request for %x (%s).',
key,
this.hostname());
this.send(new packets.GetProofPacket(root, key));
}
/**
* Send a `proof` packet.
* @param {Number} version
* @param {Buffer} root
* @param {Buffer} key
* @param {Proof} proof
*/
sendProof(root, key, proof) {
this.logger.info(
'Sending proof (%s).',
this.hostname());
this.proofWindow.increase(1);
if (!this.proofWindow.allow()) {
this.logger.debug('proof: rate limit exceeded (%s).', this.hostname);
this.ban();
return;
}
this.send(new packets.ProofPacket(root, key, proof));
}
/**
* Increase banscore on peer.
* @param {Number} score
* @returns {Boolean}
*/
increaseBan(score) {
this.banScore += score;
if (this.banScore >= this.options.banScore) {
this.logger.debug('Ban threshold exceeded (%s).', this.hostname());
this.ban();
return true;
}
return false;
}
/**
* Ban peer.
*/
ban() {
this.emit('ban');
}
/**
* Send a `reject` packet to peer.
* @param {Number} msg
* @param {VerifyError} err
* @returns {Boolean}
*/
reject(msg, err) {
this.sendReject(err.code, err.reason, msg, err.hash);
return this.increaseBan(err.score);
}
/**
* Returns human readable list of services
* that are available.
* @returns {String[]}
*/
getServiceNames() {
const enabled = [];
for (const [service, bit] of Object.entries(services)) {
if (this.hasServices(bit))
enabled.push(service);
}
return enabled;
}
/**
* Test whether required services are available.
* @param {Number} services
* @returns {Boolean}
*/
hasServices(services) {
return (this.services & services) === services;
}
/**
* Test whether the peer sent us a
* compatible compact block handshake.
* @returns {Boolean}
*/
hasCompact() {
if (this.compactMode === -1)
return false;
return true;
}
/**
* Inspect the peer.
* @returns {String}
*/
inspect() {
return '<Peer:'
+ ` handshake=${this.handshake}`
+ ` host=${this.hostname()}`
+ ` outbound=${this.outbound}`
+ ` ping=${this.minPing}`
+ '>';
}
}
/**
* Max output bytes buffered before
* invoking stall behavior for peer.
* @const {Number}
* @default
*/
Peer.DRAIN_MAX = 10 << 20;
/**
* Interval to check for drainage
* and required responses from peer.
* @const {Number}
* @default
*/
Peer.STALL_INTERVAL = 5000;
/**
* Interval for pinging peers.
* @const {Number}
* @default
*/
Peer.PING_INTERVAL = 30000;
/**
* Interval to flush invs.
* Higher means more invs (usually
* txs) will be accumulated before
* flushing.
* @const {Number}
* @default
*/
Peer.INV_INTERVAL = 5000;
/**
* Required time for peers to
* respond to messages (i.e.
* getblocks/getdata).
* @const {Number}
* @default
*/
Peer.RESPONSE_TIMEOUT = 30000;
/**
* Required time for loader to
* respond with block/merkleblock.
* @const {Number}
* @default
*/
Peer.BLOCK_TIMEOUT = 120000;
/**
* Required time for loader to
* respond with a tx.
* @const {Number}
* @default
*/
Peer.TX_TIMEOUT = 120000;
/**
* Required time for peer to
* respond with a name.
* @const {Number}
* @default
*/
Peer.NAME_TIMEOUT = 5000;
/**
* Generic timeout interval.
* @const {Number}
* @default
*/
Peer.TIMEOUT_INTERVAL = 20 * 60000;
/**
* Connection timeout.
* @const {Number}
* @default
*/
Peer.CONNECT_TIMEOUT = 5 * 1000;
/**
* Handshake timeout.
* @const {Number}
* @default
*/
Peer.HANDSHAKE_TIMEOUT = 5 * 1000;
/**
* Peer Options
* @alias module:net.PeerOptions
*/
class PeerOptions {
/**
* Create peer options.
* @constructor
*/
constructor(options) {
this.network = Network.primary;
this.logger = Logger.global;
this.createSocket = tcp.createSocket;
this.version = common.PROTOCOL_VERSION;
this.services = common.LOCAL_SERVICES;
this.agent = common.USER_AGENT;
this.identityKey = common.ZERO_KEY;
this.noRelay = false;
this.spv = false;
this.compact = false;
this.headers = false;
this.banScore = common.BAN_SCORE;
this.maxProofRPS = 100;
this.getHeight = PeerOptions.getHeight;
this.isFull = PeerOptions.isFull;
this.createNonce = PeerOptions.createNonce;
this.hasNonce = PeerOptions.hasNonce;
this.getRate = PeerOptions.getRate;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from object.
* @private
* @param {Object} options
* @returns {PeerOptions}
*/
fromOptions(options) {
assert(options, 'Options are required.');
if (options.network != null)
this.network = Network.get(options.network);
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger;
}
if (options.createSocket != null) {
assert(typeof options.createSocket === 'function');
this.createSocket = options.createSocket;
}
if (options.version != null) {
assert(typeof options.version === 'number');
this.version = options.version;
}
if (options.services != null) {
assert(typeof options.services === 'number');
this.services = options.services;
}
if (options.agent != null) {
assert(typeof options.agent === 'string');
this.agent = options.agent;
}
if (options.identityKey != null) {
assert(Buffer.isBuffer(options.identityKey));
assert(options.identityKey.length === 32);
this.identityKey = options.identityKey;
}
if (options.noRelay != null) {
assert(typeof options.noRelay === 'boolean');
this.noRelay = options.noRelay;
}
if (options.spv != null) {
assert(typeof options.spv === 'boolean');
this.spv = options.spv;
}
if (options.compact != null) {
assert(typeof options.compact === 'boolean');
this.compact = options.compact;
}
if (options.headers != null) {
assert(typeof options.headers === 'boolean');
this.headers = options.headers;
}
if (options.banScore != null) {
assert(typeof options.banScore === 'number');
this.banScore = options.banScore;
}
if (options.maxProofRPS != null) {
assert(typeof options.maxProofRPS === 'number');
this.maxProofRPS = options.maxProofRPS;
}
if (options.getHeight != null) {
assert(typeof options.getHeight === 'function');
this.getHeight = options.getHeight;
}
if (options.isFull != null) {
assert(typeof options.isFull === 'function');
this.isFull = options.isFull;
}
if (options.createNonce != null) {
assert(typeof options.createNonce === 'function');
this.createNonce = options.createNonce;
}
if (options.hasNonce != null) {
assert(typeof options.hasNonce === 'function');
this.hasNonce = options.hasNonce;
}
if (options.getRate != null) {
assert(typeof options.getRate === 'function');
this.getRate = options.getRate;
}
return this;
}
/**
* Instantiate options from object.
* @param {Object} options
* @returns {PeerOptions}
*/
static fromOptions(options) {
return new this().fromOptions(options);
}
/**
* Get the chain height.
* @private
* @returns {Number}
*/
static getHeight() {
return 0;
}
/**
* Test whether the chain is synced.
* @private
* @returns {Boolean}
*/
static isFull() {
return false;
}
/**
* Create a version packet nonce.
* @private
* @param {String} hostname
* @returns {Buffer}
*/
static createNonce(hostname) {
return common.nonce();
}
/**
* Test whether version nonce is ours.
* @private
* @param {Buffer} nonce
* @returns {Boolean}
*/
static hasNonce(nonce) {
return false;
}
/**
* Get fee rate for txid.
* @private
* @param {Hash} hash
* @returns {Rate}
*/
static getRate(hash) {
return -1;
}
}
/**
* Request Entry
* @ignore
*/
class RequestEntry {
/**
* Create a request entry.
* @constructor
*/
constructor() {
this.timeout = 0;
this.jobs = [];
}
addJob(resolve, reject) {
this.jobs.push({ resolve, reject });
}
setTimeout(timeout) {
this.timeout = Date.now() + timeout;
}
reject(err) {
for (const job of this.jobs)
job.reject(err);
this.jobs.length = 0;
}
resolve(result) {
for (const job of this.jobs)
job.resolve(result);
this.jobs.length = 0;
}
}
/*
* Expose
*/
module.exports = Peer;