diodejs 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bindPort.js +74 -22
- package/connection.js +226 -53
- package/package.json +1 -1
- package/publishPort.js +12 -2
- package/rpc.js +82 -5
- package/test/bindPort.test.js +165 -1
- package/test/connection.ticketRetry.test.js +162 -0
- package/test/publishPort.test.js +52 -1
- package/test/rpc.dioTraffic.test.js +124 -0
package/bindPort.js
CHANGED
|
@@ -403,7 +403,7 @@ class BindPort {
|
|
|
403
403
|
logger.info(() => `Port closed for ref: ${dataRef.toString('hex')}`);
|
|
404
404
|
}
|
|
405
405
|
} else {
|
|
406
|
-
if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request') {
|
|
406
|
+
if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request' && messageType != 'response') {
|
|
407
407
|
logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
|
|
408
408
|
}
|
|
409
409
|
}
|
|
@@ -660,6 +660,46 @@ class BindPort {
|
|
|
660
660
|
let rpc = null;
|
|
661
661
|
|
|
662
662
|
let ref;
|
|
663
|
+
let clientClosed = false;
|
|
664
|
+
let remoteCleanupStarted = false;
|
|
665
|
+
let tlsSocketWrapper = null;
|
|
666
|
+
|
|
667
|
+
const closeRemoteRef = async () => {
|
|
668
|
+
if (remoteCleanupStarted) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!ref || !connection || !rpc) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
remoteCleanupStarted = true;
|
|
676
|
+
|
|
677
|
+
const storedSocket = typeof connection.getClientSocket === 'function'
|
|
678
|
+
? connection.getClientSocket(ref)
|
|
679
|
+
: null;
|
|
680
|
+
if (storedSocket && storedSocket !== clientSocket && typeof storedSocket.end === 'function') {
|
|
681
|
+
try { storedSocket.end(); } catch {}
|
|
682
|
+
}
|
|
683
|
+
if (typeof connection.deleteClientSocket === 'function') {
|
|
684
|
+
try { connection.deleteClientSocket(ref); } catch {}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
await rpc.portClose(ref);
|
|
689
|
+
logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
logger.error(() => `Error closing port on device: ${error}`);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
clientSocket.once('close', () => {
|
|
696
|
+
clientClosed = true;
|
|
697
|
+
closeRemoteRef();
|
|
698
|
+
});
|
|
699
|
+
clientSocket.once('error', (err) => {
|
|
700
|
+
logger.error(() => `Client socket error: ${err}`);
|
|
701
|
+
});
|
|
702
|
+
|
|
663
703
|
if (useNative) {
|
|
664
704
|
try {
|
|
665
705
|
connection = await this._resolveConnectionForDevice(deviceId);
|
|
@@ -783,6 +823,12 @@ class BindPort {
|
|
|
783
823
|
return;
|
|
784
824
|
}
|
|
785
825
|
|
|
826
|
+
if (clientClosed || clientSocket.destroyed) {
|
|
827
|
+
logger.warn(() => `Local client disconnected before port ${formattedTargetPort} opened; closing ref ${ref.toString('hex')}`);
|
|
828
|
+
await closeRemoteRef();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
786
832
|
if (protocol === 'tls') {
|
|
787
833
|
// For tls protocol, create a proper tls connection
|
|
788
834
|
try {
|
|
@@ -829,16 +875,27 @@ class BindPort {
|
|
|
829
875
|
diodeSocket,
|
|
830
876
|
tlsSocket,
|
|
831
877
|
end: () => {
|
|
832
|
-
tlsSocket.end();
|
|
833
|
-
diodeSocket._destroy(null, () => {});
|
|
878
|
+
try { tlsSocket.end(); } catch {}
|
|
879
|
+
try { diodeSocket._destroy(null, () => {}); } catch {}
|
|
834
880
|
}
|
|
835
881
|
};
|
|
882
|
+
tlsSocketWrapper = socketWrapper;
|
|
836
883
|
|
|
837
884
|
// Store the socket wrapper
|
|
838
885
|
connection.addClientSocket(ref, socketWrapper);
|
|
886
|
+
tlsSocket.once('close', () => {
|
|
887
|
+
if (!clientSocket.destroyed) {
|
|
888
|
+
clientSocket.end();
|
|
889
|
+
}
|
|
890
|
+
closeRemoteRef();
|
|
891
|
+
});
|
|
839
892
|
|
|
840
893
|
} catch (error) {
|
|
841
894
|
logger.error(() => `Error setting up tls connection: ${error}`);
|
|
895
|
+
if (tlsSocketWrapper && typeof tlsSocketWrapper.end === 'function') {
|
|
896
|
+
tlsSocketWrapper.end();
|
|
897
|
+
}
|
|
898
|
+
await closeRemoteRef();
|
|
842
899
|
clientSocket.destroy();
|
|
843
900
|
return;
|
|
844
901
|
}
|
|
@@ -847,9 +904,19 @@ class BindPort {
|
|
|
847
904
|
connection.addClientSocket(ref, clientSocket);
|
|
848
905
|
|
|
849
906
|
// Handle data from client to device
|
|
850
|
-
clientSocket.on('data',
|
|
907
|
+
clientSocket.on('data', (data) => {
|
|
908
|
+
clientSocket.pause();
|
|
851
909
|
try {
|
|
852
|
-
|
|
910
|
+
rpc.portSend(ref, data)
|
|
911
|
+
.then(() => {
|
|
912
|
+
if (!clientSocket.destroyed) {
|
|
913
|
+
clientSocket.resume();
|
|
914
|
+
}
|
|
915
|
+
})
|
|
916
|
+
.catch((error) => {
|
|
917
|
+
logger.error(() => `Error sending data to device: ${error}`);
|
|
918
|
+
clientSocket.destroy();
|
|
919
|
+
});
|
|
853
920
|
} catch (error) {
|
|
854
921
|
logger.error(() => `Error sending data to device: ${error}`);
|
|
855
922
|
clientSocket.destroy();
|
|
@@ -858,24 +925,9 @@ class BindPort {
|
|
|
858
925
|
}
|
|
859
926
|
|
|
860
927
|
// Handle client socket closure (common for all protocols)
|
|
861
|
-
clientSocket.
|
|
928
|
+
clientSocket.once('end', () => {
|
|
862
929
|
logger.info(() => 'Client disconnected');
|
|
863
|
-
|
|
864
|
-
try {
|
|
865
|
-
await rpc.portClose(ref);
|
|
866
|
-
logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
|
|
867
|
-
connection.deleteClientSocket(ref);
|
|
868
|
-
} catch (error) {
|
|
869
|
-
logger.error(() => `Error closing port on device: ${error}`);
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
logger.warn(() => 'Ref is invalid or no longer in clientSockets.');
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// Handle client socket errors
|
|
877
|
-
clientSocket.on('error', (err) => {
|
|
878
|
-
logger.error(() => `Client socket error: ${err}`);
|
|
930
|
+
closeRemoteRef();
|
|
879
931
|
});
|
|
880
932
|
});
|
|
881
933
|
|
package/connection.js
CHANGED
|
@@ -91,9 +91,13 @@ class DiodeConnection extends EventEmitter {
|
|
|
91
91
|
// Add ticket batching configuration
|
|
92
92
|
this.lastTicketUpdate = Date.now();
|
|
93
93
|
this.accumulatedBytes = 0;
|
|
94
|
-
this.ticketUpdateThreshold = parseInt(process.env.DIODE_TICKET_BYTES_THRESHOLD, 10) ||
|
|
94
|
+
this.ticketUpdateThreshold = parseInt(process.env.DIODE_TICKET_BYTES_THRESHOLD, 10) || 4 * 1024 * 1024; // 4MB default
|
|
95
95
|
this.ticketUpdateInterval = parseInt(process.env.DIODE_TICKET_UPDATE_INTERVAL, 10) || 30000; // 30 seconds default
|
|
96
96
|
this.ticketUpdateTimer = null;
|
|
97
|
+
this.ticketUpdateInFlight = false;
|
|
98
|
+
this.pendingTicketUpdateForce = false;
|
|
99
|
+
this.lastAcceptedTicketBytes = 0;
|
|
100
|
+
this.lastRelayMeasuredBytes = 0;
|
|
97
101
|
|
|
98
102
|
// Log the ticket batching settings
|
|
99
103
|
logger.info(() => `Ticket batching settings - Bytes Threshold: ${this.ticketUpdateThreshold} bytes, Update Interval: ${this.ticketUpdateInterval}ms`);
|
|
@@ -144,6 +148,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
144
148
|
this._startTicketUpdateTimer();
|
|
145
149
|
// Send the ticketv2 command
|
|
146
150
|
try {
|
|
151
|
+
await this._syncMeasuredBytesWithRelay();
|
|
147
152
|
const ticketCommand = await this.createTicketCommand();
|
|
148
153
|
const response = await this.sendCommand(ticketCommand).catch(reject);
|
|
149
154
|
resolve();
|
|
@@ -155,6 +160,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
155
160
|
|
|
156
161
|
this.socket.on('data', (data) => {
|
|
157
162
|
// logger.debug(() => `Received data: ${data.toString('hex')}`);
|
|
163
|
+
this._recordTrafficBytes(data.length);
|
|
158
164
|
try {
|
|
159
165
|
this._handleData(data);
|
|
160
166
|
} catch (error) {
|
|
@@ -371,17 +377,35 @@ class DiodeConnection extends EventEmitter {
|
|
|
371
377
|
logger.debug(() => `Received response for requestId: ${requestId}`);
|
|
372
378
|
logger.debug(() => `Response Type: '${responseType}'`);
|
|
373
379
|
|
|
374
|
-
const
|
|
380
|
+
const pending = this.pendingRequests.get(requestId);
|
|
381
|
+
const { resolve, reject } = pending;
|
|
375
382
|
try{
|
|
376
383
|
if (responseType === 'response') {
|
|
377
384
|
if (!Array.isArray(responseRaw) && makeReadable(responseRaw) === 'too_low') {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
385
|
+
const originalCommand = pending && pending.commandArray;
|
|
386
|
+
const retryCount = pending && pending.ticketRetryCount ? pending.ticketRetryCount : 0;
|
|
387
|
+
const isTicketCommand = Array.isArray(originalCommand) &&
|
|
388
|
+
(originalCommand[0] === 'ticket' || originalCommand[0] === 'ticketv2');
|
|
389
|
+
|
|
390
|
+
if (isTicketCommand && retryCount < 1) {
|
|
391
|
+
this.fixResponse(responseData);
|
|
392
|
+
this.pendingRequests.delete(requestId);
|
|
393
|
+
this._syncMeasuredBytesWithRelay()
|
|
394
|
+
.catch((error) => {
|
|
395
|
+
logger.debug(() => `Unable to sync relay measured bytes after too_low: ${error}`);
|
|
396
|
+
})
|
|
397
|
+
.then(() => this.createTicketCommand())
|
|
398
|
+
.then((ticketCommand) => this.sendCommand(ticketCommand, { ticketRetryCount: retryCount + 1 }))
|
|
399
|
+
.then(resolve)
|
|
400
|
+
.catch(reject);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this._recordTicketResponse(originalCommand, responseData);
|
|
383
404
|
resolve(responseData);
|
|
405
|
+
this.pendingRequests.delete(requestId);
|
|
406
|
+
return;
|
|
384
407
|
}
|
|
408
|
+
this._recordTicketResponse(pending && pending.commandArray, responseData);
|
|
385
409
|
resolve(responseData);
|
|
386
410
|
} else if (responseType === 'error') {
|
|
387
411
|
if (responseData.length > 1) {
|
|
@@ -433,11 +457,23 @@ class DiodeConnection extends EventEmitter {
|
|
|
433
457
|
}
|
|
434
458
|
|
|
435
459
|
// Send a fresh ticket promptly to avoid disconnect
|
|
436
|
-
this.
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
460
|
+
this._syncMeasuredBytesWithRelay()
|
|
461
|
+
.catch((error) => {
|
|
462
|
+
logger.debug(() => `Unable to sync relay measured bytes for ticket_request: ${error}`);
|
|
463
|
+
})
|
|
464
|
+
.then(() => this.createTicketCommand())
|
|
465
|
+
.then((ticketCommand) => {
|
|
466
|
+
const ticketTotalBytes = parseUInt(ticketCommand[5]);
|
|
467
|
+
return this.sendCommand(ticketCommand).then((responseData) => ({ responseData, ticketTotalBytes }));
|
|
468
|
+
})
|
|
469
|
+
.then(({ responseData, ticketTotalBytes }) => {
|
|
470
|
+
const status = responseData && responseData[0] !== undefined ? parseResponseType(responseData[0]) : '';
|
|
471
|
+
if (status === 'thanks!') {
|
|
472
|
+
if (Number.isFinite(ticketTotalBytes)) {
|
|
473
|
+
this.accumulatedBytes = Math.max(0, this.totalBytes - ticketTotalBytes);
|
|
474
|
+
}
|
|
475
|
+
this.lastTicketUpdate = Date.now();
|
|
476
|
+
}
|
|
441
477
|
})
|
|
442
478
|
.catch((error) => {
|
|
443
479
|
logger.error(() => `Error handling ticket_request: ${error}`);
|
|
@@ -446,26 +482,141 @@ class DiodeConnection extends EventEmitter {
|
|
|
446
482
|
}
|
|
447
483
|
|
|
448
484
|
fixResponse(response) {
|
|
449
|
-
/*
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
485
|
+
/*
|
|
486
|
+
ticketv2 too_low responses are:
|
|
487
|
+
[
|
|
488
|
+
'too_low',
|
|
489
|
+
chain_id,
|
|
490
|
+
epoch,
|
|
491
|
+
last_total_connections,
|
|
492
|
+
last_total_bytes,
|
|
493
|
+
local_address,
|
|
494
|
+
device_signature
|
|
495
|
+
]
|
|
496
|
+
The byte value is the relay's last accepted paid floor. The live
|
|
497
|
+
unpaid measurement still comes from the relay "bytes" command.
|
|
461
498
|
*/
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
499
|
+
const lastTicket = this._parseTooLowTicketSummary(response);
|
|
500
|
+
if (Number.isFinite(lastTicket.totalConnections)) {
|
|
501
|
+
this.totalConnections = Math.max(this.totalConnections, lastTicket.totalConnections);
|
|
502
|
+
}
|
|
503
|
+
if (Number.isFinite(lastTicket.totalBytes)) {
|
|
504
|
+
this.lastAcceptedTicketBytes = Math.max(this.lastAcceptedTicketBytes, lastTicket.totalBytes);
|
|
505
|
+
const relayMeasuredBytes = Number.isFinite(this.lastRelayMeasuredBytes) ? this.lastRelayMeasuredBytes : 0;
|
|
506
|
+
const pendingBytes = Math.max(this.accumulatedBytes, relayMeasuredBytes, 0);
|
|
507
|
+
this.totalBytes = Math.max(this.totalBytes, lastTicket.totalBytes + pendingBytes + 1024);
|
|
508
|
+
this.accumulatedBytes = Math.max(this.accumulatedBytes, this.totalBytes - lastTicket.totalBytes);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_parseTooLowTicketSummary(response) {
|
|
513
|
+
if (!Array.isArray(response)) {
|
|
514
|
+
return { totalConnections: null, totalBytes: null };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const firstItem = response[0];
|
|
518
|
+
const firstItemType = typeof firstItem === 'string' || Buffer.isBuffer(firstItem) || firstItem instanceof Uint8Array
|
|
519
|
+
? parseResponseType(firstItem)
|
|
520
|
+
: '';
|
|
521
|
+
let normalized = response;
|
|
522
|
+
if (firstItemType === 'response') {
|
|
523
|
+
const secondItem = response[1];
|
|
524
|
+
const secondItemType = typeof secondItem === 'string' || Buffer.isBuffer(secondItem) || secondItem instanceof Uint8Array
|
|
525
|
+
? parseResponseType(secondItem)
|
|
526
|
+
: '';
|
|
527
|
+
normalized = secondItemType === 'too_low' ? response.slice(2) : response;
|
|
528
|
+
} else if (firstItemType === 'too_low') {
|
|
529
|
+
normalized = response.slice(1);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (normalized.length >= 6) {
|
|
533
|
+
return {
|
|
534
|
+
version: 2,
|
|
535
|
+
chainId: parseUInt(normalized[0]),
|
|
536
|
+
epoch: parseUInt(normalized[1]),
|
|
537
|
+
totalConnections: parseUInt(normalized[2]),
|
|
538
|
+
totalBytes: parseUInt(normalized[3]),
|
|
539
|
+
localAddress: normalized[4],
|
|
540
|
+
deviceSignature: normalized[5],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (normalized.length >= 5) {
|
|
545
|
+
return {
|
|
546
|
+
version: 1,
|
|
547
|
+
blockHash: normalized[0],
|
|
548
|
+
totalConnections: parseUInt(normalized[1]),
|
|
549
|
+
totalBytes: parseUInt(normalized[2]),
|
|
550
|
+
localAddress: normalized[3],
|
|
551
|
+
deviceSignature: normalized[4],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return { totalConnections: null, totalBytes: null };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_isTicketCommand(commandArray) {
|
|
559
|
+
return Array.isArray(commandArray) &&
|
|
560
|
+
(commandArray[0] === 'ticket' || commandArray[0] === 'ticketv2');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
_ticketTotalBytes(commandArray) {
|
|
564
|
+
if (!this._isTicketCommand(commandArray)) return null;
|
|
565
|
+
const index = commandArray[0] === 'ticketv2' ? 5 : 4;
|
|
566
|
+
return parseUInt(commandArray[index]);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
_recordTicketResponse(commandArray, responseData) {
|
|
570
|
+
if (!this._isTicketCommand(commandArray) || !Array.isArray(responseData)) return;
|
|
571
|
+
const status = responseData[0] !== undefined ? parseResponseType(responseData[0]) : '';
|
|
572
|
+
if (status !== 'thanks!') return;
|
|
573
|
+
const ticketTotalBytes = this._ticketTotalBytes(commandArray);
|
|
574
|
+
if (!Number.isFinite(ticketTotalBytes)) return;
|
|
575
|
+
this.lastAcceptedTicketBytes = Math.max(this.lastAcceptedTicketBytes, ticketTotalBytes);
|
|
576
|
+
this.accumulatedBytes = Math.max(0, this.totalBytes - ticketTotalBytes);
|
|
577
|
+
this.lastTicketUpdate = Date.now();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
_recordTrafficBytes(bytesCount) {
|
|
581
|
+
if (!Number.isFinite(bytesCount) || bytesCount <= 0) return;
|
|
582
|
+
this.totalBytes += bytesCount;
|
|
583
|
+
this.accumulatedBytes += bytesCount;
|
|
584
|
+
|
|
585
|
+
if (this.accumulatedBytes >= this.ticketUpdateThreshold) {
|
|
586
|
+
this._updateTicketIfNeeded();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_parseRelaySignedInt(valueRaw) {
|
|
591
|
+
const encoded = parseUInt(valueRaw);
|
|
592
|
+
if (!Number.isFinite(encoded)) return null;
|
|
593
|
+
if (encoded % 2 === 0) return encoded / 2;
|
|
594
|
+
return -((encoded - 1) / 2);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async _syncMeasuredBytesWithRelay() {
|
|
598
|
+
if (!this.socket || this.socket.destroyed) return null;
|
|
599
|
+
const responseData = await this.sendCommand(['bytes']);
|
|
600
|
+
const measuredBytes = responseData && responseData[0] !== undefined
|
|
601
|
+
? this._parseRelaySignedInt(responseData[0])
|
|
602
|
+
: null;
|
|
603
|
+
if (!Number.isFinite(measuredBytes) || measuredBytes <= 0) {
|
|
604
|
+
return measuredBytes;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
this.lastRelayMeasuredBytes = measuredBytes;
|
|
608
|
+
const baseBytes = Number.isFinite(this.lastAcceptedTicketBytes) && this.lastAcceptedTicketBytes > 0
|
|
609
|
+
? this.lastAcceptedTicketBytes
|
|
610
|
+
: 128000;
|
|
611
|
+
const targetTotalBytes = baseBytes + measuredBytes + 1024;
|
|
612
|
+
if (targetTotalBytes > this.totalBytes) {
|
|
613
|
+
this.totalBytes = targetTotalBytes;
|
|
614
|
+
this.accumulatedBytes = Math.max(this.accumulatedBytes, this.totalBytes - baseBytes);
|
|
615
|
+
}
|
|
616
|
+
return measuredBytes;
|
|
466
617
|
}
|
|
467
618
|
|
|
468
|
-
sendCommand(commandArray) {
|
|
619
|
+
sendCommand(commandArray, options = {}) {
|
|
469
620
|
return new Promise((resolve, reject) => {
|
|
470
621
|
this._ensureConnected().then(() => {
|
|
471
622
|
const requestId = this._getNextRequestId();
|
|
@@ -473,7 +624,12 @@ class DiodeConnection extends EventEmitter {
|
|
|
473
624
|
const commandWithId = [requestId, commandArray];
|
|
474
625
|
|
|
475
626
|
// Store the promise callbacks to resolve/reject later
|
|
476
|
-
this.pendingRequests.set(requestId, {
|
|
627
|
+
this.pendingRequests.set(requestId, {
|
|
628
|
+
resolve,
|
|
629
|
+
reject,
|
|
630
|
+
commandArray,
|
|
631
|
+
ticketRetryCount: options.ticketRetryCount || 0,
|
|
632
|
+
});
|
|
477
633
|
|
|
478
634
|
const commandBuffer = RLP.encode(commandWithId);
|
|
479
635
|
const byteLength = commandBuffer.length; // Buffer/Uint8Array length is bytes
|
|
@@ -488,6 +644,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
488
644
|
// logger.debug(() => `Command buffer: ${message.toString('hex')}`);
|
|
489
645
|
|
|
490
646
|
this.socket.write(message);
|
|
647
|
+
this._recordTrafficBytes(message.length);
|
|
491
648
|
}).catch(reject);
|
|
492
649
|
});
|
|
493
650
|
}
|
|
@@ -499,9 +656,6 @@ class DiodeConnection extends EventEmitter {
|
|
|
499
656
|
// Build the message as [requestId, [commandArray]]
|
|
500
657
|
const commandWithId = [requestId, commandArray];
|
|
501
658
|
|
|
502
|
-
// Store the promise callbacks to resolve/reject later
|
|
503
|
-
this.pendingRequests.set(requestId, { resolve, reject });
|
|
504
|
-
|
|
505
659
|
const commandBuffer = RLP.encode(commandWithId);
|
|
506
660
|
const byteLength = commandBuffer.length; // Buffer/Uint8Array length is bytes
|
|
507
661
|
|
|
@@ -514,8 +668,8 @@ class DiodeConnection extends EventEmitter {
|
|
|
514
668
|
logger.debug(() => `Sending command with requestId ${requestId}: ${commandArray}`);
|
|
515
669
|
// logger.debug(() => `Command buffer: ${message.toString('hex')}`);
|
|
516
670
|
|
|
517
|
-
this.socket.write(message);
|
|
518
|
-
|
|
671
|
+
this.socket.write(message, resolve);
|
|
672
|
+
this._recordTrafficBytes(message.length);
|
|
519
673
|
}).catch(reject);
|
|
520
674
|
});
|
|
521
675
|
}
|
|
@@ -765,7 +919,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
765
919
|
}
|
|
766
920
|
|
|
767
921
|
this.ticketUpdateTimer = setTimeout(() => {
|
|
768
|
-
this._updateTicketIfNeeded(
|
|
922
|
+
this._updateTicketIfNeeded();
|
|
769
923
|
}, this.ticketUpdateInterval);
|
|
770
924
|
}
|
|
771
925
|
|
|
@@ -775,6 +929,11 @@ class DiodeConnection extends EventEmitter {
|
|
|
775
929
|
if (!this.socket || this.socket.destroyed) {
|
|
776
930
|
return;
|
|
777
931
|
}
|
|
932
|
+
|
|
933
|
+
if (this.ticketUpdateInFlight) {
|
|
934
|
+
this.pendingTicketUpdateForce = this.pendingTicketUpdateForce || force;
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
778
937
|
|
|
779
938
|
const timeSinceLastUpdate = Date.now() - this.lastTicketUpdate;
|
|
780
939
|
|
|
@@ -783,18 +942,38 @@ class DiodeConnection extends EventEmitter {
|
|
|
783
942
|
(this.accumulatedBytes >= this.ticketUpdateThreshold ||
|
|
784
943
|
timeSinceLastUpdate >= this.ticketUpdateInterval))) {
|
|
785
944
|
|
|
945
|
+
this.ticketUpdateInFlight = true;
|
|
946
|
+
let accepted = false;
|
|
786
947
|
try {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
948
|
+
if (this.accumulatedBytes > 0 || force) {
|
|
949
|
+
logger.debug(() => `Updating ticket: accumulated ${this.accumulatedBytes} bytes, ${timeSinceLastUpdate}ms since last update`);
|
|
950
|
+
await this._syncMeasuredBytesWithRelay().catch((error) => {
|
|
951
|
+
logger.debug(() => `Unable to sync relay measured bytes before ticket update: ${error}`);
|
|
952
|
+
});
|
|
953
|
+
const ticketCommand = await this.createTicketCommand();
|
|
954
|
+
const ticketTotalBytes = parseUInt(ticketCommand[5]);
|
|
955
|
+
const responseData = await this.sendCommand(ticketCommand);
|
|
956
|
+
const status = responseData && responseData[0] !== undefined ? parseResponseType(responseData[0]) : '';
|
|
957
|
+
if (status === 'thanks!') {
|
|
958
|
+
accepted = true;
|
|
959
|
+
if (Number.isFinite(ticketTotalBytes)) {
|
|
960
|
+
this.accumulatedBytes = Math.max(0, this.totalBytes - ticketTotalBytes);
|
|
961
|
+
}
|
|
962
|
+
this.lastTicketUpdate = Date.now();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
796
965
|
} catch (error) {
|
|
797
|
-
|
|
966
|
+
logger.error(() => `Error updating ticket: ${error}`);
|
|
967
|
+
} finally {
|
|
968
|
+
this.ticketUpdateInFlight = false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (accepted && (this.pendingTicketUpdateForce || this.accumulatedBytes >= this.ticketUpdateThreshold)) {
|
|
972
|
+
const pendingForce = this.pendingTicketUpdateForce;
|
|
973
|
+
this.pendingTicketUpdateForce = false;
|
|
974
|
+
setImmediate(() => this._updateTicketIfNeeded(pendingForce));
|
|
975
|
+
} else {
|
|
976
|
+
this.pendingTicketUpdateForce = false;
|
|
798
977
|
}
|
|
799
978
|
}
|
|
800
979
|
|
|
@@ -804,13 +983,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
804
983
|
|
|
805
984
|
// Add method to track bytes without immediate ticket update
|
|
806
985
|
addBytes(bytesCount) {
|
|
807
|
-
this.
|
|
808
|
-
this.accumulatedBytes += bytesCount;
|
|
809
|
-
|
|
810
|
-
// Optionally check if we should update ticket now
|
|
811
|
-
if (this.accumulatedBytes >= this.ticketUpdateThreshold) {
|
|
812
|
-
this._updateTicketIfNeeded();
|
|
813
|
-
}
|
|
986
|
+
this._recordTrafficBytes(bytesCount);
|
|
814
987
|
}
|
|
815
988
|
|
|
816
989
|
// Method to set ticket batching options
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diodejs",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "A JavaScript client for interacting with the Diode network. It provides functionalities to bind and publish ports, send RPC commands, and handle responses.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/publishPort.js
CHANGED
|
@@ -231,7 +231,7 @@ class PublishPort extends EventEmitter {
|
|
|
231
231
|
} else if (messageType === 'portclose') {
|
|
232
232
|
this.handlePortClose(sessionIdRaw, messageContent, connection);
|
|
233
233
|
} else {
|
|
234
|
-
if (messageType !== 'ticket_request') {
|
|
234
|
+
if (messageType !== 'ticket_request' && messageType !== 'response') {
|
|
235
235
|
logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
|
|
236
236
|
}
|
|
237
237
|
}
|
|
@@ -736,7 +736,17 @@ class PublishPort extends EventEmitter {
|
|
|
736
736
|
} else {
|
|
737
737
|
localSocket.on('data', (data) => {
|
|
738
738
|
// When data is received from the local service, send it back via Diode
|
|
739
|
-
|
|
739
|
+
localSocket.pause();
|
|
740
|
+
rpc.portSend(ref, data)
|
|
741
|
+
.then(() => {
|
|
742
|
+
if (!localSocket.destroyed) {
|
|
743
|
+
localSocket.resume();
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
.catch((error) => {
|
|
747
|
+
logger.error(() => `Error sending data to device: ${error}`);
|
|
748
|
+
localSocket.destroy();
|
|
749
|
+
});
|
|
740
750
|
});
|
|
741
751
|
|
|
742
752
|
localSocket.on('end', () => {
|
package/rpc.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
//rpc.js
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
makeReadable,
|
|
4
|
+
parseRequestId,
|
|
5
|
+
parseResponseType,
|
|
6
|
+
parseReason,
|
|
7
|
+
parseUInt,
|
|
8
|
+
toBufferView,
|
|
9
|
+
} = require('./utils');
|
|
3
10
|
const logger = require('./logger');
|
|
4
11
|
|
|
5
12
|
function decodeToken(value) {
|
|
@@ -89,6 +96,80 @@ class DiodeRPC {
|
|
|
89
96
|
expiry: null,
|
|
90
97
|
};
|
|
91
98
|
}
|
|
99
|
+
|
|
100
|
+
async dioTraffic(options = {}) {
|
|
101
|
+
const chainId = options.chainId === undefined ? 1284 : options.chainId;
|
|
102
|
+
const params = options.epoch === undefined ? [chainId] : [chainId, options.epoch];
|
|
103
|
+
|
|
104
|
+
return this.nodeRpc('dio_traffic', params, options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
dio_traffic(options = {}) {
|
|
108
|
+
return this.dioTraffic(options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async nodeRpc(method, params = [], options = {}) {
|
|
112
|
+
if (typeof fetch !== 'function') {
|
|
113
|
+
throw new Error('global fetch is required for node RPC calls');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
117
|
+
? Math.floor(options.timeoutMs)
|
|
118
|
+
: 30000;
|
|
119
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
120
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(this._nodeRpcEndpoint(options), {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
jsonrpc: '2.0',
|
|
128
|
+
id: options.id || 1,
|
|
129
|
+
method,
|
|
130
|
+
params,
|
|
131
|
+
}),
|
|
132
|
+
signal: controller ? controller.signal : undefined,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let payload = {};
|
|
136
|
+
try {
|
|
137
|
+
payload = await response.json();
|
|
138
|
+
} catch (_) {
|
|
139
|
+
payload = {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!response.ok || payload.error) {
|
|
143
|
+
const error = new Error(payload?.error?.message || `${method} failed with status ${response.status}`);
|
|
144
|
+
error.status = response.status;
|
|
145
|
+
error.response = payload;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return payload.result;
|
|
150
|
+
} finally {
|
|
151
|
+
if (timer) {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_nodeRpcEndpoint(options = {}) {
|
|
158
|
+
const protocol = options.protocol || 'https';
|
|
159
|
+
const rpcPort = Number.isFinite(options.rpcPort) && options.rpcPort > 0 ? options.rpcPort : 8443;
|
|
160
|
+
let host = options.host || '';
|
|
161
|
+
if (!host && this.connection) {
|
|
162
|
+
host = this.connection.host;
|
|
163
|
+
}
|
|
164
|
+
if (!host && this.connection && typeof this.connection.getServerRelayHost === 'function') {
|
|
165
|
+
host = this.connection.getServerRelayHost();
|
|
166
|
+
}
|
|
167
|
+
if (!host) {
|
|
168
|
+
throw new Error('No relay host available for node RPC call');
|
|
169
|
+
}
|
|
170
|
+
const formattedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
171
|
+
return `${protocol}://${formattedHost}:${rpcPort}/`;
|
|
172
|
+
}
|
|
92
173
|
|
|
93
174
|
getBlockPeak() {
|
|
94
175
|
return this.connection.sendCommand(['getblockpeak']).then((responseData) => {
|
|
@@ -223,10 +304,6 @@ class DiodeRPC {
|
|
|
223
304
|
}
|
|
224
305
|
|
|
225
306
|
async portSend(ref, data) {
|
|
226
|
-
// Update bytes count but don't update ticket yet
|
|
227
|
-
const bytesToSend = data.length;
|
|
228
|
-
this.connection.addBytes(bytesToSend);
|
|
229
|
-
|
|
230
307
|
// Maximum size that can be sent in a single message (less than 65535 to be safe)
|
|
231
308
|
const MAX_CHUNK_SIZE = 65000;
|
|
232
309
|
|
package/test/bindPort.test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const test = require('node:test');
|
|
2
2
|
const assert = require('node:assert/strict');
|
|
3
3
|
const EventEmitter = require('events');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const { once } = require('events');
|
|
4
6
|
|
|
5
7
|
const BindPort = require('../bindPort');
|
|
6
8
|
|
|
@@ -9,21 +11,61 @@ function makeRef(hex) {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function makeRelay(hostKey, result, calls) {
|
|
14
|
+
const clientSockets = new Map();
|
|
15
|
+
const portCloseCalls = [];
|
|
12
16
|
return {
|
|
13
17
|
_managerHostKey: hostKey,
|
|
14
18
|
socket: { destroyed: false },
|
|
19
|
+
clientSockets,
|
|
20
|
+
portCloseCalls,
|
|
15
21
|
RPC: {
|
|
16
22
|
portOpen: async (_deviceId, port, flags) => {
|
|
17
23
|
calls.push({ hostKey, port, flags });
|
|
18
24
|
if (result instanceof Error) {
|
|
19
25
|
throw result;
|
|
20
26
|
}
|
|
21
|
-
return result;
|
|
27
|
+
return typeof result === 'function' ? result() : result;
|
|
22
28
|
},
|
|
29
|
+
portClose: async (ref) => {
|
|
30
|
+
portCloseCalls.push(ref);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
addClientSocket(ref, socket) {
|
|
34
|
+
clientSockets.set(ref.toString('hex'), socket);
|
|
35
|
+
},
|
|
36
|
+
getClientSocket(ref) {
|
|
37
|
+
return clientSockets.get(ref.toString('hex'));
|
|
38
|
+
},
|
|
39
|
+
deleteClientSocket(ref) {
|
|
40
|
+
return clientSockets.delete(ref.toString('hex'));
|
|
41
|
+
},
|
|
42
|
+
hasClientSocket(ref) {
|
|
43
|
+
return clientSockets.has(ref.toString('hex'));
|
|
23
44
|
},
|
|
24
45
|
};
|
|
25
46
|
}
|
|
26
47
|
|
|
48
|
+
function deferred() {
|
|
49
|
+
let resolve;
|
|
50
|
+
let reject;
|
|
51
|
+
const promise = new Promise((res, rej) => {
|
|
52
|
+
resolve = res;
|
|
53
|
+
reject = rej;
|
|
54
|
+
});
|
|
55
|
+
return { promise, resolve, reject };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function waitFor(predicate, timeoutMs = 1000) {
|
|
59
|
+
const startedAt = Date.now();
|
|
60
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
61
|
+
if (predicate()) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
65
|
+
}
|
|
66
|
+
assert.equal(predicate(), true);
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
class FakeManager extends EventEmitter {
|
|
28
70
|
constructor({ relays, resolvedRelay, nearestRelay, resolveError = null }) {
|
|
29
71
|
super();
|
|
@@ -123,3 +165,125 @@ test('multiple BindPort instances share one manager listener set', () => {
|
|
|
123
165
|
assert.equal(manager.listenerCount('end'), 1);
|
|
124
166
|
assert.equal(manager.listenerCount('error'), 1);
|
|
125
167
|
});
|
|
168
|
+
|
|
169
|
+
test('API bind closes ref when local TCP client disconnects before portopen returns', async () => {
|
|
170
|
+
const calls = [];
|
|
171
|
+
const opened = deferred();
|
|
172
|
+
const ref = makeRef('0a0b0c0d');
|
|
173
|
+
const relay = makeRelay('relay.example:41046', () => opened.promise, calls);
|
|
174
|
+
const manager = new FakeManager({
|
|
175
|
+
relays: [relay],
|
|
176
|
+
resolvedRelay: relay,
|
|
177
|
+
nearestRelay: relay,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const bind = new BindPort(manager, {
|
|
181
|
+
0: {
|
|
182
|
+
targetPort: 8088,
|
|
183
|
+
deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
|
|
184
|
+
protocol: 'tcp',
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
|
|
188
|
+
const server = bind.servers.get(0);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await once(server, 'listening');
|
|
192
|
+
const client = net.connect(server.address().port, '127.0.0.1');
|
|
193
|
+
await once(client, 'connect');
|
|
194
|
+
client.destroy();
|
|
195
|
+
await once(client, 'close');
|
|
196
|
+
|
|
197
|
+
opened.resolve(ref);
|
|
198
|
+
await waitFor(() => relay.portCloseCalls.length === 1);
|
|
199
|
+
|
|
200
|
+
assert.equal(relay.portCloseCalls[0].toString('hex'), ref.toString('hex'));
|
|
201
|
+
assert.equal(relay.hasClientSocket(ref), false);
|
|
202
|
+
} finally {
|
|
203
|
+
server.close();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('API bind closes ref when local TCP client disconnects after portopen returns', async () => {
|
|
208
|
+
const calls = [];
|
|
209
|
+
const ref = makeRef('0e0f1011');
|
|
210
|
+
const relay = makeRelay('relay.example:41046', ref, calls);
|
|
211
|
+
const manager = new FakeManager({
|
|
212
|
+
relays: [relay],
|
|
213
|
+
resolvedRelay: relay,
|
|
214
|
+
nearestRelay: relay,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const bind = new BindPort(manager, {
|
|
218
|
+
0: {
|
|
219
|
+
targetPort: 8088,
|
|
220
|
+
deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
|
|
221
|
+
protocol: 'tcp',
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
|
|
225
|
+
const server = bind.servers.get(0);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await once(server, 'listening');
|
|
229
|
+
const client = net.connect(server.address().port, '127.0.0.1');
|
|
230
|
+
await once(client, 'connect');
|
|
231
|
+
await waitFor(() => relay.hasClientSocket(ref));
|
|
232
|
+
|
|
233
|
+
client.end();
|
|
234
|
+
await once(client, 'close');
|
|
235
|
+
await waitFor(() => relay.portCloseCalls.length === 1);
|
|
236
|
+
|
|
237
|
+
assert.equal(relay.portCloseCalls[0].toString('hex'), ref.toString('hex'));
|
|
238
|
+
assert.equal(relay.hasClientSocket(ref), false);
|
|
239
|
+
} finally {
|
|
240
|
+
server.close();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('API bind pauses local TCP socket while portSend is in flight', async () => {
|
|
245
|
+
const calls = [];
|
|
246
|
+
const send = deferred();
|
|
247
|
+
const ref = makeRef('12131415');
|
|
248
|
+
const relay = makeRelay('relay.example:41046', ref, calls);
|
|
249
|
+
const portSendCalls = [];
|
|
250
|
+
relay.RPC.portSend = async (...args) => {
|
|
251
|
+
portSendCalls.push(args);
|
|
252
|
+
return send.promise;
|
|
253
|
+
};
|
|
254
|
+
const manager = new FakeManager({
|
|
255
|
+
relays: [relay],
|
|
256
|
+
resolvedRelay: relay,
|
|
257
|
+
nearestRelay: relay,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const bind = new BindPort(manager, {
|
|
261
|
+
0: {
|
|
262
|
+
targetPort: 8088,
|
|
263
|
+
deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
|
|
264
|
+
protocol: 'tcp',
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
|
|
268
|
+
const server = bind.servers.get(0);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await once(server, 'listening');
|
|
272
|
+
const client = net.connect(server.address().port, '127.0.0.1');
|
|
273
|
+
await once(client, 'connect');
|
|
274
|
+
await waitFor(() => relay.hasClientSocket(ref));
|
|
275
|
+
|
|
276
|
+
client.write(Buffer.from('hello'));
|
|
277
|
+
await waitFor(() => portSendCalls.length === 1);
|
|
278
|
+
|
|
279
|
+
const acceptedSocket = relay.getClientSocket(ref);
|
|
280
|
+
assert.equal(acceptedSocket.isPaused(), true);
|
|
281
|
+
|
|
282
|
+
send.resolve();
|
|
283
|
+
await waitFor(() => acceptedSocket.isPaused() === false);
|
|
284
|
+
|
|
285
|
+
client.destroy();
|
|
286
|
+
} finally {
|
|
287
|
+
server.close();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { RLP } = require('@ethereumjs/rlp');
|
|
7
|
+
|
|
8
|
+
const DiodeConnection = require('../connection');
|
|
9
|
+
|
|
10
|
+
function makeConnection() {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diode-ticket-retry-test-'));
|
|
12
|
+
return new DiodeConnection('relay.example', 41046, path.join(tempDir, 'keys.json'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function encodeResponse(requestId, response) {
|
|
16
|
+
const payload = RLP.encode([requestId, response]);
|
|
17
|
+
const length = Buffer.alloc(2);
|
|
18
|
+
length.writeUInt16BE(payload.length, 0);
|
|
19
|
+
return Buffer.concat([length, Buffer.from(payload)]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tooLowResponse(overrides = {}) {
|
|
23
|
+
const totalConnections = overrides.totalConnections === undefined ? 11 : overrides.totalConnections;
|
|
24
|
+
const totalBytes = overrides.totalBytes === undefined ? 135591 : overrides.totalBytes;
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
'response',
|
|
28
|
+
'too_low',
|
|
29
|
+
1284,
|
|
30
|
+
687,
|
|
31
|
+
totalConnections,
|
|
32
|
+
totalBytes,
|
|
33
|
+
Buffer.from([0]),
|
|
34
|
+
Buffer.from([1]),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('ticket too_low response retries once with repaired ticket', async () => {
|
|
39
|
+
const connection = makeConnection();
|
|
40
|
+
let retryCount = 0;
|
|
41
|
+
|
|
42
|
+
connection.fixResponse = () => {};
|
|
43
|
+
connection.createTicketCommand = async () => ['ticketv2'];
|
|
44
|
+
connection.sendCommand = async (command, options) => {
|
|
45
|
+
retryCount += 1;
|
|
46
|
+
assert.deepEqual(command, ['ticketv2']);
|
|
47
|
+
assert.equal(options.ticketRetryCount, 1);
|
|
48
|
+
return ['thanks!', 1];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
52
|
+
connection.pendingRequests.set(1, {
|
|
53
|
+
resolve,
|
|
54
|
+
reject,
|
|
55
|
+
commandArray: ['ticketv2'],
|
|
56
|
+
ticketRetryCount: 0,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
connection._handleData(encodeResponse(1, tooLowResponse()));
|
|
61
|
+
const result = await resultPromise;
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(result, ['thanks!', 1]);
|
|
64
|
+
assert.equal(retryCount, 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('ticket too_low response does not retry recursively', async () => {
|
|
68
|
+
const connection = makeConnection();
|
|
69
|
+
let retryCount = 0;
|
|
70
|
+
|
|
71
|
+
connection.fixResponse = () => {};
|
|
72
|
+
connection.createTicketCommand = async () => {
|
|
73
|
+
retryCount += 1;
|
|
74
|
+
return ['ticketv2'];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
78
|
+
connection.pendingRequests.set(1, {
|
|
79
|
+
resolve,
|
|
80
|
+
reject,
|
|
81
|
+
commandArray: ['ticketv2'],
|
|
82
|
+
ticketRetryCount: 1,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
connection._handleData(encodeResponse(1, tooLowResponse()));
|
|
87
|
+
const result = await resultPromise;
|
|
88
|
+
|
|
89
|
+
assert.equal(retryCount, 0);
|
|
90
|
+
assert.equal(Buffer.from(result[0]).toString('utf8'), 'too_low');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('session responses do not swallow later unsolicited messages on the same session', async () => {
|
|
94
|
+
const connection = makeConnection();
|
|
95
|
+
const writes = [];
|
|
96
|
+
|
|
97
|
+
connection._ensureConnected = async () => {};
|
|
98
|
+
connection.socket = {
|
|
99
|
+
write(message, callback) {
|
|
100
|
+
writes.push(message);
|
|
101
|
+
callback();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await connection.sendCommandWithSessionId(['response', Buffer.from([1]), 'ok'], 7);
|
|
106
|
+
|
|
107
|
+
assert.equal(connection.pendingRequests.has(7), false);
|
|
108
|
+
assert.equal(writes.length, 1);
|
|
109
|
+
|
|
110
|
+
const unsolicited = new Promise((resolve) => {
|
|
111
|
+
connection.once('unsolicited', resolve);
|
|
112
|
+
});
|
|
113
|
+
connection._handleData(encodeResponse(7, ['portsend', Buffer.from([1]), Buffer.from('hello')]));
|
|
114
|
+
|
|
115
|
+
const message = await unsolicited;
|
|
116
|
+
assert.equal(Buffer.from(message[0]).readUIntBE(0, Buffer.from(message[0]).length), 7);
|
|
117
|
+
assert.equal(Buffer.from(message[1][0]).toString('utf8'), 'portsend');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('too_low repair does not reduce local byte counters', () => {
|
|
121
|
+
const connection = makeConnection();
|
|
122
|
+
connection.totalConnections = 40;
|
|
123
|
+
connection.totalBytes = 1304576;
|
|
124
|
+
|
|
125
|
+
connection.fixResponse(tooLowResponse());
|
|
126
|
+
|
|
127
|
+
assert.equal(connection.totalConnections, 40);
|
|
128
|
+
assert.equal(connection.totalBytes, 1304576);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('too_low repair uses node-reported paid byte floor', () => {
|
|
132
|
+
const connection = makeConnection();
|
|
133
|
+
connection.totalConnections = 4;
|
|
134
|
+
connection.totalBytes = 128000;
|
|
135
|
+
connection.accumulatedBytes = 2048;
|
|
136
|
+
connection.lastRelayMeasuredBytes = 4096;
|
|
137
|
+
|
|
138
|
+
connection.fixResponse(tooLowResponse({
|
|
139
|
+
totalConnections: 7,
|
|
140
|
+
totalBytes: 135591,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
assert.equal(connection.totalConnections, 7);
|
|
144
|
+
assert.equal(connection.lastAcceptedTicketBytes, 135591);
|
|
145
|
+
assert.equal(connection.totalBytes, 140711);
|
|
146
|
+
assert.equal(connection.accumulatedBytes, 5120);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('ticket update keeps accumulated bytes when ticket is rejected', async () => {
|
|
150
|
+
const connection = makeConnection();
|
|
151
|
+
connection.socket = { destroyed: false };
|
|
152
|
+
connection.accumulatedBytes = 1000;
|
|
153
|
+
connection.lastTicketUpdate = 123;
|
|
154
|
+
connection.createTicketCommand = async () => ['ticketv2'];
|
|
155
|
+
connection.sendCommand = async () => ['too_low'];
|
|
156
|
+
connection._startTicketUpdateTimer = () => {};
|
|
157
|
+
|
|
158
|
+
await connection._updateTicketIfNeeded(true);
|
|
159
|
+
|
|
160
|
+
assert.equal(connection.accumulatedBytes, 1000);
|
|
161
|
+
assert.equal(connection.lastTicketUpdate, 123);
|
|
162
|
+
});
|
package/test/publishPort.test.js
CHANGED
|
@@ -15,10 +15,17 @@ class FakeStreamSocket extends EventEmitter {
|
|
|
15
15
|
this.remoteAddress = undefined;
|
|
16
16
|
this.remotePort = undefined;
|
|
17
17
|
this.writes = [];
|
|
18
|
+
this.pauseCalls = 0;
|
|
19
|
+
this.resumeCalls = 0;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
setNoDelay() {}
|
|
21
|
-
pause() {
|
|
23
|
+
pause() {
|
|
24
|
+
this.pauseCalls += 1;
|
|
25
|
+
}
|
|
26
|
+
resume() {
|
|
27
|
+
this.resumeCalls += 1;
|
|
28
|
+
}
|
|
22
29
|
write(data) {
|
|
23
30
|
this.writes.push(data);
|
|
24
31
|
}
|
|
@@ -145,6 +152,27 @@ function makeDeviceId(hexByte) {
|
|
|
145
152
|
return Buffer.from(byte.repeat(20), 'hex');
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
function deferred() {
|
|
156
|
+
let resolve;
|
|
157
|
+
let reject;
|
|
158
|
+
const promise = new Promise((res, rej) => {
|
|
159
|
+
resolve = res;
|
|
160
|
+
reject = rej;
|
|
161
|
+
});
|
|
162
|
+
return { promise, resolve, reject };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function waitFor(predicate, timeoutMs = 1000) {
|
|
166
|
+
const startedAt = Date.now();
|
|
167
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
168
|
+
if (predicate()) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
172
|
+
}
|
|
173
|
+
assert.equal(predicate(), true);
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
test('PublishPort array input defaults host to 127.0.0.1', () => {
|
|
149
177
|
const connection = new FakeConnection();
|
|
150
178
|
const publishPort = new PublishPort(connection, [8080]);
|
|
@@ -211,6 +239,29 @@ test('TCP publish connects to configured host', () => {
|
|
|
211
239
|
assert.deepEqual(connectCalls[0], { port: 8080, host: '192.168.1.10' });
|
|
212
240
|
});
|
|
213
241
|
|
|
242
|
+
test('TCP publish pauses local service socket while portSend is in flight', async () => {
|
|
243
|
+
const connection = new FakeConnection();
|
|
244
|
+
const publishPort = new PublishPort(connection, [8080]);
|
|
245
|
+
const localSocket = new FakeStreamSocket();
|
|
246
|
+
const send = deferred();
|
|
247
|
+
connection.RPC.portSend = async (...args) => {
|
|
248
|
+
connection.portSendCalls.push(args);
|
|
249
|
+
return send.promise;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
publishPort.setupLocalSocketHandlers(localSocket, makeRef('04'), 'tcp', connection.RPC, connection);
|
|
253
|
+
localSocket.emit('data', Buffer.from('hello'));
|
|
254
|
+
|
|
255
|
+
await waitFor(() => connection.portSendCalls.length === 1);
|
|
256
|
+
assert.equal(localSocket.pauseCalls, 1);
|
|
257
|
+
assert.equal(localSocket.resumeCalls, 0);
|
|
258
|
+
|
|
259
|
+
send.resolve();
|
|
260
|
+
await waitFor(() => localSocket.resumeCalls === 1);
|
|
261
|
+
|
|
262
|
+
publishPort.stopListening();
|
|
263
|
+
});
|
|
264
|
+
|
|
214
265
|
test('TLS publish backend socket connects to configured host', () => {
|
|
215
266
|
const connection = new FakeConnection();
|
|
216
267
|
const publishPort = new PublishPort(connection, {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
|
|
5
|
+
const DiodeRPC = require('../rpc');
|
|
6
|
+
|
|
7
|
+
function withServer(handler) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const server = http.createServer(handler);
|
|
10
|
+
server.once('error', reject);
|
|
11
|
+
server.listen(0, '127.0.0.1', () => {
|
|
12
|
+
resolve({
|
|
13
|
+
port: server.address().port,
|
|
14
|
+
close: () => new Promise((done) => server.close(done)),
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readJson(req) {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
for await (const chunk of req) {
|
|
23
|
+
chunks.push(chunk);
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeRpc(port) {
|
|
29
|
+
return new DiodeRPC({ host: '127.0.0.1' }).dioTraffic({
|
|
30
|
+
protocol: 'http',
|
|
31
|
+
rpcPort: port,
|
|
32
|
+
chainId: 1284,
|
|
33
|
+
epoch: 687,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('dioTraffic posts dio_traffic to the connected relay node RPC endpoint', async () => {
|
|
38
|
+
let requestBody = null;
|
|
39
|
+
const result = {
|
|
40
|
+
chain_id: '0x504',
|
|
41
|
+
epoch: '0x2af',
|
|
42
|
+
fleets: {},
|
|
43
|
+
};
|
|
44
|
+
const server = await withServer(async (req, res) => {
|
|
45
|
+
requestBody = await readJson(req);
|
|
46
|
+
res.setHeader('content-type', 'application/json');
|
|
47
|
+
res.end(JSON.stringify({
|
|
48
|
+
jsonrpc: '2.0',
|
|
49
|
+
id: requestBody.id,
|
|
50
|
+
result,
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = await makeRpc(server.port);
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(response, result);
|
|
58
|
+
assert.equal(requestBody.method, 'dio_traffic');
|
|
59
|
+
assert.deepEqual(requestBody.params, [1284, 687]);
|
|
60
|
+
} finally {
|
|
61
|
+
await server.close();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('dioTraffic defaults to chain 1284 and omits epoch when not supplied', async () => {
|
|
66
|
+
let requestBody = null;
|
|
67
|
+
const server = await withServer(async (req, res) => {
|
|
68
|
+
requestBody = await readJson(req);
|
|
69
|
+
res.setHeader('content-type', 'application/json');
|
|
70
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: requestBody.id, result: { fleets: {} } }));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const rpc = new DiodeRPC({ host: '127.0.0.1' });
|
|
75
|
+
|
|
76
|
+
await rpc.dioTraffic({
|
|
77
|
+
protocol: 'http',
|
|
78
|
+
rpcPort: server.port,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(requestBody.params, [1284]);
|
|
82
|
+
} finally {
|
|
83
|
+
await server.close();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('dio_traffic aliases dioTraffic', async () => {
|
|
88
|
+
let requestBody = null;
|
|
89
|
+
const server = await withServer(async (req, res) => {
|
|
90
|
+
requestBody = await readJson(req);
|
|
91
|
+
res.setHeader('content-type', 'application/json');
|
|
92
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: requestBody.id, result: { fleets: {} } }));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const rpc = new DiodeRPC({ host: '127.0.0.1' });
|
|
97
|
+
|
|
98
|
+
await rpc.dio_traffic({
|
|
99
|
+
protocol: 'http',
|
|
100
|
+
rpcPort: server.port,
|
|
101
|
+
chainId: 1284,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.deepEqual(requestBody.params, [1284]);
|
|
105
|
+
} finally {
|
|
106
|
+
await server.close();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('dioTraffic throws JSON-RPC errors', async () => {
|
|
111
|
+
const server = await withServer(async (_req, res) => {
|
|
112
|
+
res.setHeader('content-type', 'application/json');
|
|
113
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: 1, error: { message: 'bad params' } }));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await assert.rejects(
|
|
118
|
+
() => makeRpc(server.port),
|
|
119
|
+
/bad params/,
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
await server.close();
|
|
123
|
+
}
|
|
124
|
+
});
|