xud
Version:
Exchange Union Daemon
927 lines • 46.7 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 __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