UNPKG

xud

Version:
939 lines 47 kB
"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