This repository has been archived by the owner on May 5, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
connection.ts
1257 lines (1125 loc) · 50.5 KB
/
connection.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { EventEmitter } from 'events'
import * as Debug from 'debug'
import { DataAndMoneyStream } from './stream'
import * as IlpPacket from 'ilp-packet'
import * as cryptoHelper from './crypto'
import {
Packet,
Frame,
StreamMoneyFrame,
StreamCloseFrame,
StreamDataFrame,
StreamMaxMoneyFrame,
FrameType,
IlpPacketType,
ConnectionNewAddressFrame,
ErrorCode,
ConnectionCloseFrame,
ConnectionStreamIdBlockedFrame,
ConnectionMaxStreamIdFrame,
StreamMaxDataFrame,
StreamDataBlockedFrame,
ConnectionMaxDataFrame,
ConnectionDataBlockedFrame,
StreamMoneyBlockedFrame
} from './packet'
import { Reader } from 'oer-utils'
import { Plugin } from './util/plugin-interface'
import BigNumber from 'bignumber.js'
require('source-map-support').install()
const TEST_PACKET_AMOUNT = new BigNumber(1000)
const RETRY_DELAY_START = 100
const MAX_DATA_SIZE = 32767
const DEFAULT_MAX_REMOTE_STREAMS = 10
export interface ConnectionOpts {
/** Ledger plugin (V2) */
plugin: Plugin,
/** ILP Address of the remote entity */
destinationAccount?: string,
/** ILP Address of the plugin */
sourceAccount?: string,
/** Specifies how much worse than the initial test packet that the exchange rate is allowed to get before packets are rejected */
slippage?: BigNumber.Value,
/** Pad packets to the maximum size (data field of 32767 bytes). False by default */
enablePadding?: boolean,
/** User-specified connection identifier that was passed into [`generateAddressAndSecret`]{@link Server#generateAddressAndSecret} */
connectionTag?: string,
/** Maximum number of streams the other entity can have open at once. Defaults to 10 */
maxRemoteStreams?: number,
/** Number of bytes each connection can have in the buffer. Defaults to 65534 */
connectionBufferSize?: number
}
export interface FullConnectionOpts extends ConnectionOpts {
sourceAccount: string,
isServer: boolean,
/** Shared secret generated by the server with [`generateAddressAndSecret`]{@link Server#generateAddressAndSecret} */
sharedSecret: Buffer
}
export class ConnectionError extends Error {
streamErrorCode: ErrorCode
constructor (message: string, streamErrorCode?: ErrorCode) {
super(message)
this.streamErrorCode = streamErrorCode || ErrorCode.InternalError
}
}
/**
* Class representing the connection between a [`Client`]{@link createConnection} and a [`Server`]{@link Server}.
* A single connection can be used to send or receive on [Streams]{@link DataAndMoneyStream}.
*
* Streams are created using the [`createStream`]{@link createStream} method.
* The `'stream'` event will be emitted whenever a new incoming stream is opened by the other party.
*/
export class Connection extends EventEmitter {
/** Application identifier for a certain connection */
readonly connectionTag?: string
protected plugin: Plugin
protected sourceAccount: string
protected destinationAccount?: string
protected sharedSecret: Buffer
protected isServer: boolean
protected slippage: BigNumber
protected allowableReceiveExtra: BigNumber
protected enablePadding: boolean
protected maxBufferedData: number
protected nextPacketSequence: number
protected streams: Map<number, DataAndMoneyStream>
protected closedStreams: { [id: number]: boolean }
protected nextStreamId: number
protected maxStreamId: number
protected debug: Debug.IDebugger
protected sending: boolean
/** Used to probe for the Maximum Packet Amount if the connectors don't tell us directly */
protected testMaximumPacketAmount: BigNumber
/** The path's Maximum Packet Amount, discovered through F08 errors */
protected maximumPacketAmount: BigNumber
protected closed: boolean
protected exchangeRate?: BigNumber
protected retryDelay: number
protected queuedFrames: Frame[]
protected remoteClosed: boolean
protected remoteMaxStreamId: number
protected remoteKnowsOurAccount: boolean
// TODO use bignumbers for byte offsets
protected remoteMaxOffset: number
protected _totalReceived: BigNumber
protected _totalSent: BigNumber
protected _totalDelivered: BigNumber
protected _lastPacketExchangeRate: BigNumber
constructor (opts: FullConnectionOpts) {
super()
this.plugin = opts.plugin
this.sourceAccount = opts.sourceAccount
this.destinationAccount = opts.destinationAccount
this.sharedSecret = opts.sharedSecret
this.isServer = opts.isServer
this.slippage = new BigNumber(opts.slippage || 0)
this.allowableReceiveExtra = new BigNumber(1.01)
this.enablePadding = !!opts.enablePadding
this.connectionTag = opts.connectionTag
this.maxStreamId = 2 * (opts.maxRemoteStreams || DEFAULT_MAX_REMOTE_STREAMS)
this.maxBufferedData = opts.connectionBufferSize || MAX_DATA_SIZE * 2
this.nextPacketSequence = 1
// TODO should streams be a Map or just an object?
this.streams = new Map()
this.closedStreams = {}
this.nextStreamId = (this.isServer ? 2 : 1)
this.debug = Debug(`ilp-protocol-stream:${this.isServer ? 'Server' : 'Client'}:Connection`)
this.sending = false
this.closed = true
this.queuedFrames = []
this.maximumPacketAmount = new BigNumber(Infinity)
this.testMaximumPacketAmount = new BigNumber(Infinity)
this.retryDelay = RETRY_DELAY_START
this.remoteClosed = false
this.remoteKnowsOurAccount = this.isServer
this.remoteMaxStreamId = DEFAULT_MAX_REMOTE_STREAMS
this.remoteMaxOffset = this.maxBufferedData
this._totalReceived = new BigNumber(0)
this._totalSent = new BigNumber(0)
this._totalDelivered = new BigNumber(0)
this._lastPacketExchangeRate = new BigNumber(0)
}
/**
* New incoming stream event
* @event stream
* @type {DataAndMoneyStream}
*/
/**
* Start sending or receiving.
* @fires stream
*/
async connect (): Promise<void> {
if (!this.closed) {
return Promise.resolve()
}
/* tslint:disable-next-line:no-floating-promises */
this.startSendLoop()
await new Promise((resolve, reject) => {
const connectHandler = () => {
cleanup()
resolve()
}
const closeHandler = () => {
cleanup()
reject(new Error('Connection was closed before it was connected'))
}
const errorHandler = (error?: Error) => {
cleanup()
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 self = this
function cleanup () {
self.removeListener('connect', connectHandler)
self.removeListener('error', errorHandler)
self.removeListener('close', closeHandler)
self.removeListener('end', closeHandler)
}
})
this.closed = false
}
/**
* Close the connection when all streams have finished sending their money and data
*/
// TODO should this be sync or async?
async end (): Promise<void> {
this.debug('closing connection')
// Create Promises on each stream that resolve on the 'end' event so
// we can wait for them all to be completed before closing the connection
let streamEndPromises: Promise<any>[] = []
for (let [_, stream] of this.streams) {
if (stream.isOpen()) {
streamEndPromises.push(new Promise((resolve, reject) => {
stream.on('end', resolve)
}))
stream.end()
}
}
await new Promise((resolve, reject) => {
this.once('_send_loop_finished', resolve)
this.once('error', reject)
/* tslint:disable-next-line:no-floating-promises */
this.startSendLoop()
})
// Wait for the send loop to finish & all the streams to end
// before marking the connection as closed so the streams
// can finish sending data or money.
await Promise.all(streamEndPromises)
this.closed = true
await this.sendConnectionClose()
this.safeEmit('end')
this.safeEmit('close')
}
/**
* Immediately close the connection and all streams
*/
// TODO should this be sync or async?
async destroy (err?: Error): Promise<void> {
this.debug('destroying connection with error:', err)
if (err) {
this.safeEmit('error', err)
}
// Create Promises on each stream that resolve on the 'close' event so
// we can wait for them all to be completed before closing the connection
let streamClosePromises: Promise<any>[] = []
for (let [_, stream] of this.streams) {
streamClosePromises.push(new Promise((resolve, reject) => {
stream.on('close', resolve)
}))
// TODO should we pass the error to each stream?
stream.destroy()
}
await this.sendConnectionClose(err)
// wait for all the streams to be closed before emitting the connection 'close'
await Promise.all(streamClosePromises)
this.safeEmit('close')
}
/**
* Returns a new bidirectional [`DataAndMoneyStream`]{@link DataAndMoneyStream}
*/
createStream (): DataAndMoneyStream {
// Make sure we don't open more streams than the remote will allow
if (this.remoteMaxStreamId < this.nextStreamId) {
this.debug(`cannot creat another stream. nextStreamId: ${this.nextStreamId}, remote maxStreamId: ${this.remoteMaxStreamId}`)
this.queuedFrames.push(new ConnectionStreamIdBlockedFrame(this.nextStreamId))
throw new Error(`Creating another stream would exceed the remote connection's maximum number of open streams`)
}
// TODO should this inform the other side?
const stream = new DataAndMoneyStream({
id: this.nextStreamId,
isServer: this.isServer
})
this.streams.set(this.nextStreamId, stream)
this.debug(`created stream: ${this.nextStreamId}`)
this.nextStreamId += 2
stream.on('_maybe_start_send_loop', this.startSendLoop.bind(this))
stream.once('close', () => this.removeStreamRecord(stream))
return stream
}
/**
* Connections minimum exchange rate with slippage included, if not set '0' is returned.
*/
get minimumAcceptableExchangeRate (): string {
if (this.exchangeRate) {
const minimumExchangeWithSlippage = this.exchangeRate
.times(new BigNumber(1).minus(this.slippage))
return minimumExchangeWithSlippage.toString()
}
return '0'
}
/**
* Calculates the last exchange rate based on last packet successfully sent.
*/
get lastPacketExchangeRate (): string {
return this._lastPacketExchangeRate.toString()
}
/**
* Total delivered so far, denominated in the connection plugin's units.
*/
get totalDelivered (): string {
return this._totalDelivered.toString()
}
/**
* Total sent so far, denominated in the connection plugin's units.
*/
get totalSent (): string {
return this._totalSent.toString()
}
/**
* Total received so far by the local side, denominated in the connection plugin's units.
*/
get totalReceived (): string {
return this._totalReceived.toString()
}
/**
* (Internal) Handle incoming ILP Prepare packets.
* This will automatically fulfill all valid and expected Prepare packets.
* It passes the incoming money and/or data to the relevant streams.
* @private
*/
async handlePrepare (prepare: IlpPacket.IlpPrepare): Promise<IlpPacket.IlpFulfill> {
// Parse packet
let requestPacket: Packet
try {
requestPacket = Packet.decryptAndDeserialize(this.sharedSecret, prepare.data)
} catch (err) {
this.debug(`error parsing frames:`, err)
throw new IlpPacket.Errors.UnexpectedPaymentError('')
}
this.debug('handling packet:', JSON.stringify(requestPacket))
if (requestPacket.ilpPacketType.valueOf() !== IlpPacket.Type.TYPE_ILP_PREPARE) {
this.debug(`prepare packet contains a frame that says it should be something other than a prepare: ${requestPacket.ilpPacketType}`)
throw new IlpPacket.Errors.UnexpectedPaymentError('')
}
let responseFrames: Frame[] = []
// Tell peer how much data connection can receive
responseFrames.push(new ConnectionMaxDataFrame(this.getIncomingOffsets().maxAcceptable))
const throwFinalApplicationError = () => {
responseFrames = responseFrames.concat(this.queuedFrames)
this.queuedFrames = []
const responsePacket = new Packet(requestPacket.sequence, IlpPacketType.Reject, prepare.amount, responseFrames)
this.debug(`rejecting packet ${requestPacket.sequence}: ${JSON.stringify(responsePacket)}`)
throw new IlpPacket.Errors.FinalApplicationError('', responsePacket.serializeAndEncrypt(this.sharedSecret, (this.enablePadding ? MAX_DATA_SIZE : undefined)))
}
// Handle new streams
for (let frame of requestPacket.frames) {
if (frame.type === FrameType.StreamMoney
|| frame.type === FrameType.StreamData
// TODO should frames that set the max values open the stream?
|| frame.type === FrameType.StreamMaxMoney
|| frame.type === FrameType.StreamMaxData) {
const streamId = frame.streamId.toNumber()
// Check if the stream was already closed
if (this.closedStreams[streamId]) {
this.debug(`got packet with frame for stream ${streamId}, which was already closed`)
// Don't bother sending an error frame back unless they've actually sent money or data
if (frame.type !== FrameType.StreamMoney && frame.type !== FrameType.StreamData) {
continue
}
// Respond with a StreamClose frame (unless there is already one queued)
const framesToSend = responseFrames.concat(this.queuedFrames)
const includesStreamClose = framesToSend.find((frame) => frame.type === FrameType.StreamClose && frame.streamId.isEqualTo(streamId))
if (!includesStreamClose) {
responseFrames.push(new StreamCloseFrame(streamId, ErrorCode.StreamStateError, 'Stream is already closed'))
}
throwFinalApplicationError()
}
try {
// Note this will throw if the stream was already closed
this.handleNewStream(frame.streamId.toNumber())
} catch (err) {
this.debug(`error handling new stream ${frame.streamId}:`, err && err.message)
throwFinalApplicationError()
}
}
}
// TODO don't throw errors in expected cases -- they are slower than just returning a value
try {
this.handleControlFrames(requestPacket.frames)
} catch (err) {
this.debug('error handling frames:', err && err.message)
throwFinalApplicationError()
}
// TODO keep a running total of the offsets so we don't need to recalculate each time
const incomingOffsets = this.getIncomingOffsets()
if (incomingOffsets.max > incomingOffsets.maxAcceptable) {
/* tslint:disable-next-line:no-floating-promises */
this.destroy(new ConnectionError(`Exceeded flow control limits. Max connection byte offset: ${incomingOffsets.maxAcceptable}, received: ${incomingOffsets.max}`, ErrorCode.FlowControlError))
throwFinalApplicationError()
}
if (requestPacket.prepareAmount.isGreaterThan(prepare.amount)) {
this.debug(`received less than minimum destination amount. actual: ${prepare.amount}, expected: ${requestPacket.prepareAmount}`)
throwFinalApplicationError()
}
// Ensure we can generate correct fulfillment
const fulfillment = cryptoHelper.generateFulfillment(this.sharedSecret, prepare.data)
const generatedCondition = cryptoHelper.hash(fulfillment)
if (!generatedCondition.equals(prepare.executionCondition)) {
this.debug(`got unfulfillable prepare for amount: ${prepare.amount}. generated condition: ${generatedCondition.toString('hex')}, prepare condition: ${prepare.executionCondition.toString('hex')}`)
throwFinalApplicationError()
}
// Determine amount to receive on each frame
const amountsToReceive: { stream: DataAndMoneyStream, amount: BigNumber }[] = []
const totalMoneyShares = requestPacket.frames.reduce((sum: BigNumber, frame: Frame) => {
if (frame instanceof StreamMoneyFrame) {
return sum.plus(frame.shares)
}
return sum
}, new BigNumber(0))
for (let frame of requestPacket.frames) {
if (!(frame instanceof StreamMoneyFrame)) {
continue
}
const streamId = frame.streamId.toNumber()
const streamAmount = new BigNumber(prepare.amount)
.times(frame.shares)
.dividedBy(totalMoneyShares)
// TODO make sure we don't lose any because of rounding issues
.integerValue(BigNumber.ROUND_FLOOR)
const stream = this.streams.get(streamId)!
amountsToReceive.push({
stream,
amount: streamAmount
})
// Ensure that this amount isn't more than the stream can receive
const maxStreamCanReceive = stream._getAmountStreamCanReceive()
.times(this.allowableReceiveExtra)
.integerValue(BigNumber.ROUND_CEIL)
if (maxStreamCanReceive.isLessThan(streamAmount)) {
// TODO should this be distributed to other streams if it can be?
this.debug(`peer sent too much for stream: ${streamId}. got: ${streamAmount}, max receivable: ${maxStreamCanReceive}`)
// Tell peer how much the streams they sent for can receive
responseFrames.push(new StreamMaxMoneyFrame(streamId, stream.receiveMax, stream.totalReceived))
// TODO include error frame
throwFinalApplicationError()
}
// Reject the packet if any of the streams is already closed
if (!stream.isOpen()) {
this.debug(`peer sent money for stream that was already closed: ${streamId}`)
responseFrames.push(new StreamCloseFrame(streamId, ErrorCode.StreamStateError, 'Stream is already closed'))
throwFinalApplicationError()
}
}
// Add incoming amounts to each stream
for (let { stream, amount } of amountsToReceive) {
stream._addToIncoming(amount)
}
// Tell peer about closed streams and how much each stream can receive
if (!this.closed && !this.remoteClosed) {
for (let [_, stream] of this.streams) {
const streamIsClosed = !stream.isOpen() && stream._getAmountAvailableToSend().isEqualTo(0)
if (streamIsClosed && !stream._remoteClosed) {
this.debug(`telling other side that stream ${stream.id} is closed`)
if (stream._errorMessage) {
responseFrames.push(new StreamCloseFrame(stream.id, ErrorCode.ApplicationError, stream._errorMessage))
} else {
responseFrames.push(new StreamCloseFrame(stream.id, ErrorCode.NoError, ''))
}
// TODO confirm that they get this
stream._remoteClosed = true
} else {
this.debug(`telling other side that stream ${stream.id} can receive ${stream.receiveMax}`)
responseFrames.push(new StreamMaxMoneyFrame(stream.id, stream.receiveMax, stream.totalReceived))
// TODO only send these frames when we need to
responseFrames.push(new StreamMaxDataFrame(stream.id, stream._getIncomingOffsets().maxAcceptable))
}
}
}
// TODO make sure the queued frames aren't too big
responseFrames = responseFrames.concat(this.queuedFrames)
this.queuedFrames = []
// Return fulfillment and response packet
const responsePacket = new Packet(requestPacket.sequence, IlpPacketType.Fulfill, prepare.amount, responseFrames)
this._totalReceived = this._totalReceived.plus(prepare.amount)
this.debug(`fulfilling prepare with fulfillment: ${fulfillment.toString('hex')} and response packet: ${JSON.stringify(responsePacket)}`)
return {
fulfillment,
data: responsePacket.serializeAndEncrypt(this.sharedSecret, (this.enablePadding ? MAX_DATA_SIZE : undefined))
}
}
/**
* Parse the frames from the incoming packet and apply all effects
* except for passing money to the streams
*/
protected handleControlFrames (frames: Frame[]): void {
for (let frame of frames) {
let stream
switch (frame.type) {
case FrameType.ConnectionNewAddress:
this.debug(`peer notified us of their account: ${frame.sourceAccount}`)
const firstConnection = this.destinationAccount === undefined
this.destinationAccount = frame.sourceAccount
if (firstConnection) {
this.handleConnect()
}
// TODO reset the exchange rate and send a test packet to make sure they haven't spoofed the address
break
case FrameType.ConnectionClose:
// TODO end the connection in some other way
this.sending = false
this.closed = true
this.remoteClosed = true
if (frame.errorCode === ErrorCode.NoError) {
this.debug(`remote closed connection`)
/* tslint:disable-next-line:no-floating-promises */
this.end()
} else {
this.debug(`remote connection error. code: ${ErrorCode[frame.errorCode]}, message: ${frame.errorMessage}`)
/* tslint:disable-next-line:no-floating-promises */
this.destroy(new Error(`Remote connection error. Code: ${ErrorCode[frame.errorCode]}, message: ${frame.errorMessage}`))
}
break
case FrameType.ConnectionMaxData:
const outgoingOffsets = this.getOutgoingOffsets()
this.debug(`remote connection max byte offset is: ${frame.maxOffset}, we've sent: ${outgoingOffsets.currentOffset}, we want to send up to: ${outgoingOffsets.maxOffset}`)
if (frame.maxOffset.isGreaterThan(MAX_DATA_SIZE * 2)) {
this.remoteMaxOffset = Math.max(frame.maxOffset.toNumber(), this.remoteMaxOffset)
} else {
// We assumed their size was 64kb but it turned out to be less
this.remoteMaxOffset = frame.maxOffset.toNumber()
}
break
case FrameType.ConnectionDataBlocked:
this.debug(`remote wants to send more data but we are blocking them. current max incoming offset: ${this.getIncomingOffsets()}, remote max offset: ${frame.maxOffset}`)
break
case FrameType.ConnectionMaxStreamId:
// TODO make sure the number isn't lowered
this.debug(`remote set max stream id to ${frame.maxStreamId}`)
this.remoteMaxStreamId = frame.maxStreamId.toNumber()
break
case FrameType.ConnectionStreamIdBlocked:
this.debug(`remote wants to open more streams but we are blocking them`)
break
case FrameType.StreamClose:
this.handleStreamClose(frame)
break
case FrameType.StreamMaxMoney:
this.debug(`peer told us that stream ${frame.streamId} can receive up to: ${frame.receiveMax} and has received: ${frame.totalReceived} so far`)
stream = this.streams.get(frame.streamId.toNumber())
if (!stream) {
break
}
stream._remoteReceived = BigNumber.maximum(stream._remoteReceived, frame.totalReceived)
if (stream._remoteReceiveMax.isFinite()) {
stream._remoteReceiveMax = BigNumber.maximum(stream._remoteReceiveMax, frame.receiveMax)
} else {
stream._remoteReceiveMax = frame.receiveMax
}
if (stream._remoteReceiveMax.isGreaterThan(stream._remoteReceived)
&& stream._getAmountAvailableToSend().isGreaterThan(0)) {
/* tslint:disable-next-line:no-floating-promises */
this.startSendLoop()
}
break
case FrameType.StreamMoneyBlocked:
this.debug(`peer told us that they want to send more money on stream ${frame.streamId} but we are blocking them. they have sent: ${frame.totalSent} so far and want to send: ${frame.sendMax}`)
break
case FrameType.StreamData:
this.debug(`got data for stream ${frame.streamId}`)
stream = this.streams.get(frame.streamId.toNumber())
if (!stream) {
break
}
stream._pushIncomingData(frame.data, frame.offset.toNumber())
// Make sure the peer hasn't exceeded the flow control limits
const incomingOffsets = stream._getIncomingOffsets()
if (incomingOffsets.max > incomingOffsets.maxAcceptable) {
/* tslint:disable-next-line:no-floating-promises */
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}`))
}
break
case FrameType.StreamMaxData:
stream = this.streams.get(frame.streamId.toNumber())
if (!stream) {
break
}
this.debug(`peer told us that stream ${frame.streamId} can receive up to byte offset: ${frame.maxOffset} (we've sent up to offset: ${stream._getOutgoingOffsets().current})`)
const oldOffset = stream._remoteMaxOffset
stream._remoteMaxOffset = frame.maxOffset.toNumber()
if (stream._remoteMaxOffset > oldOffset) {
/* tslint:disable-next-line:no-floating-promises */
this.startSendLoop()
}
break
case FrameType.StreamDataBlocked:
stream = this.streams.get(frame.streamId.toNumber())
if (!stream) {
break
}
this.debug(`peer told us that stream ${frame.streamId} is blocked. they want to send up to offset: ${frame.maxOffset}, but we are only allowing up to: ${stream._getIncomingOffsets().maxAcceptable}`)
break
default:
continue
}
}
}
/**
* Handle the initial connection from the other side
*/
protected handleConnect () {
this.closed = false
this.debug('connected')
this.safeEmit('connect')
// Tell the other side our max stream id
this.queuedFrames.push(new ConnectionMaxStreamIdFrame(this.maxStreamId))
}
/**
* Ensure that the new stream is valid and does not exceed our limits
* and if it looks good, emit the 'stream' event
*/
protected handleNewStream (streamId: number): void {
if (this.streams.has(streamId) || this.closedStreams[streamId]) {
return
}
// Validate stream ID
if (this.isServer && streamId % 2 === 0) {
this.debug(`got invalid stream ID ${streamId} from peer (should be odd)`)
this.queuedFrames.push(new ConnectionCloseFrame(ErrorCode.ProtocolViolation, `Invalid Stream ID: ${streamId}. Client-initiated streams must have odd-numbered IDs`))
// TODO this should probably call this.destroy
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.debug(`got invalid stream ID ${streamId} from peer (should be even)`)
this.queuedFrames.push(new ConnectionCloseFrame(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
}
// Make sure there aren't too many open streams
if (streamId > this.maxStreamId) {
this.debug(`peer opened too many streams. got stream: ${streamId}, but max stream id is: ${this.maxStreamId}. closing connection`)
this.queuedFrames.push(new ConnectionCloseFrame(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
}
// Let the other side know if they're getting close to the number of streams
if (this.maxStreamId * .75 < streamId) {
this.debug(`informing peer that our max stream id is: ${this.maxStreamId}`)
this.queuedFrames.push(new ConnectionMaxStreamIdFrame(this.maxStreamId))
}
this.debug(`got new stream: ${streamId}`)
const stream = new DataAndMoneyStream({
id: streamId,
isServer: this.isServer
})
this.streams.set(streamId, stream)
stream.on('_maybe_start_send_loop', () => this.startSendLoop())
stream.once('close', () => this.removeStreamRecord(stream))
this.safeEmit('stream', stream)
}
/**
* Mark the stream as closed
*/
protected handleStreamClose (frame: StreamCloseFrame) {
const streamId = frame.streamId.toNumber()
const stream = this.streams.get(streamId)
if (!stream) {
this.debug(`remote error on stream ${streamId}, but we don't have a record of that stream`)
return
}
if (!stream.isOpen() || stream._remoteSentEnd) {
return
}
this.debug(`peer closed stream ${stream.id} with error code: ${ErrorCode[frame.errorCode]} and message: ${frame.errorMessage}`)
// TODO should we confirm with the other side that we closed it?
stream._sentEnd = true
let err
if (frame.errorMessage) {
err = new Error(frame.errorMessage)
err.name = ErrorCode[frame.errorCode]
}
stream._remoteEnded(err)
// TODO make sure we don't send more than one of these frames per packet
this.maxStreamId += 2
this.debug(`raising maxStreamId to ${this.maxStreamId}`)
this.queuedFrames.push(new ConnectionMaxStreamIdFrame(this.maxStreamId))
// Start send loop to make sure this frame is sent
/* tslint:disable-next-line:no-floating-promises */
this.startSendLoop()
}
/**
* (Internal) Start sending packets with money and/or data, as necessary.
* @private
*/
protected async startSendLoop () {
if (this.sending) {
return
}
if (this.remoteClosed) {
this.debug('remote connection is already closed, not starting another loop')
this.safeEmit('_send_loop_finished')
return
}
if (!this.destinationAccount) {
this.debug('not sending because we do not know the client\'s address')
this.sending = false
return
}
this.sending = true
this.debug('starting send loop')
try {
while (this.sending) {
// Send a test packet first to determine the exchange rate
if (!this.exchangeRate) {
this.debug('determining exchange rate')
await this.sendTestPacket()
if (this.exchangeRate) {
this.safeEmit('connect')
this.debug('connected')
}
} else {
// TODO Send multiple packets at the same time (don't await promise)
// TODO Figure out if we need to wait before sending the next one
await this.loadAndSendPacket()
}
}
} catch (err) {
// TODO should a connection error be an error on all of the streams?
return this.destroy(err)
}
this.debug('finished sending')
this.safeEmit('_send_loop_finished')
for (let [_, stream] of this.streams) {
stream.emit('_send_loop_finished')
}
}
/**
* Load up a packet money and/or data, send it to the other party, and handle the result.
* @private
*/
protected async loadAndSendPacket (): Promise<void> {
// Actually send on the next tick of the event loop in case multiple streams
// have their limits raised at the same time
await new Promise((resolve, reject) => setImmediate(resolve))
this.debug('loadAndSendPacket')
let amountToSend = new BigNumber(0)
// Set packet number to correlate response with request
const requestPacket = new Packet(this.nextPacketSequence++, IlpPacketType.Prepare)
// TODO make sure these aren't too big
requestPacket.frames = this.queuedFrames
this.queuedFrames = []
// Send control frames
// TODO only send the max amount when it changes
for (let [_, stream] of this.streams) {
if (stream.isOpen()) {
requestPacket.frames.push(new StreamMaxMoneyFrame(stream.id, stream.receiveMax, stream.totalReceived))
}
}
if (this.closed && !this.remoteClosed) {
// TODO how do we know if there was an error?
this.debug('sending connection close frame')
requestPacket.frames.push(new ConnectionCloseFrame(ErrorCode.NoError, ''))
// TODO don't put any more frames because the connection is closed
// TODO only mark this as closed once we confirm that with the receiver
this.remoteClosed = true
}
// Determine how much to send based on amount frames and path maximum packet amount
let maxAmountFromNextStream = this.testMaximumPacketAmount
const streamsSentFrom = []
for (let [_, stream] of this.streams) {
if (stream._sentEnd) {
// TODO just remove closed streams?
continue
}
// Determine how much to send from this stream based on how much it has available
// and how much the receiver side of this stream can receive
let amountToSendFromStream = BigNumber.minimum(stream._getAmountAvailableToSend(), maxAmountFromNextStream)
if (this.exchangeRate) {
const maxDestinationAmount = stream._remoteReceiveMax.minus(stream._remoteReceived)
const maxSourceAmount = maxDestinationAmount.dividedBy(this.exchangeRate).integerValue(BigNumber.ROUND_CEIL)
if (maxSourceAmount.isLessThan(amountToSendFromStream)) {
this.debug(`stream ${stream.id} could send ${amountToSendFromStream} but that would be more than the receiver says they can receive, so we'll send ${maxSourceAmount} instead`)
amountToSendFromStream = maxSourceAmount
}
}
this.debug(`amount to send from stream ${stream.id}: ${amountToSendFromStream}, exchange rate: ${this.exchangeRate}, remote total received: ${stream._remoteReceived}, remote receive max: ${stream._remoteReceiveMax}`)
// Hold the money and add a frame to the packet
if (amountToSendFromStream.isGreaterThan(0)) {
stream._holdOutgoing(requestPacket.sequence.toString(), amountToSendFromStream)
// TODO make sure the length of the frames doesn't exceed packet data limit
requestPacket.frames.push(new StreamMoneyFrame(stream.id, amountToSendFromStream))
amountToSend = amountToSend.plus(amountToSendFromStream)
maxAmountFromNextStream = maxAmountFromNextStream.minus(amountToSendFromStream)
streamsSentFrom.push(stream)
}
// Tell peer if they're blocking us from sending money
const amountLeftStreamWantsToSend = new BigNumber(stream.sendMax).minus(stream.totalSent).minus(amountToSendFromStream)
/* tslint:disable-next-line:no-unnecessary-type-assertion */
if (amountLeftStreamWantsToSend.times(this.exchangeRate!).isGreaterThan(stream._remoteReceiveMax.minus(stream._remoteReceived))) {
requestPacket.frames.push(new StreamMoneyBlockedFrame(stream.id, stream.sendMax, stream.totalSent))
}
if (maxAmountFromNextStream.isEqualTo(0)) {
// TODO make sure that we start with those later frames the next time around
break
}
}
let bytesLeftInPacket = MAX_DATA_SIZE - requestPacket.byteLength()
// Respect connection-level flow control
const maxBytesRemoteConnectionCanReceive = this.remoteMaxOffset - this.getOutgoingOffsets().currentOffset
if (bytesLeftInPacket > maxBytesRemoteConnectionCanReceive) {
const outgoingMaxOffset = this.getOutgoingOffsets().maxOffset
this.debug(`peer is blocking us from sending more data. they will only accept up to offset: ${this.remoteMaxOffset}, but we want to send up to: ${outgoingMaxOffset}`)
requestPacket.frames.push(new ConnectionDataBlockedFrame(outgoingMaxOffset))
bytesLeftInPacket = maxBytesRemoteConnectionCanReceive
}
for (let [_, stream] of this.streams) {
// TODO use a sensible estimate for the StreamDataFrame overhead
const { data, offset } = stream._getAvailableDataToSend(bytesLeftInPacket - 20)
if (data && data.length > 0) {
const streamDataFrame = new StreamDataFrame(stream.id, offset, data)
this.debug(`sending ${data.length} bytes from stream ${stream.id}`)
bytesLeftInPacket -= streamDataFrame.byteLength()
requestPacket.frames.push(streamDataFrame)
}
// Inform remote which streams are blocked
const maxOutgoingOffset = stream._isDataBlocked()
if (maxOutgoingOffset) {
this.debug(`telling remote that stream ${stream.id} is blocked and has more data to send`)
requestPacket.frames.push(new StreamDataBlockedFrame(stream.id, maxOutgoingOffset))
}
}
// Check if we can stop sending
if (amountToSend.isEqualTo(0)) {
if (requestPacket.frames.length === 0) {
this.sending = false
return
} else {
// Check if any Close, Data, or Money Frames are present in the packet.
// If any of those are do not sent sending to false so the send loop
// has an opportunity to retry if those packets are rejected.
if (!requestPacket.frames.find(frame =>
((frame.type === FrameType.StreamClose)
|| (frame.type === FrameType.StreamData)
|| (frame.type === FrameType.StreamMoney)))) {
this.sending = false
}
}
}
// Set minimum destination amount
if (this.exchangeRate) {
const minimumDestinationAmount = amountToSend.times(this.exchangeRate)
.times(new BigNumber(1).minus(this.slippage))
.integerValue(BigNumber.ROUND_FLOOR)
if (minimumDestinationAmount.isGreaterThan(0)) {
requestPacket.prepareAmount = minimumDestinationAmount
}
}
const responsePacket = await this.sendPacket(requestPacket, amountToSend, false)
if (responsePacket) {
this.handleControlFrames(responsePacket.frames)
// Track the exchange rate for the last packet (whether it was fulfilled or rejected)
if (amountToSend.isGreaterThan(0)) {
this._lastPacketExchangeRate = responsePacket.prepareAmount.dividedBy(amountToSend)
}
if (responsePacket.ilpPacketType === IlpPacketType.Fulfill) {
for (let stream of streamsSentFrom) {
stream._executeHold(requestPacket.sequence.toString())
}
// Update stats based on amount sent
this._totalDelivered = this._totalDelivered.plus(responsePacket.prepareAmount)
this._totalSent = this._totalSent.plus(amountToSend)
// If we're trying to pinpoint the Maximum Packet Amount, raise
// the limit because we know that the testMaximumPacketAmount works
if (this.maximumPacketAmount.isFinite()
&& amountToSend.isEqualTo(this.testMaximumPacketAmount)
&& this.testMaximumPacketAmount.isLessThan(this.maximumPacketAmount)) {
const newTestMax = this.maximumPacketAmount.plus(this.testMaximumPacketAmount).dividedToIntegerBy(2)
this.debug(`maximum packet amount is between ${this.testMaximumPacketAmount} and ${this.maximumPacketAmount}, trying: ${newTestMax}`)
this.testMaximumPacketAmount = newTestMax
}
// Reset the retry delay
this.retryDelay = RETRY_DELAY_START
}
}
}
/**
* (Internal) Send an unfulfillable test packet. Primarily used for determining the path exchange rate.
* @private
*/
protected async sendTestPacket (amount?: BigNumber): Promise<void> {
this.debug('sendTestPacket')
if (!this.destinationAccount) {
throw new Error('Cannot send test packet. Destination account is unknown')
}
// Set packet number to correlate response with request
const requestPacket = new Packet(this.nextPacketSequence++, IlpPacketType.Prepare)
if (!this.remoteKnowsOurAccount) {
this.debug('sending source address to peer')
// TODO attach a token to the account?
requestPacket.frames.push(new ConnectionNewAddressFrame(this.sourceAccount))
}
const sourceAmount = amount || BigNumber.minimum(TEST_PACKET_AMOUNT, this.testMaximumPacketAmount)
const responsePacket = await this.sendPacket(requestPacket, sourceAmount, true)
if (!responsePacket) {
return
}
this.remoteKnowsOurAccount = true
// Determine exchange rate from amount that arrived
this.exchangeRate = responsePacket.prepareAmount.dividedBy(sourceAmount)
this.debug(`determined exchange rate to be: ${this.exchangeRate}`)
if (this.exchangeRate.isEqualTo(0)) {
// TODO this could also happen if the exchange rate is less than 1 / TEST_PACKET_AMOUNT
throw new Error('Exchange rate is 0. We will not be able to send anything through this path')
}
this.handleControlFrames(responsePacket.frames)
}
/**
* Send a ConnectionClose frame to the other side