ilp-protocol-stream
Version:
Interledger Transport Protocol for sending multiple streams of money and data over ILP.
958 lines • 64.8 kB
JavaScript
"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