UNPKG

xud

Version:
927 lines 46.7 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 __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; 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 crypto_1 = require("crypto"); const events_1 = require("events"); const json_stable_stringify_1 = __importDefault(require("json-stable-stringify")); const net_1 = __importDefault(require("net")); const secp256k1_1 = __importDefault(require("secp256k1")); const socks_1 = require("socks"); const enums_1 = require("../constants/enums"); 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 Framer_1 = __importDefault(require("./Framer")); const packets_1 = require("./packets"); const Packet_1 = require("./packets/Packet"); const packets = __importStar(require("./packets/types")); const Parser_1 = __importDefault(require("./Parser")); var PeerStatus; (function (PeerStatus) { /** The peer was just created and we have not begun the handshake yet. */ PeerStatus[PeerStatus["New"] = 0] = "New"; /** We have begun the handshake procedure with the peer. */ PeerStatus[PeerStatus["Opening"] = 1] = "Opening"; /** We have received and authenticated a [[SessionInitPacket]] from the peer. */ PeerStatus[PeerStatus["Open"] = 2] = "Open"; /** We have closed this peer and its respective socket connection. */ PeerStatus[PeerStatus["Closed"] = 3] = "Closed"; })(PeerStatus || (PeerStatus = {})); /** Represents a remote peer and manages a TCP socket and incoming/outgoing communication with that peer. */ let Peer = /** @class */ (() => { class Peer extends events_1.EventEmitter { /** * @param address The socket address for the connection to this peer. */ constructor(logger, address, network) { super(); this.logger = logger; this.address = address; /** Whether the peer is included in the p2p pool list of peers and will receive broadcasted packets. */ this.active = false; /** * Currencies that we cannot swap because we are missing a swap client identifier or because the * peer's token identifier for this currency does not match ours - for example this may happen * because a peer is using a different token contract address for a currency than we are. */ this.disabledCurrencies = new Set(); this.status = PeerStatus.New; /** Trading pairs advertised by this peer which we have verified that we can swap. */ this.activePairs = new Set(); /** Currencies that we have verified we can swap with this peer. */ this.activeCurrencies = new Set(); this.responseMap = new Map(); this.connectionRetriesRevoked = false; this.getAdvertisedCurrencies = () => { const advertisedCurrencies = new Set(); this.advertisedPairs.forEach((advertisedPair) => { const [baseCurrency, quoteCurrency] = advertisedPair.split('/'); advertisedCurrencies.add(baseCurrency); advertisedCurrencies.add(quoteCurrency); }); return advertisedCurrencies; }; this.getIdentifier = (clientType, currency) => { if (!this.nodeState) { return undefined; } switch (clientType) { case enums_1.SwapClientType.Lnd: return this.getLndPubKey(currency); case enums_1.SwapClientType.Connext: return this.connextIdentifier; default: return; } }; this.getTokenIdentifier = (currency) => { if (!this.nodeState) { return undefined; } return this.nodeState.tokenIdentifiers[currency]; }; this.getStatus = () => { let status; if (this.connected) { status = this.nodePubKey ? `Connected to ${this.label}` : `Connected pre-handshake to ${this.label}`; } else { status = 'Not connected'; } return status; }; /** * Prepares a peer for use by establishing a socket connection and beginning the handshake. * @returns the session init packet from beginning the handshake */ this.beginOpen = ({ ownNodeState, ownNodeKey, ownVersion, expectedNodePubKey, retryConnecting = false, torport, }) => __awaiter(this, void 0, void 0, function* () { assert_1.default(this.status === PeerStatus.New); assert_1.default(this.inbound || expectedNodePubKey); assert_1.default(!retryConnecting || !this.inbound); this.status = PeerStatus.Opening; this.expectedNodePubKey = expectedNodePubKey; yield this.initConnection(retryConnecting, torport); this.initStall(); return this.beginHandshake(ownNodeState, ownNodeKey, ownVersion); }); /** * Finishes opening a peer for use by marking the peer as opened, completing the handshake, * and setting up the ping packet timer. * @param ownNodeState our node state data to send to the peer * @param ownNodeKey our identity node key * @param ownVersion the version of xud we are running * @param sessionInit the session init packet we received when beginning the handshake */ this.completeOpen = (ownNodeState, ownNodeKey, ownVersion, sessionInit) => __awaiter(this, void 0, void 0, function* () { assert_1.default(this.status === PeerStatus.Opening); yield this.completeHandshake(ownNodeState, ownNodeKey, ownVersion, sessionInit); this.status = PeerStatus.Open; // Setup the ping interval this.pingTimer = setInterval(this.sendPing, Peer.PING_INTERVAL); // Setup a timer to periodicially check if we can swap inactive pairs this.checkPairsTimer = setInterval(() => this.emit('verifyPairs'), Peer.CHECK_PAIRS_INTERVAL); }); /** * Close a peer by ensuring the socket is destroyed and terminating all timers. */ this.close = (reason, reasonPayload) => __awaiter(this, void 0, void 0, function* () { if (this.status === PeerStatus.Closed) { return; } this.status = PeerStatus.Closed; this.revokeConnectionRetries(); if (this.socket) { if (!this.socket.destroyed) { if (reason !== undefined) { this.logger.debug(`Peer ${this.label}: closing socket. reason: ${enums_1.DisconnectionReason[reason]}`); this.sentDisconnectionReason = reason; yield this.sendPacket(new packets.DisconnectingPacket({ reason, payload: reasonPayload })); } this.socket.destroy(); this.socket.removeAllListeners(); } delete this.socket; } if (this.retryConnectionTimer) { clearTimeout(this.retryConnectionTimer); this.retryConnectionTimer = undefined; } if (this.discoverTimer) { clearInterval(this.discoverTimer); this.discoverTimer = undefined; } if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = undefined; } if (this.checkPairsTimer) { clearInterval(this.checkPairsTimer); this.checkPairsTimer = undefined; } if (this.stallTimer) { clearInterval(this.stallTimer); this.stallTimer = undefined; } let rejectionMsg; if (reason) { rejectionMsg = `Peer ${this.label} closed due to ${enums_1.DisconnectionReason[reason]} ${reasonPayload || ''}`; } else if (this.recvDisconnectionReason) { rejectionMsg = `Peer ${this.label} disconnected from us due to ${enums_1.DisconnectionReason[this.recvDisconnectionReason]}`; } else { rejectionMsg = `Peer ${this.label} was destroyed`; } for (const [packetType, entry] of this.responseMap) { this.responseMap.delete(packetType); entry.reject(new Error(rejectionMsg)); } this.emit('close'); }); this.revokeConnectionRetries = () => { this.connectionRetriesRevoked = true; }; this.sendPacket = (packet) => __awaiter(this, void 0, void 0, function* () { const data = yield this.framer.frame(packet, this.outEncryptionKey); try { yield new Promise((resolve, reject) => { if (this.socket && !this.socket.destroyed) { this.socket.write(data, (err) => { if (err) { this.logger.trace(`could not send ${packets_1.PacketType[packet.type]} packet to ${this.label}: ${JSON.stringify(packet)}`); reject(err); } else { this.logger.trace(`Sent ${packets_1.PacketType[packet.type]} packet to ${this.label}: ${JSON.stringify(packet)}`); if (packet.direction === packets_1.PacketDirection.Request) { this.addResponseTimeout(packet.header.id, packet.responseType, Peer.RESPONSE_TIMEOUT); } resolve(); } }); } else { this.logger.warn(`could not send packet to ${this.label} because socket is nonexistent or destroyed`); resolve(); } }); } catch (err) { this.logger.error(`failed sending data to ${this.label}`, err); } }); this.sendOrders = (orders, reqId) => __awaiter(this, void 0, void 0, function* () { const packet = new packets.OrdersPacket(orders, reqId); yield this.sendPacket(packet); }); /** Sends a [[NodesPacket]] containing node connection info to this peer. */ this.sendNodes = (nodes, reqId) => __awaiter(this, void 0, void 0, function* () { const packet = new packets.NodesPacket(nodes, reqId); yield this.sendPacket(packet); }); this.deactivateCurrency = (currency) => { if (this.activeCurrencies.has(currency)) { this.activeCurrencies.delete(currency); this.activePairs.forEach((activePairId) => { const [baseCurrency, quoteCurrency] = activePairId.split('/'); if (baseCurrency === currency || quoteCurrency === currency) { this.deactivatePair(activePairId); } }); this.logger.debug(`deactivated ${currency} for peer ${this.label}`); } }; this.activateCurrency = (currency) => this.activeCurrencies.add(currency); this.disableCurrency = (currency) => { if (!this.disabledCurrencies.has(currency)) { this.disabledCurrencies.add(currency); this.deactivateCurrency(currency); } }; this.enableCurrency = (currency) => { if (this.disabledCurrencies.delete(currency)) { this.logger.debug(`enabled ${currency} for peer ${this.label}`); } }; /** * Deactivates a trading pair with this peer. */ this.deactivatePair = (pairId) => { if (!this.nodeState) { throw new Error('cannot deactivate a trading pair before handshake is complete'); } if (this.activePairs.delete(pairId)) { this.emit('pairDropped', pairId); } // TODO: notify peer that we have deactivated this pair? }; /** * Activates a trading pair with this peer. */ this.activatePair = (pairId) => __awaiter(this, void 0, void 0, function* () { if (!this.nodeState) { throw new Error('cannot activate a trading pair before handshake is complete'); } this.activePairs.add(pairId); // request peer's orders yield this.sendPacket(new packets.GetOrdersPacket({ pairIds: [pairId] })); }); this.isPairActive = (pairId) => this.activePairs.has(pairId); this.isCurrencyActive = (currency) => this.activeCurrencies.has(currency); /** * Ensure we are connected (for inbound connections) or listen for the `connect` socket event (for outbound connections) * and set the [[connectTime]] timestamp. If an outbound connection attempt errors or times out, throw an error. */ this.initConnection = (retry = false, torport) => __awaiter(this, void 0, void 0, function* () { if (this.connected) { // in case of an inbound peer, we will already be connected assert_1.default(this.socket); assert_1.default(this.inbound); this.connectTime = Date.now(); this.logger.debug(this.getStatus()); return; } return new Promise((resolve, reject) => { const startTime = Date.now(); let retryDelay = Peer.CONNECTION_RETRIES_MIN_DELAY; let retries = 0; this.inbound = false; this.connectionRetriesRevoked = false; const connectViaProxy = () => { this.socket = net_1.default.connect(torport, 'localhost'); const proxyOptions = { proxy: { host: 'localhost', port: torport, type: 5, }, command: 'connect', destination: { host: this.address.host, port: this.address.port, }, existing_socket: this.socket, }; socks_1.SocksClient.createConnection(proxyOptions) .then((info) => { assert_1.default(this.socket === info.socket); onConnect(); }) .catch(onError); }; const connect = () => { if (torport) { connectViaProxy(); } else { this.socket = net_1.default.connect(this.address.port, this.address.host); this.socket.once('connect', onConnect); this.socket.on('error', onError); } }; const cleanup = () => { if (this.socket) { this.socket.removeListener('error', onError); this.socket.removeListener('connect', onConnect); } if (this.retryConnectionTimer) { clearTimeout(this.retryConnectionTimer); this.retryConnectionTimer = undefined; } }; const onConnect = () => { this.connectTime = Date.now(); this.bindSocket(); this.logger.debug(this.getStatus()); this.emit('connect'); cleanup(); resolve(); }; const onError = (err) => __awaiter(this, void 0, void 0, function* () { cleanup(); if (!retry) { yield this.close(); reject(errors_1.default.COULD_NOT_CONNECT(this.address, err)); return; } if (Date.now() - startTime + retryDelay > Peer.CONNECTION_RETRIES_MAX_PERIOD) { yield this.close(); reject(errors_1.default.CONNECTION_RETRIES_MAX_PERIOD_EXCEEDED); return; } if (this.connectionRetriesRevoked) { yield this.close(); reject(errors_1.default.CONNECTION_RETRIES_REVOKED); return; } this.logger.debug(`Connection attempt #${retries + 1} to ${this.label} ` + `failed: ${err.message}. retrying in ${retryDelay / 1000} sec...`); this.retryConnectionTimer = setTimeout(() => { retryDelay = Math.min(Peer.CONNECTION_RETRIES_MAX_DELAY, retryDelay * 2); retries = retries + 1; connect(); }, retryDelay); }); connect(); }); }); this.initStall = () => { if (this.status === PeerStatus.Closed) { return; } assert_1.default(!this.stallTimer); this.stallTimer = setInterval(this.checkTimeout, Peer.STALL_INTERVAL); }; /** * Waits for a packet to be received from peer. * @returns A promise that is resolved once the packet is received or rejects on timeout. */ this.wait = (reqId, resType, timeout, cb) => { const entry = this.getOrAddPendingResponseEntry(reqId, resType); return new Promise((resolve, reject) => { entry.addJob(resolve, reject); if (cb) { entry.addCb(cb); } if (timeout) { entry.setTimeout(timeout); } }); }; this.waitSessionInit = () => __awaiter(this, void 0, void 0, function* () { if (!this.sessionInitPacket) { yield this.wait(packets_1.PacketType.SessionInit.toString(), undefined, Peer.RESPONSE_TIMEOUT); } return this.sessionInitPacket; }); /** * Potentially timeout peer if it hasn't responded. */ this.checkTimeout = () => __awaiter(this, void 0, void 0, function* () { const now = utils_1.ms(); for (const [packetId, entry] of this.responseMap) { if (now > entry.timeout) { const request = packets_1.PacketType[parseInt(packetId, 10)] || packetId; const err = errors_1.default.RESPONSE_TIMEOUT(request); this.logger.error(`Peer timed out waiting for response to packet ${packetId}`); entry.reject(err); yield this.close(enums_1.DisconnectionReason.ResponseStalling, packetId); } } }); /** * Wait for a packet to be received from peer. */ this.addResponseTimeout = (reqId, resType, timeout) => { if (this.status !== PeerStatus.Closed) { const entry = this.getOrAddPendingResponseEntry(reqId, resType); entry.setTimeout(timeout); } }; this.getOrAddPendingResponseEntry = (reqId, resType) => { let entry = this.responseMap.get(reqId); if (!entry) { entry = new PendingResponseEntry(resType); this.responseMap.set(reqId, entry); } return entry; }; /** * Fulfill a pending response entry for solicited responses, penalize unsolicited responses. * @returns false if no pending response entry exists for the provided key, otherwise true */ this.fulfillResponseEntry = (packet) => { const { reqId } = packet.header; if (!reqId) { this.logger.debug(`Peer ${this.label} sent a response packet without reqId`); // TODO: penalize return false; } const entry = this.responseMap.get(reqId); if (!entry) { this.logger.debug(`Peer ${this.label} sent an unsolicited response packet (${reqId})`); // TODO: penalize return false; } const isExpectedType = entry.resType === undefined || (Packet_1.isPacketType(entry.resType) && packet.type === entry.resType) || (Packet_1.isPacketTypeArray(entry.resType) && entry.resType.includes(packet.type)); if (!isExpectedType) { this.logger.debug(`Peer ${this.label} sent an unsolicited packet type (${packets_1.PacketType[packet.type]}) for response packet (${reqId})`); // TODO: penalize return false; } this.responseMap.delete(reqId); entry.resolve(packet); return true; }; /** * Binds listeners to a newly connected socket for `error`, `close`, and `data` events. */ this.bindSocket = () => { assert_1.default(this.socket); this.socket.on('error', (err) => { this.logger.error(`Peer (${this.label}) error`, err); }); this.socket.on('close', (hadError) => __awaiter(this, void 0, void 0, function* () { // emitted once the socket is fully closed if (this.nodePubKey === undefined) { this.logger.info(`Socket closed prior to handshake with ${this.label}`); } else if (hadError) { this.logger.warn(`Peer ${this.label} socket closed due to error`); } else { this.logger.info(`Peer ${this.label} socket closed`); } yield this.close(); })); this.socket.on('data', this.parser.feed); this.socket.setNoDelay(true); }; this.bindParser = (parser) => { parser.on('packet', this.handlePacket); parser.on('error', (err) => __awaiter(this, void 0, void 0, function* () { if (this.status === PeerStatus.Closed) { return; } switch (err.code) { case errors_1.errorCodes.PARSER_INVALID_PACKET: case errors_1.errorCodes.PARSER_UNKNOWN_PACKET_TYPE: case errors_1.errorCodes.PARSER_DATA_INTEGRITY_ERR: case errors_1.errorCodes.PARSER_MAX_BUFFER_SIZE_EXCEEDED: case errors_1.errorCodes.FRAMER_MSG_NOT_ENCRYPTED: case errors_1.errorCodes.FRAMER_INVALID_NETWORK_MAGIC_VALUE: case errors_1.errorCodes.FRAMER_INCOMPATIBLE_MSG_ORIGIN_NETWORK: case errors_1.errorCodes.FRAMER_INVALID_MSG_LENGTH: this.logger.warn(`Peer (${this.label}): ${err.message}`); this.emit('reputation', enums_1.ReputationEvent.WireProtocolErr); yield this.close(enums_1.DisconnectionReason.WireProtocolErr, err.message); break; } })); }; /** Checks if a given packet is solicited and fulfills the pending response entry if it's a response. */ this.isPacketSolicited = (packet) => __awaiter(this, void 0, void 0, function* () { if (this.status !== PeerStatus.Open && packet.type !== packets_1.PacketType.SessionInit && packet.type !== packets_1.PacketType.SessionAck && packet.type !== packets_1.PacketType.Disconnecting) { // until the connection is opened, we only accept SessionInit, SessionAck, and Disconnecting packets return false; } if (packet.direction === packets_1.PacketDirection.Response) { // lookup a pending response entry for this packet by its reqId if (!this.fulfillResponseEntry(packet)) { return false; } } return true; }); this.handlePacket = (packet) => __awaiter(this, void 0, void 0, function* () { this.logger.trace(`Received ${packets_1.PacketType[packet.type]} packet from ${this.label}: ${JSON.stringify(packet)}`); if (yield this.isPacketSolicited(packet)) { switch (packet.type) { case packets_1.PacketType.SessionInit: { this.handleSessionInit(packet); break; } case packets_1.PacketType.NodeStateUpdate: { this.handleNodeStateUpdate(packet); break; } case packets_1.PacketType.Ping: { yield this.handlePing(packet); break; } case packets_1.PacketType.Disconnecting: { this.handleDisconnecting(packet); break; } default: this.emit('packet', packet); break; } } else { // TODO: penalize for unsolicited packets } }); /** * Authenticates the identity of a peer with a [[SessionInitPacket]] and sets the peer's node state. * Throws an error and closes the peer if authentication fails. * @param packet the session init packet * @param nodePubKey our node pub key * @param expectedNodePubKey the expected node pub key of the sender of the init packet */ this.authenticateSessionInit = (packet, nodePubKey, expectedNodePubKey) => __awaiter(this, void 0, void 0, function* () { const body = packet.body; const { sign } = body, bodyWithoutSign = __rest(body, ["sign"]); /** The pub key of the node that sent the init packet. */ const sourceNodePubKey = body.nodePubKey; /** The pub key of the node that the init packet is intended for. */ const targetNodePubKey = body.peerPubKey; // verify that the init packet came from the expected node if (expectedNodePubKey && expectedNodePubKey !== sourceNodePubKey) { yield this.close(enums_1.DisconnectionReason.UnexpectedIdentity); throw errors_1.default.UNEXPECTED_NODE_PUB_KEY(sourceNodePubKey, expectedNodePubKey, addressUtils_1.default.toString(this.address)); } // verify that the init packet was intended for us if (targetNodePubKey !== nodePubKey) { this.emit('reputation', enums_1.ReputationEvent.InvalidAuth); yield this.close(enums_1.DisconnectionReason.AuthFailureInvalidTarget); throw errors_1.default.AUTH_FAILURE_INVALID_TARGET(sourceNodePubKey, targetNodePubKey); } // verify that the msg was signed by the peer const msg = json_stable_stringify_1.default(bodyWithoutSign); const msgHash = crypto_1.createHash('sha256').update(msg).digest(); const verified = secp256k1_1.default.verify(msgHash, Buffer.from(sign, 'hex'), Buffer.from(sourceNodePubKey, 'hex')); if (!verified) { this.emit('reputation', enums_1.ReputationEvent.InvalidAuth); yield this.close(enums_1.DisconnectionReason.AuthFailureInvalidSignature); throw errors_1.default.AUTH_FAILURE_INVALID_SIGNATURE(sourceNodePubKey); } // finally set this peer's node state to the node state in the init packet body this.nodeState = body.nodeState; this.nodePubKey = body.nodePubKey; this._version = body.version; }); /** * Sends a [[SessionInitPacket]] and waits for a [[SessionAckPacket]]. */ this.initSession = (ownNodeState, ownNodeKey, ownVersion, expectedNodePubKey) => __awaiter(this, void 0, void 0, function* () { const ECDH = crypto_1.createECDH('secp256k1'); const ephemeralPubKey = ECDH.generateKeys().toString('hex'); const packet = this.createSessionInitPacket({ ephemeralPubKey, ownNodeState, ownNodeKey, ownVersion, expectedNodePubKey, }); yield this.sendPacket(packet); yield this.wait(packet.header.id, packet.responseType, Peer.RESPONSE_TIMEOUT, (packet) => { // enabling in-encryption synchronously, // expecting the following peer msg to be encrypted const sessionAck = packet; const key = ECDH.computeSecret(sessionAck.body.ephemeralPubKey, 'hex'); this.setInEncryption(key); }); }); /** * Sends a [[SessionAckPacket]] in response to a given [[SessionInitPacket]]. */ this.ackSession = (sessionInit) => __awaiter(this, void 0, void 0, function* () { const ECDH = crypto_1.createECDH('secp256k1'); const ephemeralPubKey = ECDH.generateKeys().toString('hex'); yield this.sendPacket(new packets.SessionAckPacket({ ephemeralPubKey }, sessionInit.header.id)); // enabling out-encryption synchronously, // so that the following msg will be encrypted const key = ECDH.computeSecret(sessionInit.body.ephemeralPubKey, 'hex'); this.setOutEncryption(key); }); /** * Begins the handshake by waiting for a [[SessionInitPacket]] as well as sending our own * [[SessionInitPacket]] first if we are the outbound peer. * @returns the session init packet we receive */ this.beginHandshake = (ownNodeState, ownNodeKey, ownVersion) => __awaiter(this, void 0, void 0, function* () { let sessionInit; if (!this.inbound) { // outbound handshake assert_1.default(this.expectedNodePubKey); yield this.initSession(ownNodeState, ownNodeKey, ownVersion, this.expectedNodePubKey); sessionInit = yield this.waitSessionInit(); yield this.authenticateSessionInit(sessionInit, ownNodeKey.pubKey, this.expectedNodePubKey); } else { // inbound handshake sessionInit = yield this.waitSessionInit(); yield this.authenticateSessionInit(sessionInit, ownNodeKey.pubKey); } return sessionInit; }); /** * Completes the handshake by sending the [[SessionAckPacket]] and our [[SessionInitPacket]] if it * has not been sent already, as is the case with inbound peers. */ this.completeHandshake = (ownNodeState, ownNodeKey, ownVersion, sessionInit) => __awaiter(this, void 0, void 0, function* () { if (!this.inbound) { // outbound handshake yield this.ackSession(sessionInit); } else { // inbound handshake yield this.ackSession(sessionInit); yield this.initSession(ownNodeState, ownNodeKey, ownVersion, sessionInit.body.nodePubKey); } }); this.sendPing = () => __awaiter(this, void 0, void 0, function* () { const packet = new packets.PingPacket(); yield this.sendPacket(packet); }); this.sendGetNodes = () => __awaiter(this, void 0, void 0, function* () { const packet = new packets.GetNodesPacket(); yield this.sendPacket(packet); return packet; }); this.discoverNodes = () => __awaiter(this, void 0, void 0, function* () { const packet = yield this.sendGetNodes(); const res = yield this.wait(packet.header.id, packet.responseType); return res.body.length; }); this.sendPong = (pingId) => __awaiter(this, void 0, void 0, function* () { const packet = new packets.PongPacket(undefined, pingId); yield this.sendPacket(packet); }); this.handlePing = (packet) => __awaiter(this, void 0, void 0, function* () { yield this.sendPong(packet.header.id); }); this.createSessionInitPacket = ({ ephemeralPubKey, ownNodeState, ownNodeKey, ownVersion, expectedNodePubKey }) => { let body = { ephemeralPubKey, version: ownVersion, peerPubKey: expectedNodePubKey, nodePubKey: ownNodeKey.pubKey, nodeState: ownNodeState, }; const msg = json_stable_stringify_1.default(body); const msgHash = crypto_1.createHash('sha256').update(msg).digest(); const { signature } = secp256k1_1.default.sign(msgHash, ownNodeKey.privKey); body = Object.assign(Object.assign({}, body), { sign: signature.toString('hex') }); return new packets.SessionInitPacket(body); }; this.handleDisconnecting = (packet) => { if (!this.recvDisconnectionReason && packet.body && packet.body.reason !== undefined) { this.logger.debug(`received disconnecting packet from ${this.label} due to ${enums_1.DisconnectionReason[packet.body.reason]}`); this.recvDisconnectionReason = packet.body.reason; } else { // protocol violation: packet should be sent once only, with body, with `reason` field // TODO: penalize peer } }; this.handleSessionInit = (packet) => { this.sessionInitPacket = packet; const entry = this.responseMap.get(packets_1.PacketType.SessionInit.toString()); if (entry) { this.responseMap.delete(packets_1.PacketType.SessionInit.toString()); entry.resolve(packet); } }; this.handleNodeStateUpdate = (packet) => { const nodeStateUpdate = packet.body; this.logger.verbose(`received node state update packet from ${this.label}: ${JSON.stringify(nodeStateUpdate)}`); this.activePairs.forEach((pairId) => { if (!nodeStateUpdate.pairs.includes(pairId)) { // a trading pair was previously active but is not in the updated node state this.deactivatePair(pairId); } }); this.nodeState = nodeStateUpdate; this.emit('nodeStateUpdate'); this.emit('verifyPairs'); }; this.setOutEncryption = (key) => { this.outEncryptionKey = key; this.logger.debug(`Peer ${this.label} session out-encryption enabled`); }; this.setInEncryption = (key) => { this.parser.setEncryptionKey(key); this.logger.debug(`Peer ${this.label} session in-encryption enabled`); }; this.network = network; this.framer = new Framer_1.default(this.network); this.parser = new Parser_1.default(this.framer); this.bindParser(this.parser); } /** The version of xud this peer is using, or an empty string if it is still not known. */ get version() { return this._version || ''; } /** The hex-encoded node public key for this peer, or undefined if it is still not known. */ get nodePubKey() { return this._nodePubKey; } set nodePubKey(nodePubKey) { this._nodePubKey = nodePubKey; this._alias = nodePubKey ? aliasUtils_1.pubKeyToAlias(nodePubKey) : undefined; } get alias() { return this._alias; } /* The label is used to describe the node in logs and error messages only */ get label() { if (this.nodePubKey) { return `${this.nodePubKey} (${this.alias})`; } return this.expectedNodePubKey ? `${this.expectedNodePubKey}@${addressUtils_1.default.toString(this.address)}` : addressUtils_1.default.toString(this.address); } get addresses() { return this.nodeState ? this.nodeState.addresses : undefined; } get connextIdentifier() { return this.nodeState ? this.nodeState.connextIdentifier : undefined; } /** Returns a list of trading pairs advertised by this peer. */ get advertisedPairs() { if (this.nodeState) { return this.nodeState.pairs; } return []; } get connected() { return this.socket !== undefined && !this.socket.destroyed; } get info() { return { address: addressUtils_1.default.toString(this.address), alias: this.alias, nodePubKey: this.nodePubKey, inbound: this.inbound, pairs: this.nodeState ? this.nodeState.pairs : [], xudVersion: this.version, secondsConnected: Math.round((Date.now() - this.connectTime) / 1000), lndPubKeys: this.nodeState ? this.nodeState.lndPubKeys : undefined, connextIdentifier: this.nodeState ? this.nodeState.connextIdentifier : undefined, }; } getLndPubKey(currency) { if (!this.nodeState || !currency) { return undefined; } return this.nodeState.lndPubKeys[currency]; } /** * Gets lnd client's listening uris for the provided currency. * @param currency */ getLndUris(currency) { if (this.nodeState && this.nodeState.lndUris) { return this.nodeState.lndUris[currency]; } return; } /** * Sets public key and alias for this node together so that they are always in sync. */ setIdentifiers(nodePubKey) { this._nodePubKey = nodePubKey; this._alias = aliasUtils_1.pubKeyToAlias(nodePubKey); } } /** Interval to check required responses from peer. */ Peer.STALL_INTERVAL = 5000; /** Interval for pinging peers. */ Peer.PING_INTERVAL = 30000; /** Interval for checking if we can reactivate any inactive pairs with peers. */ Peer.CHECK_PAIRS_INTERVAL = 60000; /** Response timeout for response packets. */ Peer.RESPONSE_TIMEOUT = 10000; /** Connection retries min delay. */ Peer.CONNECTION_RETRIES_MIN_DELAY = 5000; /** Connection retries max delay. */ Peer.CONNECTION_RETRIES_MAX_DELAY = 3600000; // 1 hour /** Connection retries max period. */ Peer.CONNECTION_RETRIES_MAX_PERIOD = 259200000; // 3 days /** * Creates a Peer from an inbound socket connection. */ Peer.fromInbound = (socket, logger, network) => { const peer = new Peer(logger, addressUtils_1.default.fromSocket(socket), network); peer.inbound = true; peer.socket = socket; peer.bindSocket(); return peer; }; return Peer; })(); /** A class representing a wait for an anticipated response packet from a peer. */ class PendingResponseEntry { constructor(resType) { this.resType = resType; this.timeout = 0; /** An array of tasks to resolve or reject. */ this.jobs = []; /** An array of callbacks to be called synchronously when entry resolve. */ this.callbacks = []; this.addJob = (resolve, reject) => { this.jobs.push(new Job(resolve, reject)); }; this.addCb = (cb) => { this.callbacks.push(cb); }; this.setTimeout = (timeout) => { this.timeout = utils_1.ms() + timeout; }; this.resolve = (result) => { for (const job of this.jobs) { job.resolve(result); } for (const cb of this.callbacks) { cb(result); } this.jobs.length = 0; this.callbacks.length = 0; }; this.reject = (err) => { for (const job of this.jobs) { job.reject(err); } this.jobs.length = 0; }; } } /** A pair of functions for resolving or rejecting a task. */ class Job { constructor(resolve, reject) { this.resolve = resolve; this.reject = reject; } } exports.default = Peer; //# sourceMappingURL=Peer.js.map