UNPKG

@sangaman/xud

Version:
597 lines 24.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 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) : new P(function (resolve) { resolve(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 }; }; 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)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); const net_1 = __importDefault(require("net")); const events_1 = require("events"); const Parser_1 = __importStar(require("./Parser")); const packets = __importStar(require("./packets/types")); const utils_1 = require("../utils/utils"); const packets_1 = require("./packets"); const errors_1 = __importDefault(require("./errors")); const addressUtils_1 = __importDefault(require("../utils/addressUtils")); const enums_1 = require("../types/enums"); const crypto_1 = require("crypto"); /** Represents a remote XU peer */ class Peer extends events_1.EventEmitter { constructor(logger, pool) { super(); this.logger = logger; this.pool = pool; this.connected = false; this.opened = false; this.parser = new Parser_1.default(packets_1.Packet.PROTOCOL_DELIMITER); this.closed = false; this.responseMap = new Map(); this.banScore = 0; this.lastRecv = 0; this.lastSend = 0; /** A counter for packets sent to be used for assigning unique packet ids. */ this.packetCount = 0; this.getStatus = () => { let status; if (this.connected) { status = this.nodePubKey ? `Connected to peer ${this.nodePubKey}` : `Connected pre-handshake to peer ${addressUtils_1.default.toString(this.socketAddress)}`; } else { status = 'Not connected'; } return status; }; /** * Prepare a connection for use by ensuring it is active, exchanging [[HelloPacket]] with handshake data, * and emit the `open` event if everything succeeds. Throw an error on unexpected handshake data. * @param handshakeData our handshake data to send to the peer * @param nodePubKey the expected nodePubKey of the node we are opening a connection with */ this.open = (handshakeData, nodePubKey) => __awaiter(this, void 0, void 0, function* () { assert_1.default(!this.opened); assert_1.default(!this.closed); this.opened = true; yield this.initConnection(); this.initStall(); yield this.initHello(handshakeData); // TODO: Check that the peer's version is compatible with ours if (nodePubKey && this.nodePubKey !== nodePubKey) { this.close(); throw errors_1.default.UNEXPECTED_NODE_PUB_KEY(this.nodePubKey, nodePubKey, addressUtils_1.default.toString(this.socketAddress)); } this.finalizeOpen(); // let the pool know that this peer is ready to go this.emit('open'); }); /** * Close a peer by ensuring the socket is destroyed and terminating all timers. */ this.close = () => { if (this.closed) { return; } this.closed = true; this.connected = false; if (this.socket) { if (!this.socket.destroyed) { this.socket.destroy(); } delete this.socket; } if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = undefined; } if (this.stallTimer) { clearInterval(this.stallTimer); this.stallTimer = undefined; } if (this.connectTimeout) { clearTimeout(this.connectTimeout); this.connectTimeout = undefined; } for (const [packetType, entry] of this.responseMap) { this.responseMap.delete(packetType); entry.reject(new Error('Peer was destroyed')); } this.emit('close'); }; this.sendPacket = (packet) => { this.sendRaw(packet.toRaw()); this.packetCount += 1; if (packet.direction === packets_1.PacketDirection.REQUEST) { this.addResponseTimeout(packet.header.id, Peer.RESPONSE_TIMEOUT); } }; this.sendOrders = (orders, reqId) => { const packet = new packets.OrdersPacket(orders, reqId); this.sendPacket(packet); }; this.sendNodes = (nodes, reqId) => { const packet = new packets.NodesPacket(nodes, reqId); this.sendPacket(packet); }; this.sendRaw = (packetStr) => { if (this.socket) { this.socket.write(packetStr + packets_1.Packet.PROTOCOL_DELIMITER); this.lastSend = Date.now(); } }; this.increaseBan = (score) => { this.banScore += score; if (this.banScore >= 100) { // TODO: make configurable this.logger.debug(`Ban threshold exceeded (${this.nodePubKey})`); this.emit('ban'); return true; } return false; }; /** * 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 = () => { assert_1.default(this.socket); if (this.connected) { assert_1.default(this.inbound); this.connectTime = Date.now(); this.logger.debug(this.getStatus()); return Promise.resolve(); } return new Promise((resolve, reject) => { const cleanup = () => { if (this.connectTimeout) { clearTimeout(this.connectTimeout); this.connectTimeout = undefined; } if (this.socket) { this.socket.removeListener('error', onError); this.socket.removeListener('connect', onConnect); } }; const onError = (err) => { cleanup(); reject(err); }; const onConnect = () => { this.connectTime = Date.now(); this.connected = true; this.logger.debug(this.getStatus()); this.emit('connect'); cleanup(); resolve(); }; const onTimeout = () => { cleanup(); reject(new Error('Connection timed out.')); }; this.socket.once('connect', onConnect); this.socket.once('error', onError); this.connectTimeout = setTimeout(onTimeout, Peer.CONNECTION_TIMEOUT); }); }; this.initStall = () => { assert_1.default(!this.closed); assert_1.default(!this.stallTimer); this.stallTimer = setInterval(this.checkTimeout, Peer.STALL_INTERVAL); }; this.initHello = (handshakeData) => __awaiter(this, void 0, void 0, function* () { const packet = this.sendHello(handshakeData); if (!this.handshakeState) { // wait for an incoming HelloPacket yield this.wait(packets_1.PacketType.HELLO, Peer.RESPONSE_TIMEOUT); } }); this.finalizeOpen = () => { assert_1.default(!this.closed); // Setup the ping interval this.pingTimer = setInterval(this.sendPing, Peer.PING_INTERVAL); }; /** * Wait for a packet to be received from peer. Executed on timeout or once packet is received. */ this.wait = (packetId, timeout) => { const entry = this.getOrAddPendingResponseEntry(packetId); return new Promise((resolve, reject) => { entry.addJob(resolve, reject); if (timeout) { entry.setTimeout(timeout); } }); }; /** * Potentially timeout peer if it hasn't responded. */ this.checkTimeout = () => { const now = utils_1.ms(); for (const [packetType, entry] of this.responseMap) { if (now > entry.timeout) { this.error(`Peer (${this.nodePubKey}) is stalling (${packetType})`); this.close(); return; } } }; /** * Wait for a packet to be received from peer. */ this.addResponseTimeout = (packetId, timeout) => { if (this.closed) { return undefined; } const entry = this.getOrAddPendingResponseEntry(packetId); entry.setTimeout(timeout); return entry; }; this.getOrAddPendingResponseEntry = (packetId) => { let entry = this.responseMap.get(packetId); if (!entry) { entry = new PendingResponseEntry(); this.responseMap.set(packetId, 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.nodePubKey}) sent a response packet without reqId`); // TODO: penalize return false; } const entry = this.responseMap.get(reqId); if (!entry) { this.logger.debug(`Peer (${this.nodePubKey}) sent an unsolicited response packet (${reqId})`); // TODO: penalize return false; } this.responseMap.delete(reqId); entry.resolve(packet); return true; }; this.connect = (address) => { assert_1.default(!this.socket); const socket = net_1.default.connect(address.port, address.host); this.socketAddress = address; this.inbound = false; this.connected = false; this.bindSocket(socket); }; this.accept = (socket) => { assert_1.default(!this.socket); this.socketAddress = addressUtils_1.default.fromSocket(socket); this.inbound = true; this.connected = true; this.bindSocket(socket); }; this.bindSocket = (socket) => { assert_1.default(!this.socket); this.socket = socket; this.socket.once('error', (err) => { if (!this.connected) { return; } this.error(err); // socket close event will be called immediately after the socket error }); this.socket.once('close', (hadError) => { // emitted once the socket is fully closed if (this.connected) { // don't log anything on close if we've never connected as the connection failure is logged elsewhere if (this.nodePubKey === undefined) { this.logger.info(`Socket closed prior to handshake (${addressUtils_1.default.toString(this.socketAddress)})`); } else if (hadError) { this.logger.warn(`Peer ${this.nodePubKey} socket closed due to error`); } else { this.logger.info(`Peer ${this.nodePubKey} socket closed`); } } this.close(); }); this.socket.on('data', (data) => { this.lastRecv = Date.now(); const dataStr = data.toString(); if (this.nodePubKey !== undefined) { this.logger.debug(`Received data (${this.nodePubKey}): ${dataStr}`); } else { this.logger.debug(`Received data (${addressUtils_1.default.toString(this.socketAddress)}): ${data.toString()}`); } this.parser.feed(dataStr); }); this.socket.setNoDelay(true); }; this.bindParser = (parser) => { parser.on('packet', this.handlePacket); parser.on('error', (err) => { if (this.closed) { return; } switch (err.type) { case Parser_1.ParserErrorType.UNPARSEABLE_MESSAGE: this.logger.warn(`Unparsable peer message: ${err.payload}`); this.increaseBan(10); break; case Parser_1.ParserErrorType.INVALID_MESSAGE: this.logger.warn(`Invalid peer message: ${err.payload}`); this.increaseBan(10); break; case Parser_1.ParserErrorType.UNKNOWN_PACKET_TYPE: this.logger.warn(`Unknown peer message type: ${err.payload}`); this.increaseBan(20); } }); }; /** Check if a given packet is solicited and fulfill the pending response entry if it's a response. */ this.isPacketSolicited = (packet) => { let solicted = true; if (packet.direction === packets_1.PacketDirection.RESPONSE) { // lookup a pending response entry for this packet by its reqId if (!this.fulfillResponseEntry(packet)) { solicted = false; } } return solicted; }; this.handlePacket = (packet) => { if (this.isPacketSolicited(packet)) { switch (packet.type) { case packets_1.PacketType.HELLO: { this.handleHello(packet); break; } case packets_1.PacketType.PING: { this.handlePing(packet); break; } case packets_1.PacketType.DEAL_REQUEST: { this.handleDealRequest(packet); break; } case packets_1.PacketType.DEAL_RESPONSE: { this.handleDealResponse(packet); break; } case packets_1.PacketType.SWAP_REQUEST: { this.handleSwapRequest(packet); break; } case packets_1.PacketType.SWAP_RESPONSE: { this.handleSwapResponse(packet); break; } default: this.emit('packet', packet); break; } } }; this.error = (err) => { if (this.closed) { return; } if (err instanceof Error) { this.emit('error', err); } else { this.emit('error', new Error(err)); } }; this.sendHello = (handshakeData) => { // TODO: use real values const packet = new packets.HelloPacket(handshakeData); this.sendPacket(packet); return packet; }; this.handleHello = (packet) => { const entry = this.responseMap.get(packets_1.PacketType.HELLO); if (!entry) { this.logger.debug(`Peer (${this.nodePubKey}) sent an unsolicited Hello packet`); // TODO: penalize } else { this.responseMap.delete(packets_1.PacketType.HELLO); entry.resolve(packet); this.handshakeState = packet.body; } }; this.sendPing = () => { const packet = new packets.PingPacket(); this.sendPacket(packet); return packet; }; this.handlePing = (packet) => { this.sendPong(packet.header.id); }; this.handleDealRequest = (request) => { this.logger.debug('Got this: ' + JSON.stringify(request)); const requestBody = request.body; if (!requestBody) { return; } const preImage = crypto_1.randomBytes(32).toString('hex'); const hash = crypto_1.createHash('sha256'); const r_hash = hash.update(preImage).digest('hex'); const makerDealId = crypto_1.randomBytes(32).toString('hex'); let makerPubKey; switch (requestBody.makerCoin) { case enums_1.CurrencyType.BTC: makerPubKey = this.pool.lndBtcClient ? this.pool.lndBtcClient.pubKey : undefined; break; case enums_1.CurrencyType.LTC: makerPubKey = this.pool.lndLtcClient ? this.pool.lndLtcClient.pubKey : undefined; break; default: return; } if (!makerPubKey) { // TODO: proper error handling. return; } const deal = { makerDealId, makerPubKey, preImage, r_hash, myRole: enums_1.SwapDealRole.Maker, takerAmount: requestBody.takerAmount, takerCoin: requestBody.takerCoin, takerPubKey: requestBody.takerPubKey, takerDealId: requestBody.takerDealId, makerAmount: requestBody.makerAmount, makerCoin: requestBody.makerCoin, createTime: Date.now(), }; const body = { makerPubKey, makerDealId, r_hash, takerDealId: requestBody.takerDealId, }; this.pool.swapDeals.add(deal); this.logger.debug('swap deal: ' + JSON.stringify(deal)); this.logger.debug('sending back to peer: ' + JSON.stringify(body)); const packet = new packets.DealResponse(body, request.header.id); this.sendPacket(packet); }; this.handleDealResponse = (response) => { if (!response.body) { return; } const deal = this.pool.swapDeals.get(enums_1.SwapDealRole.Taker, response.body.takerDealId); if (!deal) { return; } deal.makerPubKey = response.body.makerPubKey; deal.makerDealId = response.body.makerDealId; deal.r_hash = response.body.r_hash; this.logger.debug('updated deal: ' + JSON.stringify(this.pool.swapDeals.get(enums_1.SwapDealRole.Taker, response.body.takerDealId))); const body = { makerDealId: deal.makerDealId, }; const packet = new packets.SwapRequest(body); this.sendPacket(packet); }; this.handleSwapRequest = (request) => { if (!request.body) { return; } const deal = this.pool.swapDeals.get(enums_1.SwapDealRole.Maker, request.body.makerDealId); if (!deal) { return; } const body = { r_preimage: deal.preImage, }; const packet = new packets.SwapResponse(body, request.header.id); this.logger.debug('sending back to peer: ' + JSON.stringify(body)); this.sendPacket(packet); }; this.handleSwapResponse = (response) => { if (!response) { return; } if (!response.body) { return; } this.logger.debug('Swap completed. r_preimage = ' + response.body.r_preimage); }; this.sendPong = (pingId) => { const packet = new packets.PongPacket(undefined, pingId); this.sendPacket(packet); return packet; }; this.bindParser(this.parser); } get nodePubKey() { return this.handshakeState ? this.handshakeState.nodePubKey : undefined; } get addresses() { return this.handshakeState ? this.handshakeState.addresses : undefined; } get info() { return { address: addressUtils_1.default.toString(this.socketAddress), nodePubKey: this.handshakeState ? this.handshakeState.nodePubKey : undefined, inbound: this.inbound, pairs: this.handshakeState ? this.handshakeState.pairs : undefined, xudVersion: this.handshakeState ? this.handshakeState.version : undefined, secondsConnected: Math.round((Date.now() - this.connectTime) / 1000), }; } /** Create an outbound connection to a node. */ static fromOutbound(address, logger, pool) { const peer = new Peer(logger, pool); peer.connect(address); return peer; } static fromInbound(socket, logger, pool) { const peer = new Peer(logger, pool); peer.accept(socket); return peer; } } /** Interval to check required responses from peer. */ Peer.STALL_INTERVAL = 5000; /** Interval for pinging peers. */ Peer.PING_INTERVAL = 30000; /** Socket connection timeout for outbound peers. */ Peer.CONNECTION_TIMEOUT = 10000; /** Response timeout for response packets. */ Peer.RESPONSE_TIMEOUT = 10000; /** A class representing a wait for an anticipated response packet from a peer. */ class PendingResponseEntry { constructor() { this.timeout = 0; /** An array of tasks to resolve or reject. */ this.jobs = []; this.addJob = (resolve, reject) => { this.jobs.push(new Job(resolve, reject)); }; this.setTimeout = (timeout) => { this.timeout = utils_1.ms() + timeout; }; this.resolve = (result) => { for (const job of this.jobs) { job.resolve(result); } this.jobs.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