UNPKG

ilp-protocol-stream

Version:

Interledger Transport Protocol for sending multiple streams of money and data over ILP.

958 lines 64.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (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 (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Connection = exports.IlpRejectionError = exports.ConnectionError = void 0; const events_1 = require("events"); const ilp_logger_1 = __importDefault(require("ilp-logger")); const stream_1 = require("./stream"); const IlpPacket = __importStar(require("ilp-packet")); const cryptoHelper = __importStar(require("./crypto")); const packet_1 = require("./packet"); const oer_utils_1 = require("oer-utils"); const congestion_1 = require("./util/congestion"); const long_1 = require("./util/long"); const long_2 = __importDefault(require("long")); const rational_1 = __importDefault(require("./util/rational")); const receipt_1 = require("./util/receipt"); const stoppable_timeout_1 = require("./util/stoppable-timeout"); const uuid_1 = require("uuid"); const RETRY_DELAY_START = 100; const RETRY_DELAY_MAX = 43200000; const RETRY_DELAY_INCREASE_FACTOR = 1.5; const DEFAULT_PACKET_TIMEOUT = 30000; const DEFAULT_IDLE_TIMEOUT = 60000; const MAX_DATA_SIZE = 32767; const DEFAULT_MAX_REMOTE_STREAMS = 10; const DEFAULT_MINIMUM_EXCHANGE_RATE_PRECISION = 3; const TEST_PACKET_MAX_ATTEMPTS = 15; class ConnectionError extends Error { constructor(message, streamErrorCode) { super(message); this.streamErrorCode = streamErrorCode || packet_1.ErrorCode.InternalError; } } exports.ConnectionError = ConnectionError; class IlpRejectionError extends Error { constructor(message, ilpReject) { super(message); this.ilpReject = ilpReject; } } exports.IlpRejectionError = IlpRejectionError; var RemoteState; (function (RemoteState) { RemoteState[RemoteState["Init"] = 0] = "Init"; RemoteState[RemoteState["Connected"] = 1] = "Connected"; RemoteState[RemoteState["Closed"] = 2] = "Closed"; })(RemoteState || (RemoteState = {})); function defaultGetExpiry() { return new Date(Date.now() + DEFAULT_PACKET_TIMEOUT); } class Connection extends events_1.EventEmitter { constructor(opts) { super(); this.rateRetryTimer = new stoppable_timeout_1.StoppableTimeout(); this.looping = false; this.done = false; this.remoteState = RemoteState.Init; const lastAddressSegment = opts.destinationAccount ? opts.destinationAccount.split('.').slice(-1)[0] : undefined; this.connectionId = (opts.connectionId || lastAddressSegment || (0, uuid_1.v4)()) .replace(/[-_]/g, '') .slice(0, 8); this.plugin = opts.plugin; this._sourceAccount = opts.sourceAccount; this._sourceAssetCode = opts.assetCode; this._sourceAssetScale = opts.assetScale; this._destinationAccount = opts.destinationAccount; this.sharedSecret = opts.sharedSecret; this.isServer = opts.isServer; this._pskKey = opts.pskKey; this._fulfillmentKey = opts.fulfillmentKey; this.slippage = rational_1.default.fromNumber(opts.slippage === undefined ? 0.01 : opts.slippage, true); if (this.slippage.greaterThanOne()) { throw new Error('slippage must be less than one'); } this.allowableReceiveExtra = rational_1.default.fromNumber(1.01, true); this.enablePadding = !!opts.enablePadding; this.connectionTag = opts.connectionTag; if (!opts.receiptNonce !== !opts.receiptSecret) { throw new Error('receiptNonce and receiptSecret must accompany each other'); } this._receiptNonce = opts.receiptNonce; this._receiptSecret = opts.receiptSecret; this.maxStreamId = 2 * (opts.maxRemoteStreams || DEFAULT_MAX_REMOTE_STREAMS); this.maxBufferedData = opts.connectionBufferSize || MAX_DATA_SIZE * 2; this.minExchangeRatePrecision = opts.minExchangeRatePrecision || DEFAULT_MINIMUM_EXCHANGE_RATE_PRECISION; this.exchangeRate = opts.exchangeRate === undefined ? undefined : rational_1.default.fromNumber(opts.exchangeRate, true); this.getExpiry = opts.getExpiry || defaultGetExpiry; this.shouldFulfill = opts.shouldFulfill; this.idleTimeout = opts.idleTimeout || DEFAULT_IDLE_TIMEOUT; this.lastActive = new Date(); this.nextPacketSequence = 1; this.streams = new Map(); this.closedStreams = new Set(); this.nextStreamId = this.isServer ? 2 : 1; this.log = (0, ilp_logger_1.default)(`ilp-protocol-stream:${this.isServer ? 'Server' : 'Client'}:Connection:${this.connectionId}`); this.sending = false; this.connected = false; this.closed = true; this.queuedFrames = []; this.congestion = new congestion_1.CongestionController({ maximumPacketAmount: opts.maximumPacketAmount === undefined ? undefined : long_2.default.fromString(opts.maximumPacketAmount, true), }); this.retryDelay = RETRY_DELAY_START; this.remoteKnowsOurAccount = this.isServer; this.remoteMaxStreamId = DEFAULT_MAX_REMOTE_STREAMS * 2; this.remoteMaxOffset = this.maxBufferedData; this._incomingHold = long_2.default.UZERO; this._totalReceived = long_2.default.UZERO; this._totalSent = long_2.default.UZERO; this._totalDelivered = long_2.default.UZERO; this._lastPacketExchangeRate = rational_1.default.UZERO; this.once('connect', () => this.startIdleTimer()); } static async build(opts) { const pskKey = await cryptoHelper.generatePskEncryptionKey(opts.sharedSecret); const fulfillmentKey = await cryptoHelper.generateFulfillmentKey(opts.sharedSecret); return new Connection(Object.assign({ pskKey, fulfillmentKey }, opts)); } async connect() { if (!this.closed) { return Promise.resolve(); } this.startSendLoop(); await new Promise((resolve, reject) => { const connectHandler = () => { cleanup(); resolve(); }; const closeHandler = () => { cleanup(); this.stopTimers(); reject(new Error('Connection was closed before it was connected')); }; const errorHandler = (error) => { cleanup(); this.stopTimers(); reject(new Error(`Error connecting${error ? ': ' + error.message : ''}`)); }; this.once('connect', connectHandler); this.once('error', errorHandler); this.once('close', closeHandler); this.once('end', closeHandler); const cleanup = () => { this.removeListener('connect', connectHandler); this.removeListener('error', errorHandler); this.removeListener('close', closeHandler); this.removeListener('end', closeHandler); }; }); this.closed = false; } async end() { this.log.info('closing connection'); const streamEndPromises = []; for (const [, stream] of this.streams) { if (stream.isOpen()) { streamEndPromises.push(new Promise((resolve) => { stream.on('end', resolve); })); stream.end(); } } await new Promise((resolve, reject) => { this.once('_send_loop_finished', resolve); this.once('error', reject); this.startSendLoop(); }); await Promise.all(streamEndPromises); this.closed = true; await this.sendConnectionClose(); this.safeEmit('end'); this.safeEmit('close'); this.stopTimers(); } async destroy(err) { this.log.error('destroying connection with error:', err); if (this.done) return; if (err) { this.safeEmit('error', err); } const streamClosePromises = []; for (const [, stream] of this.streams) { streamClosePromises.push(new Promise((resolve) => { stream.on('close', resolve); })); stream.destroy(); } await this.sendConnectionClose(err || new ConnectionError('Connection destroyed', packet_1.ErrorCode.ApplicationError)); await Promise.all(streamClosePromises); this.safeEmit('close'); this.stopTimers(); } createStream() { if (this.remoteMaxStreamId < this.nextStreamId) { this.log.debug('cannot create another stream. nextStreamId: %d, remote maxStreamId: %d', this.nextStreamId, this.remoteMaxStreamId); this.queuedFrames.push(new packet_1.ConnectionStreamIdBlockedFrame(this.nextStreamId)); throw new Error(`Creating another stream would exceed the remote connection's maximum number of open streams`); } const stream = new stream_1.DataAndMoneyStream({ id: this.nextStreamId, isServer: this.isServer, connectionId: this.connectionId, }); this.streams.set(this.nextStreamId, stream); this.log.debug('created stream: %d', this.nextStreamId); this.nextStreamId += 2; stream.on('_maybe_start_send_loop', this.startSendLoop.bind(this)); stream.once('close', () => this.removeStreamRecord(stream)); return stream; } get destinationAccount() { return this._destinationAccount; } get destinationAssetScale() { return this._destinationAssetScale; } get destinationAssetCode() { return this._destinationAssetCode; } get sourceAccount() { return this._sourceAccount; } get sourceAssetScale() { return this._sourceAssetScale; } get sourceAssetCode() { return this._sourceAssetCode; } get minimumAcceptableExchangeRate() { if (this.exchangeRate) { const minimumExchangeWithSlippage = this.exchangeRate.multiplyByRational(this.slippage.complement()); return minimumExchangeWithSlippage.toString(); } return '0'; } get lastPacketExchangeRate() { return this._lastPacketExchangeRate.toString(); } get totalDelivered() { return this._totalDelivered.toString(); } get totalSent() { return this._totalSent.toString(); } get totalReceived() { return this._totalReceived.toString(); } async handlePrepare(prepare) { let requestPacket; try { requestPacket = await packet_1.Packet.decryptAndDeserialize(this._pskKey, prepare.data); } catch (err) { this.log.error('error parsing frames:', err); throw new IlpPacket.Errors.UnexpectedPaymentError(''); } this.log.trace('handling packet:', JSON.stringify(requestPacket)); if (requestPacket.ilpPacketType.valueOf() !== IlpPacket.Type.TYPE_ILP_PREPARE) { this.log.error('prepare packet contains a frame that says it should be something other than a prepare: %d', requestPacket.ilpPacketType); throw new IlpPacket.Errors.UnexpectedPaymentError(''); } this.bumpIdle(); let responseFrames = []; responseFrames.push(new packet_1.ConnectionMaxDataFrame(this.getIncomingOffsets().maxAcceptable)); const constructFinalApplicationError = async () => { responseFrames = responseFrames.concat(this.queuedFrames); this.queuedFrames = []; const responsePacket = new packet_1.Packet(requestPacket.sequence, packet_1.IlpPacketType.Reject, prepare.amount, responseFrames); this.log.trace('rejecting packet %s: %j', requestPacket.sequence, responsePacket); return new IlpPacket.Errors.FinalApplicationError('', await responsePacket.serializeAndEncrypt(this._pskKey, this.enablePadding ? MAX_DATA_SIZE : undefined)); }; for (const frame of requestPacket.frames) { if (frame.type === packet_1.FrameType.StreamMoney || frame.type === packet_1.FrameType.StreamData || frame.type === packet_1.FrameType.StreamMaxMoney || frame.type === packet_1.FrameType.StreamMaxData) { const streamId = frame.streamId.toNumber(); if (this.closedStreams.has(streamId)) { this.log.trace('got packet with frame for stream %d, which was already closed', streamId); if (frame.type !== packet_1.FrameType.StreamMoney && frame.type !== packet_1.FrameType.StreamData) { continue; } const testStreamClose = (frame) => { return frame.type === packet_1.FrameType.StreamClose && frame.streamId.equals(streamId); }; const includesStreamClose = responseFrames.find(testStreamClose) || this.queuedFrames.find(testStreamClose); if (!includesStreamClose) { responseFrames.push(new packet_1.StreamCloseFrame(streamId, packet_1.ErrorCode.StreamStateError, 'Stream is already closed')); } throw await constructFinalApplicationError(); } try { this.handleNewStream(frame.streamId.toNumber()); } catch (err) { this.log.debug('error handling new stream %s: %s', frame.streamId, err); throw await constructFinalApplicationError(); } } } try { this.handleControlFrames(requestPacket.frames); } catch (err) { this.log.debug('error handling frames:', err); throw await constructFinalApplicationError(); } const incomingOffsets = this.getIncomingOffsets(); if (incomingOffsets.max > incomingOffsets.maxAcceptable) { this.destroy(new ConnectionError(`Exceeded flow control limits. Max connection byte offset: ${incomingOffsets.maxAcceptable}, received: ${incomingOffsets.max}`, packet_1.ErrorCode.FlowControlError)); throw await constructFinalApplicationError(); } const incomingAmount = long_2.default.fromString(prepare.amount, true); if (requestPacket.prepareAmount.greaterThan(incomingAmount)) { this.log.debug('received less than minimum destination amount. actual: %s, expected: %s', prepare.amount, requestPacket.prepareAmount); throw await constructFinalApplicationError(); } const fulfillment = await cryptoHelper.generateFulfillment(this._fulfillmentKey, prepare.data); const generatedCondition = await cryptoHelper.hash(fulfillment); if (!generatedCondition.equals(prepare.executionCondition)) { this.log.debug('got unfulfillable prepare for amount: %s. generated condition: %h, prepare condition: %h', prepare.amount, generatedCondition, prepare.executionCondition); throw await constructFinalApplicationError(); } const amountsToReceive = []; const totalMoneyShares = requestPacket.frames.reduce((sum, frame) => { if (frame instanceof packet_1.StreamMoneyFrame) { const result = (0, long_1.checkedAdd)(sum, frame.shares); if (result.overflow) throw new Error('Total shares exceeded MaxUint64'); return result.sum; } return sum; }, long_2.default.UZERO); for (const frame of requestPacket.frames) { if (!(frame instanceof packet_1.StreamMoneyFrame)) { continue; } const streamId = frame.streamId.toNumber(); const streamAmount = (0, long_1.multiplyDivideFloor)(incomingAmount, frame.shares, totalMoneyShares); const stream = this.streams.get(streamId); if (!stream) { this.log.debug("peer sent money for stream whose id we don't recognize: %d", streamId); responseFrames.push(new packet_1.StreamCloseFrame(streamId, packet_1.ErrorCode.StreamIdError, 'Unknown stream ID')); throw await constructFinalApplicationError(); } amountsToReceive.push({ stream, amount: streamAmount, }); const maxStreamCanReceive = this.allowableReceiveExtra.multiplyByLongCeil(stream._getAmountStreamCanReceive()); if (maxStreamCanReceive.lessThan(streamAmount)) { this.log.debug('peer sent too much for stream: %d. got: %s, max receivable: %s', streamId, streamAmount, maxStreamCanReceive); responseFrames.push(new packet_1.StreamMaxMoneyFrame(streamId, stream.receiveMax, stream.totalReceived)); throw await constructFinalApplicationError(); } if (!stream.isOpen()) { this.log.debug('peer sent money for stream that was already closed: %d', streamId); responseFrames.push(new packet_1.StreamCloseFrame(streamId, packet_1.ErrorCode.StreamStateError, 'Stream is already closed')); throw await constructFinalApplicationError(); } } this.addIncomingHold(incomingAmount); if (this.shouldFulfill && incomingAmount.greaterThan(0)) { const packetId = await cryptoHelper.generateIncomingPacketId(this.sharedSecret, requestPacket.sequence); await this.shouldFulfill(incomingAmount, packetId, this.connectionTag).catch(async (err) => { this.removeIncomingHold(incomingAmount); this.log.debug('application declined to fulfill packet %s:', requestPacket.sequence, err); throw await constructFinalApplicationError(); }); } const totalsReceived = new Map(); for (const { stream, amount } of amountsToReceive) { stream._addToIncoming(amount, prepare); totalsReceived.set(stream.id, stream.totalReceived); } if (!this.closed && this.remoteState !== RemoteState.Closed) { for (const [, stream] of this.streams) { if (!stream.isOpen() && !stream._remoteClosed) { this.log.trace('telling other side that stream %d is closed', stream.id); if (stream._errorMessage) { responseFrames.push(new packet_1.StreamCloseFrame(stream.id, packet_1.ErrorCode.ApplicationError, stream._errorMessage)); } else { responseFrames.push(new packet_1.StreamCloseFrame(stream.id, packet_1.ErrorCode.NoError, '')); } stream._remoteClosed = true; } else { this.log.trace('telling other side that stream %d can receive %s', stream.id, stream.receiveMax); responseFrames.push(new packet_1.StreamMaxMoneyFrame(stream.id, stream.receiveMax, stream.totalReceived)); responseFrames.push(new packet_1.StreamMaxDataFrame(stream.id, stream._getIncomingOffsets().maxAcceptable)); } } } if (this._receiptNonce && this._receiptSecret) { for (const [streamId, totalReceived] of totalsReceived) { responseFrames.push(new packet_1.StreamReceiptFrame(streamId, (0, receipt_1.createReceipt)({ nonce: this._receiptNonce, streamId, totalReceived, secret: this._receiptSecret, }))); } } responseFrames = responseFrames.concat(this.queuedFrames); this.queuedFrames = []; const responsePacket = new packet_1.Packet(requestPacket.sequence, packet_1.IlpPacketType.Fulfill, incomingAmount, responseFrames); this.removeIncomingHold(incomingAmount); this.addTotalReceived(incomingAmount); this.log.trace('fulfilling prepare with fulfillment: %h and response packet: %j', fulfillment, responsePacket); return { fulfillment, data: await responsePacket.serializeAndEncrypt(this._pskKey, this.enablePadding ? MAX_DATA_SIZE : undefined), }; } handleControlFrames(frames) { for (const frame of frames) { let stream; switch (frame.type) { case packet_1.FrameType.ConnectionNewAddress: { this.log.trace('peer notified us of their account: %s', frame.sourceAccount); this.queuedFrames.push(new packet_1.ConnectionMaxStreamIdFrame(this.maxStreamId), new packet_1.ConnectionAssetDetailsFrame(this.sourceAssetCode, this.sourceAssetScale)); const firstConnection = this._destinationAccount === undefined; this._destinationAccount = frame.sourceAccount; if (firstConnection) { this.closed = false; this.log.info('connected'); this.safeEmit('connect'); } break; } case packet_1.FrameType.ConnectionAssetDetails: this.log.trace('peer notified us of their asset details: code=%s, scale=%d', frame.sourceAssetCode, frame.sourceAssetScale); this._destinationAssetCode = frame.sourceAssetCode; this._destinationAssetScale = frame.sourceAssetScale; break; case packet_1.FrameType.ConnectionClose: this.sending = false; this.closed = true; this.remoteState = RemoteState.Closed; if (frame.errorCode === packet_1.ErrorCode.NoError) { this.log.info('remote closed connection'); this.end().catch((err) => { this.log.warn('close failed with error=%s', err); return this.destroy(); }); } else { this.log.error('remote connection error. code: %s, message: %s', packet_1.ErrorCode[frame.errorCode], frame.errorMessage); this.destroy(new Error(`Remote connection error. Code: ${packet_1.ErrorCode[frame.errorCode]}, message: ${frame.errorMessage}`)); } break; case packet_1.FrameType.ConnectionMaxData: { const outgoingOffsets = this.getOutgoingOffsets(); this.log.trace("remote connection max byte offset is: %s, we've sent: %d, we want to send up to: %d", frame.maxOffset, outgoingOffsets.currentOffset, outgoingOffsets.maxOffset); if (frame.maxOffset.notEquals(this.maxBufferedData)) { this.remoteMaxOffset = Math.max(frame.maxOffset.toNumber(), this.remoteMaxOffset); } else { this.remoteMaxOffset = frame.maxOffset.toNumber(); } break; } case packet_1.FrameType.ConnectionDataBlocked: this.log.trace('remote wants to send more data but we are blocking them. current max incoming offset: %d, remote max offset: %s', this.getIncomingOffsets(), frame.maxOffset); break; case packet_1.FrameType.ConnectionMaxStreamId: this.log.trace('remote set max stream id to %s', frame.maxStreamId); this.remoteMaxStreamId = frame.maxStreamId.toNumber(); break; case packet_1.FrameType.ConnectionStreamIdBlocked: this.log.trace('remote wants to open more streams but we are blocking them'); break; case packet_1.FrameType.StreamClose: this.handleStreamClose(frame); break; case packet_1.FrameType.StreamMaxMoney: this.log.trace('peer told us that stream %s can receive up to: %s and has received: %s so far', frame.streamId, frame.receiveMax, frame.totalReceived); stream = this.streams.get(frame.streamId.toNumber()); if (!stream) { break; } stream._remoteReceived = (0, long_1.maxLong)(stream._remoteReceived, frame.totalReceived); if (stream._remoteReceiveMax.notEquals(long_2.default.MAX_UNSIGNED_VALUE)) { stream._remoteReceiveMax = (0, long_1.maxLong)(stream._remoteReceiveMax, frame.receiveMax); } else { stream._remoteReceiveMax = frame.receiveMax; } if (stream._remoteReceiveMax.greaterThan(stream._remoteReceived) && stream._getAmountAvailableToSend().greaterThan(0)) { this.startSendLoop(); } break; case packet_1.FrameType.StreamMoneyBlocked: this.log.debug('peer told us that they want to send more money on stream %s but we are blocking them. they have sent: %s so far and want to send: %s', frame.streamId, frame.totalSent, frame.sendMax); break; case packet_1.FrameType.StreamData: { this.log.trace('got data for stream %s', frame.streamId); stream = this.streams.get(frame.streamId.toNumber()); if (!stream) { break; } stream._pushIncomingData(frame.data, frame.offset.toNumber()); const incomingOffsets = stream._getIncomingOffsets(); if (incomingOffsets.max > incomingOffsets.maxAcceptable) { this.destroy(new ConnectionError(`Exceeded flow control limits. Stream ${stream.id} can accept up to offset: ${incomingOffsets.maxAcceptable} but got bytes up to offset: ${incomingOffsets.max}`, packet_1.ErrorCode.FlowControlError)); } break; } case packet_1.FrameType.StreamMaxData: { stream = this.streams.get(frame.streamId.toNumber()); if (!stream) { break; } const oldOffset = stream._remoteMaxOffset; const newOffset = frame.maxOffset.toNumber(); if (newOffset > oldOffset) { this.log.trace("peer told us that stream %s can receive up to byte offset: %s (we've sent up to offset: %d)", frame.streamId, frame.maxOffset, stream._getOutgoingOffsets().current); stream._remoteMaxOffset = newOffset; this.startSendLoop(); } else { this.log.trace('peer told us that stream %s can receive up to byte offset: %d; ignoring new offset: %d', frame.streamId, oldOffset, newOffset); } break; } case packet_1.FrameType.StreamDataBlocked: stream = this.streams.get(frame.streamId.toNumber()); if (!stream) { break; } this.log.debug('peer told us that stream %s is blocked. they want to send up to offset: %s, but we are only allowing up to: %d', frame.streamId, frame.maxOffset, stream._getIncomingOffsets().maxAcceptable); break; default: continue; } } } handleNewStream(streamId) { if (this.streams.has(streamId) || this.closedStreams.has(streamId)) { return; } if (this.isServer && streamId % 2 === 0) { this.log.error('got invalid stream ID %d from peer (should be odd)', streamId); this.queuedFrames.push(new packet_1.ConnectionCloseFrame(packet_1.ErrorCode.ProtocolViolation, `Invalid Stream ID: ${streamId}. Client-initiated streams must have odd-numbered IDs`)); const err = new Error(`Invalid Stream ID: ${streamId}. Client-initiated streams must have odd-numbered IDs`); this.safeEmit('error', err); throw err; } else if (!this.isServer && streamId % 2 === 1) { this.log.error('got invalid stream ID %d from peer (should be even)', streamId); this.queuedFrames.push(new packet_1.ConnectionCloseFrame(packet_1.ErrorCode.ProtocolViolation, `Invalid Stream ID: ${streamId}. Server-initiated streams must have even-numbered IDs`)); const err = new Error(`Invalid Stream ID: ${streamId}. Server-initiated streams must have even-numbered IDs`); this.safeEmit('error', err); throw err; } if (streamId > this.maxStreamId) { this.log.debug('peer opened too many streams. got stream: %d, but max stream id is: %d. closing connection', streamId, this.maxStreamId); this.queuedFrames.push(new packet_1.ConnectionCloseFrame(packet_1.ErrorCode.StreamIdError, `Maximum number of open streams exceeded. Got stream: ${streamId}, current max stream ID: ${this.maxStreamId}`)); const err = new Error(`Maximum number of open streams exceeded. Got stream: ${streamId}, current max stream ID: ${this.maxStreamId}`); this.safeEmit('error', err); throw err; } if (this.maxStreamId * 0.75 < streamId) { this.log.trace('informing peer that our max stream id is: %d', this.maxStreamId); this.queuedFrames.push(new packet_1.ConnectionMaxStreamIdFrame(this.maxStreamId)); } this.log.info('got new stream: %d', streamId); const stream = new stream_1.DataAndMoneyStream({ id: streamId, isServer: this.isServer, connectionId: this.connectionId, }); this.streams.set(streamId, stream); stream.on('_maybe_start_send_loop', () => this.startSendLoop()); stream.once('close', () => this.removeStreamRecord(stream)); this.safeEmit('stream', stream); } handleStreamClose(frame) { const streamId = frame.streamId.toNumber(); const stream = this.streams.get(streamId); if (!stream) { this.log.error("remote error on stream %d, but we don't have a record of that stream", streamId); return; } if (!stream.isOpen() || stream._remoteSentEnd) { return; } this.log.error('peer closed stream %d with error code: %s and message: %s', stream.id, packet_1.ErrorCode[frame.errorCode], frame.errorMessage); stream._sentEnd = true; let err; if (frame.errorMessage) { err = new Error(frame.errorMessage); err.name = packet_1.ErrorCode[frame.errorCode]; } stream._remoteEnded(err); this.maxStreamId += 2; this.log.trace('raising maxStreamId to %d', this.maxStreamId); this.queuedFrames.push(new packet_1.ConnectionMaxStreamIdFrame(this.maxStreamId)); this.startSendLoop(); } async startSendLoop() { if (this.looping) { this.sending = true; return; } if (this.remoteState === RemoteState.Closed) { this.log.debug('remote connection is already closed, not starting another loop'); this.safeEmit('_send_loop_finished'); return; } if (!this._destinationAccount) { this.log.debug("not sending because we do not know the client's address"); this.safeEmit('_send_loop_finished'); return; } this.looping = true; this.sending = true; this.log.debug('starting send loop'); try { while (this.sending) { if (!this.connected) { await this.setupExchangeRate(); this.connected = true; } else { await this.loadAndSendPacket(); } } this.looping = false; } catch (err) { this.looping = false; return this.destroy(err instanceof Error ? err : undefined); } this.log.debug('finished sending'); this.safeEmit('_send_loop_finished'); for (const [_, stream] of this.streams) { stream.emit('_send_loop_finished'); } } async loadAndSendPacket() { await new Promise((resolve) => setTimeout(resolve)); this.log.trace('loadAndSendPacket'); let amountToSend = long_2.default.UZERO; const requestPacket = new packet_1.Packet(this.getNextPacketSequence(), packet_1.IlpPacketType.Prepare, undefined, this.queuedFrames); this.queuedFrames = []; this.maybePushAccountFrames(requestPacket); for (const [_, stream] of this.streams) { if (stream.isOpen()) { requestPacket.frames.push(new packet_1.StreamMaxMoneyFrame(stream.id, stream.receiveMax, stream.totalReceived)); requestPacket.frames.push(new packet_1.StreamMaxDataFrame(stream.id, stream._getIncomingOffsets().maxAcceptable)); } } if (this.closed && this.remoteState === RemoteState.Connected) { this.log.trace('sending connection close frame'); requestPacket.frames.push(new packet_1.ConnectionCloseFrame(packet_1.ErrorCode.NoError, '')); this.remoteState = RemoteState.Closed; } if (!this.exchangeRate) { throw new Error('Tried to send without an exchange rate established'); } let maxAmountFromNextStream = this.congestion.testMaximumPacketAmount; if (this.exchangeRate.greaterThanOne()) { maxAmountFromNextStream = (0, long_1.minLong)(maxAmountFromNextStream, this.exchangeRate.reciprocal().multiplyByLong(long_2.default.MAX_UNSIGNED_VALUE)); } const streamsSentFrom = []; for (const [_, stream] of this.streams) { if (stream._sentEnd) { continue; } let amountToSendFromStream = (0, long_1.minLong)(stream._getAmountAvailableToSend(), maxAmountFromNextStream); const maxDestinationAmount = (0, long_1.checkedSubtract)(stream._remoteReceiveMax, stream._remoteReceived).difference; const maxSourceAmount = this.exchangeRate .reciprocal() .multiplyByLongCeil(maxDestinationAmount); if (maxSourceAmount.lessThan(amountToSendFromStream)) { this.log.trace("stream %d could send %s but that would be more than the receiver says they can receive, so we'll send %s instead", stream.id, amountToSendFromStream, maxSourceAmount); amountToSendFromStream = maxSourceAmount; } this.log.trace('amount to send from stream %d: %s, exchange rate: %s, remote total received: %s, remote receive max: %s', stream.id, amountToSendFromStream, this.exchangeRate, stream._remoteReceived, stream._remoteReceiveMax); if (amountToSendFromStream.greaterThan(0)) { stream._holdOutgoing(requestPacket.sequence.toString(), amountToSendFromStream); requestPacket.frames.push(new packet_1.StreamMoneyFrame(stream.id, amountToSendFromStream)); amountToSend = amountToSend.add(amountToSendFromStream); maxAmountFromNextStream = maxAmountFromNextStream.subtract(amountToSendFromStream); streamsSentFrom.push(stream); } const amountLeftStreamWantsToSend = long_2.default.fromString(stream.sendMax, true) .subtract(stream.totalSent) .subtract(amountToSendFromStream); if (this.exchangeRate .multiplyByLong(amountLeftStreamWantsToSend) .greaterThan((0, long_1.checkedSubtract)(stream._remoteReceiveMax, stream._remoteReceived).difference)) { requestPacket.frames.push(new packet_1.StreamMoneyBlockedFrame(stream.id, stream.sendMax, stream.totalSent)); } if (maxAmountFromNextStream.equals(0)) { break; } } let bytesLeftInPacket = MAX_DATA_SIZE - requestPacket.byteLength(); const maxBytesRemoteConnectionCanReceive = this.remoteMaxOffset - this.getOutgoingOffsets().currentOffset; if (bytesLeftInPacket > maxBytesRemoteConnectionCanReceive) { const outgoingMaxOffset = this.getOutgoingOffsets().maxOffset; this.log.debug('peer is blocking us from sending more data. they will only accept up to offset: %d, but we want to send up to: %d', this.remoteMaxOffset, outgoingMaxOffset); requestPacket.frames.push(new packet_1.ConnectionDataBlockedFrame(outgoingMaxOffset)); bytesLeftInPacket = maxBytesRemoteConnectionCanReceive; } for (const [_, stream] of this.streams) { if (bytesLeftInPacket - 20 <= 0) { break; } const { data, offset } = stream._getAvailableDataToSend(bytesLeftInPacket - 20); if (data && data.length > 0) { const streamDataFrame = new packet_1.StreamDataFrame(stream.id, offset, data); this.log.trace('sending %d bytes from stream %d', data.length, stream.id); bytesLeftInPacket -= streamDataFrame.byteLength(); requestPacket.frames.push(streamDataFrame); } const maxOutgoingOffset = stream._isDataBlocked(); if (maxOutgoingOffset) { this.log.trace('telling remote that stream %d is blocked and has more data to send', stream.id); requestPacket.frames.push(new packet_1.StreamDataBlockedFrame(stream.id, maxOutgoingOffset)); } } if (amountToSend.equals(0)) { if (requestPacket.frames.length === 0) { this.sending = false; return; } else { if (!requestPacket.frames.find((frame) => frame.type === packet_1.FrameType.StreamClose || frame.type === packet_1.FrameType.StreamData || frame.type === packet_1.FrameType.StreamMoney)) { this.sending = false; } } } const minimumDestinationAmount = this.slippage .complement() .multiplyByLong(this.exchangeRate.multiplyByLong(amountToSend)); if (minimumDestinationAmount.greaterThan(0)) { requestPacket.prepareAmount = minimumDestinationAmount; } const responsePacket = await this.sendPacket(requestPacket, amountToSend, false); if (responsePacket) { if (this.remoteState === RemoteState.Init) { this.remoteState = RemoteState.Connected; } this.remoteKnowsOurAccount = true; this.handleControlFrames(responsePacket.frames); if (amountToSend.greaterThan(0)) { this._lastPacketExchangeRate = new rational_1.default(responsePacket.prepareAmount, amountToSend, true); } if (responsePacket.ilpPacketType === packet_1.IlpPacketType.Fulfill) { for (const frame of responsePacket.frames) { if (frame.type === packet_1.FrameType.StreamReceipt) { const stream = this.streams.get(frame.streamId.toNumber()); if (stream) { stream._setReceipt(frame.receipt); } else { this.log.debug('received receipt for unknown stream %d: %h', frame.streamId, frame.receipt); } } } for (const stream of streamsSentFrom) { stream._executeHold(requestPacket.sequence.toString()); } this.addTotalDelivered(responsePacket.prepareAmount); this.addTotalSent(amountToSend); this.congestion.onFulfill(amountToSend); this.retryDelay = RETRY_DELAY_START; } } } async sendTestPacketVolley(testPacketAmounts) { const results = await Promise.all(testPacketAmounts.map(async (amount) => { try { return this.sendTestPacket(amount); } catch (err) { this.log.error('Error sending test packet for amount %d: %s', amount, err); return null; } })); const maxPacketAmounts = testPacketAmounts.map((sourceAmount, index) => { if (results[index] && results[index].code === 'F08') { try { const reader = oer_utils_1.Reader.from(results[index].data); const receivedAmount = reader.readUInt64Long(); const maximumAmount = reader.readUInt64Long(); const maximumPacketAmount = (0, long_1.multiplyDivideFloor)(sourceAmount, maximumAmount, receivedAmount); this.log.debug('sending test packet of %d resulted in F08 error that told us maximum packet amount is %s', testPacketAmounts[index], maximumPacketAmount); return maximumPacketAmount; } catch (err) { return long_2.default.MAX_UNSIGNED_VALUE; } } return long_2.default.MAX_UNSIGNED_VALUE; }); return results.reduce(({ maxDigits, exchangeRate, maxPacketAmounts, packetErrors }, result, index) => { const sourceAmount = testPacketAmounts[index]; if (result && result.code) { packetErrors.push({ sourceAmount, code: result.code, }); } if (result && result.prepareAmount) { const prepareAmount = result.prepareAmount; const exchangeRate = new rational_1.default(prepareAmount, sourceAmount, true); this.log.debug('sending test packet of %d delivered %s (exchange rate: %s)', sourceAmount, prepareAmount, exchangeRate); if ((0, long_1.countDigits)(prepareAmount) >= maxDigits) { return { maxDigits: (0, long_1.countDigits)(prepareAmount), exchangeRate, maxPacketAmounts, packetErrors, }; } } return { maxDigits, exchangeRate, maxPacketAmounts, packetErrors }; }, { maxDigits: 0, exchangeRate: rational_1.default.UZERO, maxPacketAmounts, packetErrors: [] }); } async setupExchangeRate() { if (!this.exchangeRate) { this.log.trace('determining exchange rate'); await this.determineExchangeRate(); } process.nextTick(() => { this.safeEmit('connect'); this.log.trace('connected'); }); } async determineExchangeRate() { this.log.trace('determineExchangeRate'); if (!this._destinationAccount) { throw new Error('Cannot determine exchange rate. Destination account is unknown'); } let retryDelay = RETRY_DELAY_START; let testPacketAmounts = [1, 1e3, 1e6, 1e9, 1e12].map((num) => long_2.default.fromNumber(num, true)); let attempts = 0; while (!this.exchangeRate && testPacketAmounts.length > 0 && attempts < TEST_PACKET_MAX_ATTEMPTS) { attempts++; const { maxDigits, exchangeRate, maxPacketAmounts, packetErrors } = await this.sendTestPacketVolley(testPacketAmounts); this.congestion.setMaximumAmounts((0, long_1.minLongs)(maxPacketAmounts.concat(this.congestion.maximumPacketAmount))); if (this.congestion.maximumPacketAmount.equals(0)) { this.log.error('cannot send anything through this path. the maximum packet amount is 0'); throw new Error('Cannot send. Path has a Maximum Packet Amount of 0'); } if (maxDigits >= this.minExchangeRatePrecision) { this.log.debug('determined exchange rate to be %s with %d digits precision', exchangeRate, maxDigits); this.exchangeRate = exchangeRate; return; } testPacketAmounts = maxPacketAmounts .filter((amount) => !amount.equals(long_2.default.MAX_UNSIGNED_VALUE)) .reduce((acc, curr) => [...new Set([...acc, curr.toString()])], []) .map((str) => long_2.default.fromString(str, true)); if (packetErrors.some((error) => error.code[0] === 'T')) { const smallestPacketAmount = packetErrors.reduce((min, error) => { return (0, long_1.minLong)(min, error.sourceAmount); }, long_2.default.MAX_UNSIGNED_VALUE); const reducedPacketAmount = smallestPacketAmount.subtract(smallestPacketAmount.divide(3)); this.log.debug('got Txx error(s), waiting %dms and reducing packet amount to %s before sending another test packet', retryDelay, reducedPacketAmount); testPacketAmounts = [...testPacketAmounts, reducedPacketAmount]; await this.rateRetryTimer.wait(retryDelay).catch((_err) => { this.log.debug('connection terminated before rate could be determined; delay=%d', retryDelay); throw new Error('Connection terminated before rate could be determined.'); }); retryDelay *= RETRY_DELAY_INCREASE_FACTOR; } this.log.debug('retry with packet amounts %j', testPacketAmounts); } throw new Error(`Unable to establish connection, no packets meeting the minimum exchange precision of ${this.minExchangeRatePrecision} digits made it through the path.`); } stopTimers() { if (this.rateRetryTimer) this.rateRetryTimer.stop(); if (this.idleTimer) clearTimeout(this.idleTimer); this.done = true; } async sendTestPacket(amount, timeout = DEFAULT_PACKET_TIMEOUT) { const requestPacket = new packet_1.Packet(this.getNextPacketSequence(), packet_1.IlpPacketType.Prepare); this.log.trace('sending test packet %s for amount: %s. timeout: %d', requestPacket.sequence, amount, timeout); this.maybePushAccountFrames(requestPacket); if (!this._destinationAccount) { this.log.error('tried to send test packet without having a destination account'); throw new Error('Tried to send test packet without having a destination account'); } const prepare = { destination: this._destinationAccount, amount: amount.toString(), data: await requestPacket.serializeAndEncrypt(this._pskKey), executionCondition: cryptoHelper.generateRandomCondition(), expiresAt: this.getExpiry(this._destinationAccount), }; const responseData = await new Promise((resolve, reject) => { const timer = setTimeout(() => { this.log.error('test packet %s timed out before we got a response', requestPacket.sequence); resolve(null); }, timeout); this.plugin .sendData(IlpPacket.serializeIlpPrepare(prepare)) .then((result) => { clearTimeout(timer); resolve(result); }) .catch(reject); }); if (!responseData) { return null; } this.bumpIdle(); const ilpReject = IlpPacket.deserializeIlpReject(responseData); let responsePacket; if (ilpReject.code === 'F99' && ilpReject.data.length > 0) { responsePacket = await packet_1.Packet.decryptAndDeserialize(this._pskKey, ilpReject.data); if (!responsePacket.sequence.equals(requestPacket.sequence)) { this.log.error('response packet sequence does not m