xud
Version:
Exchange Union Daemon
939 lines • 47 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = __importDefault(require("assert"));
const events_1 = require("events");
const net_1 = __importDefault(require("net"));
const semver_1 = __importDefault(require("semver"));
const enums_1 = require("../constants/enums");
const Logger_1 = __importDefault(require("../Logger"));
const addressUtils_1 = __importDefault(require("../utils/addressUtils"));
const aliasUtils_1 = require("../utils/aliasUtils");
const utils_1 = require("../utils/utils");
const errors_1 = __importStar(require("./errors"));
const Network_1 = __importDefault(require("./Network"));
const NodeList_1 = __importDefault(require("./NodeList"));
const P2PRepository_1 = __importDefault(require("./P2PRepository"));
const packets_1 = require("./packets");
const packets = __importStar(require("./packets/types"));
const Peer_1 = __importDefault(require("./Peer"));
/**
* Represents a pool of peers that handles all p2p network activity. This tracks all active and
* pending peers, optionally runs a server to listen for incoming connections, and is the primary
* interface for other modules to interact with the p2p layer.
*/
class Pool extends events_1.EventEmitter {
constructor({ config, xuNetwork, logger, models, nodeKey, version, strict = true, minCompatibleVersion }) {
var _a;
super();
/** A map of pub keys to nodes for which we have pending outgoing connections. */
this.pendingOutboundPeers = new Map();
/** A set of peers for which we have pending incoming connections. */
this.pendingInboundPeers = new Set();
/** A collection of opened, active peers. */
this.peers = new Map();
this.disconnecting = false;
this.connected = false;
this.getTokenIdentifier = (currency) => {
return this.nodeState.tokenIdentifiers[currency];
};
this.getNodePubKeyById = (nodeId) => {
var _a;
return (_a = this.nodes.getNodeById(nodeId)) === null || _a === void 0 ? void 0 : _a.nodePubKey;
};
this.getNodeId = (nodePubKey) => {
return this.nodes.getId(nodePubKey);
};
this.getNodeAlias = (nodePubKey) => {
return this.nodes.getAlias(nodePubKey);
};
/**
* Initialize the Pool by connecting to known nodes and listening to incoming peer connections, if configured to do so.
*/
this.init = () => __awaiter(this, void 0, void 0, function* () {
if (this.connected) {
return;
}
this.logger.info(`Connecting to ${this.network.xuNetwork} XU network`);
if (this.server) {
yield this.listen();
this.bindServer();
if (this.config.detectexternalip) {
yield this.detectExternalIpAddress();
}
}
this.bindNodeList();
this.loadingNodesPromise = this.nodes.load();
this.loadingNodesPromise.then(() => __awaiter(this, void 0, void 0, function* () {
if (this.nodes.count > 0 && !this.disconnecting) {
this.logger.info('Connecting to known / previously connected peers');
yield this.connectNodes(this.nodes, true, true);
this.logger.info('Completed start-up connections to known peers');
}
this.loadingNodesPromise = undefined;
})).catch((reason) => {
this.logger.error('Unexpected error connecting to known peers on startup', reason);
this.loadingNodesPromise = undefined;
});
this.verifyReachability();
this.connected = true;
});
this.detectExternalIpAddress = () => __awaiter(this, void 0, void 0, function* () {
let externalIp;
try {
externalIp = yield utils_1.getExternalIp();
this.logger.info(`retrieved external IP: ${externalIp}`);
const externalIpExists = this.nodeState.addresses.some((address) => { return address.host === externalIp; });
if (!externalIpExists) {
this.nodeState.addresses.push({
host: externalIp,
port: this.listenPort,
});
}
}
catch (error) {
this.logger.error(`error while retrieving external IP: ${error.message}`);
}
});
/**
* Updates our active trading pairs and sends a node state update packet to currently connected
* peers to notify them of the change.
*/
this.updatePairs = (pairIds) => {
this.nodeState.pairs = pairIds;
this.sendNodeStateUpdate();
};
/**
* Updates our connext public key and supported token addresses, then sends a node state update
* packet to currently connected peers to notify them of the change.
*/
this.updateConnextState = (tokenAddresses, pubKey) => {
if (pubKey) {
this.nodeState.connextIdentifier = pubKey;
}
tokenAddresses.forEach((tokenAddress, currency) => {
this.nodeState.tokenIdentifiers[currency] = tokenAddress;
});
this.sendNodeStateUpdate();
};
/**
* Updates our lnd pub key and chain identifier for a given currency and sends a node state
* update packet to currently connected peers to notify them of the change.
*/
this.updateLndState = ({ currency, pubKey, chain, uris }) => {
this.nodeState.lndPubKeys[currency] = pubKey;
if (uris) {
this.nodeState.lndUris[currency] = uris;
}
this.nodeState.tokenIdentifiers[currency] = chain;
this.sendNodeStateUpdate();
};
this.sendNodeStateUpdate = () => {
const packet = new packets.NodeStateUpdatePacket(this.nodeState);
this.peers.forEach((peer) => __awaiter(this, void 0, void 0, function* () {
yield peer.sendPacket(packet);
}));
};
this.disconnect = () => __awaiter(this, void 0, void 0, function* () {
if (!this.connected) {
return;
}
this.disconnecting = true;
if (this.loadingNodesPromise) {
yield this.loadingNodesPromise;
}
yield Promise.all([
this.unlisten(),
this.closePendingConnections(enums_1.DisconnectionReason.Shutdown),
this.closePeers(enums_1.DisconnectionReason.Shutdown)
]);
this.connected = false;
this.disconnecting = false;
});
this.bindNodeList = () => {
this.nodes.on('node.ban', (nodePubKey, events) => {
const banReasonText = (events[0] !== enums_1.ReputationEvent.ManualBan && enums_1.ReputationEvent[events[0]]) ? `due to ${enums_1.ReputationEvent[events[0]]}` : 'manually';
this.logger.info(`node ${nodePubKey} was banned ${banReasonText}`);
const peer = this.peers.get(nodePubKey);
if (peer) {
return peer.close(enums_1.DisconnectionReason.Banned, JSON.stringify(events));
}
return;
});
};
this.verifyReachability = () => {
this.nodeState.addresses.forEach((address) => __awaiter(this, void 0, void 0, function* () {
const externalAddress = addressUtils_1.default.toString(address);
this.logger.debug(`Verifying reachability of advertised address: ${externalAddress}`);
try {
const peer = new Peer_1.default(Logger_1.default.DISABLED_LOGGER, address, this.network);
this.pendingOutboundPeers.set(this.nodePubKey, peer);
yield peer.beginOpen({
ownNodeState: this.nodeState,
ownNodeKey: this.nodeKey,
ownVersion: this.version,
expectedNodePubKey: this.nodePubKey,
torport: this.config.torport,
});
this.pendingOutboundPeers.delete(this.nodePubKey);
yield peer.close();
assert_1.default.fail();
}
catch (err) {
if (typeof err.message === 'string' && err.message.includes(enums_1.DisconnectionReason[enums_1.DisconnectionReason.ConnectedToSelf])) {
this.logger.verbose(`Verified reachability of advertised address: ${externalAddress}`);
}
else {
this.logger.warn(`Could not verify reachability of advertised address: ${externalAddress}`);
}
}
}));
};
/**
* Iterate over a collection of nodes and attempt to connect to them.
* If the node is banned, already connected, or has no listening addresses, then do nothing.
* Additionally, if we're already trying to connect to a given node also do nothing.
* @param nodes a collection of nodes with a `forEach` iterator to attempt to connect to
* @param allowKnown whether to allow connecting to nodes we are already aware of, defaults to true
* @param retryConnecting whether to attempt retry connecting, defaults to false
* @returns a promise that will resolve when all outbound connections resolve
*/
this.connectNodes = (nodes, allowKnown = true, retryConnecting = false) => __awaiter(this, void 0, void 0, function* () {
const connectionPromises = [];
nodes.forEach((node) => {
// check that this node is not ourselves
const isNotUs = node.nodePubKey !== this.nodePubKey;
// check that it has listening addresses,
const hasAddresses = node.lastAddress || node.addresses.length;
// if allowKnown is false, allow nodes that we don't aware of
const isAllowed = allowKnown || !this.nodes.has(node.nodePubKey);
// determine whether we should attempt to connect
if (isNotUs && hasAddresses && isAllowed) {
connectionPromises.push(this.tryConnectNode(node, retryConnecting));
}
});
yield Promise.all(connectionPromises);
});
/**
* Attempt to create an outbound connection to a node using its known listening addresses.
*/
this.tryConnectNode = (node, retryConnecting = false) => __awaiter(this, void 0, void 0, function* () {
if (!(yield this.tryConnectWithLastAddress(node))) {
if (!(yield this.tryConnectWithAdvertisedAddresses(node)) && retryConnecting) {
yield this.tryConnectWithLastAddress(node, true);
}
}
});
this.tryConnectWithLastAddress = (node, retryConnecting = false) => __awaiter(this, void 0, void 0, function* () {
const { lastAddress, nodePubKey } = node;
if (!lastAddress)
return false;
try {
yield this.addOutbound(lastAddress, nodePubKey, retryConnecting, false);
return true;
}
catch (err) { }
return false;
});
this.tryConnectWithAdvertisedAddresses = (node) => __awaiter(this, void 0, void 0, function* () {
const { addresses, nodePubKey } = node;
// sort by lastConnected desc
const sortedAddresses = addressUtils_1.default.sortByLastConnected(addresses);
for (const address of sortedAddresses) {
if (node.lastAddress && addressUtils_1.default.areEqual(address, node.lastAddress))
continue;
try {
yield this.addOutbound(address, nodePubKey, false, false);
return true; // once we've successfully established an outbound connection, stop attempting new connections
}
catch (err) { }
}
return false;
});
/**
* Gets the active XU network as specified by the configuration.
*
* @returns the active XU network
*/
this.getNetwork = () => {
return this.network.xuNetwork;
};
/**
* Gets a node's reputation score and whether it is banned
* @param nodePubKey The node pub key of the node for which to get reputation information
* @return true if the specified node exists and the event was added, false otherwise
*/
this.getNodeReputation = (nodePubKey) => __awaiter(this, void 0, void 0, function* () {
const node = this.nodes.get(nodePubKey);
if (node) {
const { reputationScore, banned } = node;
return {
reputationScore,
banned,
};
}
else {
this.logger.warn(`node ${nodePubKey} (${aliasUtils_1.pubKeyToAlias(nodePubKey)}) not found`);
throw errors_1.default.NODE_NOT_FOUND(nodePubKey);
}
});
/**
* Attempt to add an outbound peer by connecting to a given socket address and nodePubKey.
* Throws an error if a socket connection to or the handshake with the node fails for any reason.
* @param address the socket address of the node to connect to
* @param nodePubKey the nodePubKey of the node to connect to
* @returns a promise that resolves to the connected and opened peer
*/
this.addOutbound = (address, nodePubKey, retryConnecting, revokeConnectionRetries) => __awaiter(this, void 0, void 0, function* () {
if (nodePubKey === this.nodePubKey || this.addressIsSelf(address)) {
throw errors_1.default.ATTEMPTED_CONNECTION_TO_SELF;
}
if (this.disconnecting || !this.connected) {
// if we are disconnected or disconnecting, don't make new connections to peers
throw errors_1.default.POOL_CLOSED;
}
// check if we allow connections to tor addresses
if (!this.config.tor && address.host.indexOf('.onion') !== -1) {
throw errors_1.default.NODE_TOR_ADDRESS(nodePubKey, address);
}
if (this.nodes.isBanned(nodePubKey)) {
throw errors_1.default.NODE_IS_BANNED(nodePubKey);
}
if (this.peers.has(nodePubKey)) {
throw errors_1.default.NODE_ALREADY_CONNECTED(nodePubKey, address);
}
this.logger.debug(`creating new outbound socket connection to ${address.host}:${address.port}`);
const pendingPeer = this.pendingOutboundPeers.get(nodePubKey);
if (pendingPeer) {
if (revokeConnectionRetries) {
pendingPeer.revokeConnectionRetries();
}
else {
throw errors_1.default.ALREADY_CONNECTING(nodePubKey);
}
}
const peer = new Peer_1.default(this.logger, address, this.network);
this.bindPeer(peer);
this.pendingOutboundPeers.set(nodePubKey, peer);
try {
yield this.openPeer(peer, nodePubKey, retryConnecting);
}
finally {
this.pendingOutboundPeers.delete(nodePubKey);
}
return peer;
});
this.listPeers = () => {
const peerInfos = Array.from({ length: this.peers.size });
let i = 0;
this.peers.forEach((peer) => {
peerInfos[i] = peer.info;
i += 1;
});
return peerInfos;
};
this.rawPeers = () => {
return this.peers;
};
this.addressIsSelf = (address) => {
if (address.port === this.listenPort) {
switch (address.host) {
case '::':
case '0.0.0.0':
case '::1':
case '127.0.0.1':
case 'localhost':
return true;
}
}
return false;
};
this.tryOpenPeer = (peer, peerPubKey, retryConnecting = false) => __awaiter(this, void 0, void 0, function* () {
try {
yield this.openPeer(peer, peerPubKey, retryConnecting);
}
catch (err) { }
});
/**
* Opens a connection to a peer and performs a routine for newly opened peers that includes
* requesting open orders and updating the database with the peer's information.
* @returns a promise that resolves once the connection has opened and the newly opened peer
* routine is complete
*/
this.openPeer = (peer, expectedNodePubKey, retryConnecting = false) => __awaiter(this, void 0, void 0, function* () {
if (this.disconnecting || !this.connected) {
// if we are disconnected or disconnecting, don't open new connections
throw errors_1.default.POOL_CLOSED;
}
try {
const sessionInit = yield peer.beginOpen({
expectedNodePubKey,
retryConnecting,
ownNodeState: this.nodeState,
ownNodeKey: this.nodeKey,
ownVersion: this.version,
torport: this.config.torport,
});
yield this.validatePeer(peer);
yield peer.completeOpen(this.nodeState, this.nodeKey, this.version, sessionInit);
}
catch (err) {
const msg = `could not open connection to ${peer.inbound ? 'inbound' : 'outbound'} peer`;
if (typeof err === 'string') {
this.logger.warn(`${msg} (${peer.label}): ${err}`);
}
else {
this.logger.warn(`${msg} (${peer.label}): ${err.message}`);
}
if (err.code === errors_1.errorCodes.CONNECTION_RETRIES_MAX_PERIOD_EXCEEDED) {
yield this.nodes.removeAddress(expectedNodePubKey, peer.address);
}
throw err;
}
const peerPubKey = peer.nodePubKey;
// A potential race condition exists here in the case where two peers attempt connections
// to each other simultaneously. Handshakes may be completed on both sockets without being
// detected in the validatePeer method that is called above after a handshake begins and
// the node pub key for the inbound peer is established but before it completes. In this case,
// both peers cannot close the "duplicate" connection or else they may wind up closing both
// of them. The approach below arbitrarily picks the peer with the higher node pubkey to close
// the socket when it encounters a duplicate, while the other peer waits briefly to see if
// the already established connection is closed.
if (this.peers.has(peerPubKey)) {
if (this.nodePubKey > peerPubKey) {
yield peer.close(enums_1.DisconnectionReason.AlreadyConnected);
throw errors_1.default.NODE_ALREADY_CONNECTED(peerPubKey, peer.address);
}
else {
const stillConnected = yield new Promise((resolve) => {
const timeout = setTimeout(() => resolve(true), Peer_1.default.STALL_INTERVAL);
this.peers.get(peerPubKey).once('close', () => {
resolve(false);
clearTimeout(timeout);
});
});
if (stillConnected) {
yield peer.close(enums_1.DisconnectionReason.AlreadyConnected);
throw errors_1.default.NODE_ALREADY_CONNECTED(peerPubKey, peer.address);
}
}
}
this.peers.set(peerPubKey, peer);
peer.active = true;
this.logger.verbose(`opened connection to ${peer.label} at ${addressUtils_1.default.toString(peer.address)}`);
// begin the process to handle a just opened peer, but return from this method immediately
this.handleOpenedPeer(peer).then(() => {
this.emit('peer.active', peerPubKey);
}).catch(this.logger.error);
});
this.handleOpenedPeer = (peer) => __awaiter(this, void 0, void 0, function* () {
this.emit('peer.verifyPairs', peer);
// request peer's known nodes only if p2p.discover option is true
if (this.config.discover) {
yield peer.sendGetNodes();
if (this.config.discoverminutes === 0) {
// timer is disabled
peer.discoverTimer = undefined; // defensive programming
}
else {
// timer is enabled
peer.discoverTimer = setInterval(peer.sendGetNodes, this.config.discoverminutes * 1000 * 60);
}
}
// if outbound, update the `lastConnected` field for the address we're actually connected to
const addresses = peer.inbound ? peer.addresses : peer.addresses.map((address) => {
if (addressUtils_1.default.areEqual(peer.address, address)) {
return Object.assign(Object.assign({}, address), { lastConnected: Date.now() });
}
else {
return address;
}
});
// creating the node entry
if (!this.nodes.has(peer.nodePubKey)) {
yield this.nodes.createNode({
addresses,
nodePubKey: peer.nodePubKey,
lastAddress: peer.inbound ? undefined : peer.address,
});
}
else {
// the node is known, update its listening addresses
yield this.nodes.updateAddresses(peer.nodePubKey, addresses, peer.inbound ? undefined : peer.address);
}
});
this.closePeer = (nodePubKey, reason, reasonPayload) => __awaiter(this, void 0, void 0, function* () {
const peer = this.peers.get(nodePubKey);
if (peer) {
yield peer.close(reason, reasonPayload);
this.logger.info(`Disconnected from ${peer.nodePubKey}@${addressUtils_1.default.toString(peer.address)} (${peer.alias})`);
}
else {
throw (errors_1.default.NOT_CONNECTED(nodePubKey));
}
});
this.banNode = (nodePubKey) => __awaiter(this, void 0, void 0, function* () {
if (this.nodes.isBanned(nodePubKey)) {
throw errors_1.default.NODE_ALREADY_BANNED(nodePubKey);
}
else {
const banned = yield this.nodes.ban(nodePubKey);
if (!banned) {
throw errors_1.default.NODE_NOT_FOUND(nodePubKey);
}
}
});
this.unbanNode = (nodePubKey, reconnect) => __awaiter(this, void 0, void 0, function* () {
if (this.nodes.isBanned(nodePubKey)) {
const unbanned = yield this.nodes.unBan(nodePubKey);
if (!unbanned) {
throw errors_1.default.NODE_NOT_FOUND(nodePubKey);
}
const node = this.nodes.get(nodePubKey);
if (node) {
const Node = {
nodePubKey,
addresses: node.addresses,
lastAddress: node.lastAddress,
};
this.logger.info(`node ${nodePubKey} (${aliasUtils_1.pubKeyToAlias(nodePubKey)}) was unbanned`);
if (reconnect) {
yield this.tryConnectNode(Node, false);
}
}
}
else {
throw errors_1.default.NODE_NOT_BANNED(nodePubKey);
}
});
this.discoverNodes = (peerPubKey) => __awaiter(this, void 0, void 0, function* () {
const peer = this.peers.get(peerPubKey);
if (!peer) {
throw errors_1.default.NOT_CONNECTED(peerPubKey);
}
return peer.discoverNodes();
});
// A wrapper for [[NodeList.addReputationEvent]].
this.addReputationEvent = (nodePubKey, event) => __awaiter(this, void 0, void 0, function* () {
// when in strict mode, we add all reputation events
// otherwise, we only add manual or severe reputation events to prevent unintentional bans
if (this.strict
|| event === enums_1.ReputationEvent.ManualBan
|| event === enums_1.ReputationEvent.ManualUnban
|| event === enums_1.ReputationEvent.SwapAbuse
|| event === enums_1.ReputationEvent.SwapMisbehavior
|| event === enums_1.ReputationEvent.WireProtocolErr
|| event === enums_1.ReputationEvent.InvalidAuth) {
this.logger.debug(`Peer (${nodePubKey}): reputation event: ${enums_1.ReputationEvent[event]}`);
yield this.nodes.addReputationEvent(nodePubKey, event);
}
});
this.sendToPeer = (nodePubKey, packet) => __awaiter(this, void 0, void 0, function* () {
const peer = this.peers.get(nodePubKey);
if (!peer) {
throw errors_1.default.NOT_CONNECTED(nodePubKey);
}
yield peer.sendPacket(packet);
});
/**
* Gets a peer by its node pub key or alias. Throws a [[NOT_CONNECTED]] error if the supplied identifier does not
* match any currently connected peer.
*/
this.getPeer = (peerPubKey) => {
const peer = this.peers.get(peerPubKey);
if (!peer) {
throw errors_1.default.NOT_CONNECTED(peerPubKey);
}
return peer;
};
this.tryGetPeer = (peerPubKey) => {
try {
return this.getPeer(peerPubKey);
}
catch (err) {
return;
}
};
this.broadcastOrder = (order) => {
const orderPacket = new packets.OrderPacket(order);
this.peers.forEach((peer) => __awaiter(this, void 0, void 0, function* () {
if (peer.isPairActive(order.pairId)) {
yield peer.sendPacket(orderPacket);
}
}));
};
/**
* Broadcasts an [[OrderInvalidationPacket]] to all currently connected peers.
* @param nodeToExclude the node pub key of a node to exclude from the packet broadcast
*/
this.broadcastOrderInvalidation = ({ id, pairId, quantity }, nodeToExclude) => {
const orderInvalidationPacket = new packets.OrderInvalidationPacket({ id, pairId, quantity });
this.peers.forEach((peer) => __awaiter(this, void 0, void 0, function* () {
if (!nodeToExclude || peer.nodePubKey !== nodeToExclude) {
if (peer.isPairActive(pairId)) {
yield peer.sendPacket(orderInvalidationPacket);
}
}
}));
// TODO: send only to peers which accepts the pairId
};
this.addInbound = (socket) => __awaiter(this, void 0, void 0, function* () {
const address = addressUtils_1.default.fromSocket(socket);
this.logger.debug(`new inbound socket connection from ${address.host}:${address.port}`);
const peer = Peer_1.default.fromInbound(socket, this.logger, this.network);
this.bindPeer(peer);
this.pendingInboundPeers.add(peer);
yield this.tryOpenPeer(peer);
this.pendingInboundPeers.delete(peer);
});
this.handleSocket = (socket) => __awaiter(this, void 0, void 0, function* () {
if (!socket.remoteAddress) { // client disconnected, socket is destroyed
this.logger.debug('Ignoring disconnected peer');
socket.destroy();
return;
}
if (this.nodes.isBanned(socket.remoteAddress)) {
this.logger.debug(`Ignoring banned peer (${socket.remoteAddress})`);
socket.destroy();
return;
}
yield this.addInbound(socket);
});
this.handlePacket = (peer, packet) => __awaiter(this, void 0, void 0, function* () {
switch (packet.type) {
case packets_1.PacketType.Order: {
const receivedOrder = packet.body;
this.logger.trace(`received order from ${peer.label}: ${JSON.stringify(receivedOrder)}`);
const { id, pairId } = receivedOrder;
if (peer.isPairActive(pairId)) {
if (receivedOrder.replaceOrderId) {
const orderInvalidation = {
pairId,
id: receivedOrder.replaceOrderId,
};
this.emit('packet.orderInvalidation', orderInvalidation, peer.nodePubKey);
}
const incomingOrder = Object.assign(Object.assign({}, receivedOrder), { peerPubKey: peer.nodePubKey });
this.emit('packet.order', incomingOrder);
}
else {
this.logger.debug(`received order ${id} for deactivated trading pair`);
}
break;
}
case packets_1.PacketType.OrderInvalidation: {
const orderInvalidation = packet.body;
if (peer.isPairActive(orderInvalidation.pairId)) {
this.logger.trace(`received order invalidation from ${peer.label}: ${JSON.stringify(orderInvalidation)}`);
this.emit('packet.orderInvalidation', orderInvalidation, peer.nodePubKey);
}
else {
this.logger.trace(`received order invalidation for inactive pair from ${peer.label}: ${JSON.stringify(orderInvalidation)}`);
}
break;
}
case packets_1.PacketType.GetOrders: {
const getOrdersPacketBody = packet.body;
const pairIds = getOrdersPacketBody ? getOrdersPacketBody.pairIds : [];
this.emit('packet.getOrders', peer, packet.header.id, pairIds);
break;
}
case packets_1.PacketType.Orders: {
const receivedOrders = packet.body;
if (receivedOrders.length > 0) {
this.logger.debug(`received ${receivedOrders.length} orders from ${peer.label}`);
receivedOrders.forEach((order) => {
if (peer.isPairActive(order.pairId)) {
this.emit('packet.order', Object.assign(Object.assign({}, order), { peerPubKey: peer.nodePubKey }));
}
else {
this.logger.debug(`received order ${order.id} for deactivated trading pair`);
}
});
}
break;
}
case packets_1.PacketType.GetNodes: {
yield this.handleGetNodes(peer, packet.header.id);
break;
}
case packets_1.PacketType.Nodes: {
const nodes = packet.body;
let newNodesCount = 0;
nodes.forEach((node) => {
if (!this.nodes.has(node.nodePubKey)) {
newNodesCount += 1;
}
});
this.logger.verbose(`received ${nodes.length} nodes (${newNodesCount} new) from ${peer.label}`);
yield this.connectNodes(nodes);
break;
}
case packets_1.PacketType.SanitySwap: {
this.logger.debug(`received sanitySwap from ${peer.label}: ${JSON.stringify(packet.body)}`);
this.emit('packet.sanitySwapInit', packet, peer);
break;
}
case packets_1.PacketType.SwapRequest: {
this.logger.debug(`received swapRequest from ${peer.label}: ${JSON.stringify(packet.body)}`);
this.emit('packet.swapRequest', packet, peer);
break;
}
case packets_1.PacketType.SwapAccepted: {
this.logger.debug(`received swapAccepted from ${peer.label}: ${JSON.stringify(packet.body)}`);
this.emit('packet.swapAccepted', packet, peer);
break;
}
case packets_1.PacketType.SwapFailed: {
const { body } = packet;
this.logger.debug(`received swapFailed due to ${enums_1.SwapFailureReason[body.failureReason]} from ${peer.label}: ${JSON.stringify(body)}`);
this.emit('packet.swapFailed', packet);
break;
}
}
});
/** Validates a peer. If a check fails, closes the peer and throws a p2p error. */
this.validatePeer = (peer) => __awaiter(this, void 0, void 0, function* () {
assert_1.default(peer.nodePubKey);
const peerPubKey = peer.nodePubKey;
if (peerPubKey === this.nodePubKey) {
yield peer.close(enums_1.DisconnectionReason.ConnectedToSelf);
throw errors_1.default.ATTEMPTED_CONNECTION_TO_SELF;
}
// Check if version is semantic, and higher than minCompatibleVersion.
if (!semver_1.default.valid(peer.version)) {
yield peer.close(enums_1.DisconnectionReason.MalformedVersion);
throw errors_1.default.MALFORMED_VERSION(addressUtils_1.default.toString(peer.address), peer.version);
}
// dev.note: compare returns 0 if v1 == v2, or 1 if v1 is greater, or -1 if v2 is greater.
if (semver_1.default.compare(peer.version, this.minCompatibleVersion) === -1) {
yield peer.close(enums_1.DisconnectionReason.IncompatibleProtocolVersion);
throw errors_1.default.INCOMPATIBLE_VERSION(addressUtils_1.default.toString(peer.address), this.minCompatibleVersion, peer.version);
}
if (!this.connected) {
// if we have disconnected the pool, don't allow any new connections to open
yield peer.close(enums_1.DisconnectionReason.NotAcceptingConnections);
throw errors_1.default.POOL_CLOSED;
}
if (this.nodes.isBanned(peerPubKey)) {
// TODO: Ban IP address for this session if banned peer attempts repeated connections.
yield peer.close(enums_1.DisconnectionReason.Banned);
throw errors_1.default.NODE_IS_BANNED(peerPubKey);
}
if (this.peers.has(peerPubKey)) {
// TODO: Penalize peers that attempt to create duplicate connections to us more than once.
// The first time might be due to connection retries.
yield peer.close(enums_1.DisconnectionReason.AlreadyConnected);
throw errors_1.default.NODE_ALREADY_CONNECTED(peerPubKey, peer.address);
}
// check to make sure the socket was not destroyed during or immediately after the handshake
if (!peer.connected) {
this.logger.error(`the socket to node ${peerPubKey} was disconnected`);
throw errors_1.default.NOT_CONNECTED(peerPubKey);
}
});
/**
* Responds to a [[GetNodesPacket]] by populating and sending a [[NodesPacket]].
*/
this.handleGetNodes = (peer, reqId) => __awaiter(this, void 0, void 0, function* () {
const connectedNodesInfo = [];
this.peers.forEach((connectedPeer) => {
if (connectedPeer.nodePubKey !== peer.nodePubKey && connectedPeer.addresses && connectedPeer.addresses.length > 0) {
// don't send the peer itself or any peers for whom we don't have listening addresses
connectedNodesInfo.push({
nodePubKey: connectedPeer.nodePubKey,
addresses: connectedPeer.addresses,
});
}
});
yield peer.sendNodes(connectedNodesInfo, reqId);
});
this.bindServer = () => {
this.server.on('error', (err) => {
this.logger.error(err);
});
this.server.on('connection', this.handleSocket);
};
this.bindPeer = (peer) => {
peer.on('packet', (packet) => __awaiter(this, void 0, void 0, function* () {
yield this.handlePacket(peer, packet);
}));
peer.on('pairDropped', (pairId) => {
// drop all orders for trading pairs that exist and are no longer supported
if (this.nodeState.pairs.includes(pairId)) {
this.emit('peer.pairDropped', peer.nodePubKey, pairId);
}
});
peer.on('verifyPairs', () => {
// drop all orders for trading pairs that are no longer supported
this.emit('peer.verifyPairs', peer);
});
peer.on('nodeStateUpdate', () => {
this.emit('peer.nodeStateUpdate', peer);
});
peer.once('close', () => this.handlePeerClose(peer));
peer.on('reputation', (event) => __awaiter(this, void 0, void 0, function* () {
if (peer.nodePubKey) {
yield this.addReputationEvent(peer.nodePubKey, event);
}
}));
};
this.handlePeerClose = (peer) => __awaiter(this, void 0, void 0, function* () {
if (peer.active) {
this.peers.delete(peer.nodePubKey);
}
peer.removeAllListeners();
if (!peer.active) {
return;
}
peer.active = false;
this.emit('peer.close', peer.nodePubKey);
const doesDisconnectionReasonCallForReconnection = (peer.sentDisconnectionReason === undefined || peer.sentDisconnectionReason === enums_1.DisconnectionReason.ResponseStalling) &&
(peer.recvDisconnectionReason === undefined || peer.recvDisconnectionReason === enums_1.DisconnectionReason.ResponseStalling ||
peer.recvDisconnectionReason === enums_1.DisconnectionReason.AlreadyConnected ||
peer.recvDisconnectionReason === enums_1.DisconnectionReason.Shutdown);
const addresses = peer.addresses || [];
if (doesDisconnectionReasonCallForReconnection
&& !peer.inbound // we don't make reconnection attempts to peers that connected to use
&& peer.nodePubKey // we only reconnect if we know the peer's node pubkey
&& (addresses.length || peer.address) // we only reconnect if there's an address to connect to
&& !this.disconnecting && this.connected // we don't reconnect if we're in the process of disconnecting or have disconnected the p2p pool
) {
this.logger.debug(`attempting to reconnect to a disconnected peer ${peer.label}`);
const node = { addresses, lastAddress: peer.address, nodePubKey: peer.nodePubKey };
yield this.tryConnectNode(node, true);
}
});
this.closePeers = (reason) => {
const closePromises = [];
for (const peer of this.peers.values()) {
closePromises.push(peer.close(reason));
}
return Promise.all(closePromises);
};
this.closePendingConnections = (reason) => {
const closePromises = [];
for (const peer of this.pendingOutboundPeers.values()) {
closePromises.push(peer.close(reason));
}
for (const peer of this.pendingInboundPeers) {
closePromises.push(peer.close(reason));
}
return Promise.all(closePromises);
};
/**
* Starts listening for incoming p2p connections on the configured host and port. If `this.listenPort` is 0 or undefined,
* a random available port is used and will be assigned to `this.listenPort`.
* @return a promise that resolves once the server is listening, or rejects if it fails to listen
*/
this.listen = () => {
return new Promise((resolve, reject) => {
const listenErrHandler = (err) => {
reject(err);
};
this.server.listen(this.listenPort || 0, '0.0.0.0').on('listening', () => {
const { address, port } = this.server.address();
this.logger.info(`p2p server listening on ${address}:${port}`);
if (this.listenPort === 0) {
// we didn't specify a port and grabbed any available port
this.listenPort = port;
}
this.server.removeListener('error', listenErrHandler);
resolve();
}).on('error', listenErrHandler);
});
};
/**
* Stops listening for incoming p2p connections.
* @return a promise that resolves once the server is no longer listening
*/
this.unlisten = () => {
if (this.server && this.server.listening) {
this.server.close(); // stop listening for new connections
}
return new Promise((resolve) => {
if (this.server) {
this.server.once('close', resolve);
}
else {
resolve();
}
});
};
/**
* Resolves an alias to a known node's public key. Throws an error if a unique
* pub key cannot be found for the provided alias.
*/
this.resolveAlias = (alias) => {
return this.nodes.getPubKeyForAlias(alias);
};
this.logger = logger;
this.nodeKey = nodeKey;
this.nodePubKey = nodeKey.pubKey;
this.alias = aliasUtils_1.pubKeyToAlias(nodeKey.pubKey);
this.version = version;
this.config = config;
this.strict = strict;
this.network = new Network_1.default(xuNetwork);
this.repository = new P2PRepository_1.default(models);
this.nodes = new NodeList_1.default(this.repository);
// we use the provided minCompatibleVersion if one is specified
// otherwise we attempt to read the minCompatibleVersion from package.json
// otherwise we use our own version as the minimum
this.minCompatibleVersion = (_a = minCompatibleVersion !== null && minCompatibleVersion !== void 0 ? minCompatibleVersion : require('../../package.json').minCompatibleVersion) !== null && _a !== void 0 ? _a : version;
this.nodeState = {
addresses: [],
pairs: [],
connextIdentifier: '',
lndPubKeys: {},
lndUris: {},
tokenIdentifiers: {},
};
if (config.listen) {
this.listenPort = config.port;
this.server = net_1.default.createServer();
config.addresses.forEach((addressString) => {
const address = addressUtils_1.default.fromString(addressString, config.port);
this.nodeState.addresses.push(address);
});
}
}
get peerCount() {
return this.peers.size;
}
get addresses() {
return this.nodeState.addresses;
}
}
exports.default = Pool;
//# sourceMappingURL=Pool.js.map