1
- // FullNodePeer.ts
2
-
3
1
import path from "path" ;
4
2
import os from "os" ;
5
3
import fs from "fs" ;
@@ -12,7 +10,6 @@ import { Environment } from "../utils/Environment";
12
10
import NodeCache from "node-cache" ;
13
11
import Bottleneck from "bottleneck" ;
14
12
15
- // Constants
16
13
const FULLNODE_PORT = 8444 ;
17
14
const LOCALHOST = "127.0.0.1" ;
18
15
const CHIA_NODES_HOST = "chia-nodes" ;
@@ -24,9 +21,8 @@ const DNS_HOSTS = [
24
21
] ;
25
22
const CONNECTION_TIMEOUT = 2000 ; // in milliseconds
26
23
const CACHE_DURATION = 30000 ; // in milliseconds
27
- const COOLDOWN_DURATION = 60000 ; // in milliseconds
24
+ const COOLDOWN_DURATION = 600000 ; // 10 minutes in milliseconds
28
25
const MAX_PEERS_TO_FETCH = 5 ; // Maximum number of peers to fetch from DNS
29
- const MAX_RETRIES = 3 ; // Maximum number of retry attempts
30
26
const MAX_REQUESTS_PER_MINUTE = 100 ; // Per-peer rate limit
31
27
32
28
/**
@@ -45,12 +41,12 @@ export class FullNodePeer {
45
41
// Singleton instance
46
42
private static instance : FullNodePeer | null = null ;
47
43
48
- // Cached peer with timestamp
49
- private static cachedPeer : { peer : Peer ; timestamp : number } | null = null ;
50
-
51
44
// Cooldown cache to exclude faulty peers temporarily
52
45
private static cooldownCache = new NodeCache ( { stdTTL : COOLDOWN_DURATION / 1000 } ) ;
53
46
47
+ // Failed DNS hosts cooldown cache
48
+ private static failedDNSCache = new NodeCache ( { stdTTL : COOLDOWN_DURATION / 1000 } ) ;
49
+
54
50
// Peer reliability weights
55
51
private static peerWeights : Map < string , number > = new Map ( ) ;
56
52
@@ -66,6 +62,9 @@ export class FullNodePeer {
66
62
// Map to store rate limiters per peer IP
67
63
private static peerLimiters : Map < string , Bottleneck > = new Map ( ) ;
68
64
65
+ // Round-robin index
66
+ private static roundRobinIndex : number = 0 ;
67
+
69
68
// Private constructor for singleton pattern
70
69
private constructor ( private peer : Peer ) { }
71
70
@@ -91,7 +90,7 @@ export class FullNodePeer {
91
90
this . peer = bestPeer ;
92
91
FullNodePeer . instance = this ; // Assign the initialized instance
93
92
} catch ( error : any ) {
94
- console . error ( `Initialization failed: ${ error . message } ` ) ;
93
+ console . error ( `Fullnode Initialization failed: ${ error . message } ` ) ;
95
94
throw error ;
96
95
}
97
96
}
@@ -103,6 +102,8 @@ export class FullNodePeer {
103
102
*/
104
103
public static async connect ( ) : Promise < Peer > {
105
104
const instance = FullNodePeer . getInstance ( ) ;
105
+ // Remove cached peer to ensure a new connection each time
106
+ FullNodePeer . cachedPeer = null ;
106
107
await instance . initialize ( ) ;
107
108
return instance . peer ;
108
109
}
@@ -216,6 +217,12 @@ export class FullNodePeer {
216
217
// Fetch peers from DNS introducers
217
218
const fetchedPeers : string [ ] = [ ] ;
218
219
for ( const DNS_HOST of DNS_HOSTS ) {
220
+ // Check if DNS_HOST is in failedDNSCache
221
+ if ( FullNodePeer . failedDNSCache . has ( DNS_HOST ) ) {
222
+ console . warn ( `Skipping DNS host ${ DNS_HOST } due to recent failure.` ) ;
223
+ continue ;
224
+ }
225
+
219
226
try {
220
227
const ips = await resolve4 ( DNS_HOST ) ;
221
228
if ( ips && ips . length > 0 ) {
@@ -238,6 +245,8 @@ export class FullNodePeer {
238
245
}
239
246
} catch ( error : any ) {
240
247
console . error ( `Failed to resolve IPs from ${ DNS_HOST } : ${ error . message } ` ) ;
248
+ // Add DNS_HOST to failedDNSCache for cooldown
249
+ FullNodePeer . failedDNSCache . set ( DNS_HOST , true ) ;
241
250
}
242
251
}
243
252
@@ -286,32 +295,22 @@ export class FullNodePeer {
286
295
}
287
296
288
297
/**
289
- * Selects a peer based on weighted random selection.
290
- * Prioritized peers have higher weights.
298
+ * Selects the next peer based on round-robin selection.
291
299
* @returns {string } The selected peer IP.
292
300
*/
293
- private static selectPeerByWeight ( ) : string {
294
- const peers = Array . from ( FullNodePeer . peerWeights . entries ( ) )
295
- . filter ( ( [ ip , _ ] ) => ! FullNodePeer . cooldownCache . has ( ip ) )
296
- . map ( ( [ ip , weight ] ) => ( { ip, weight } ) ) ;
297
-
298
- const totalWeight = peers . reduce ( ( sum , peer ) => sum + peer . weight , 0 ) ;
299
- if ( totalWeight === 0 ) {
300
- throw new Error ( "All peers are in cooldown." ) ;
301
- }
301
+ private static selectPeerRoundRobin ( ) : string {
302
+ const peers = Array . from ( FullNodePeer . peerWeights . keys ( ) ) . filter (
303
+ ( ip ) => ! FullNodePeer . cooldownCache . has ( ip )
304
+ ) ;
302
305
303
- const random = Math . random ( ) * totalWeight ;
304
- let cumulative = 0 ;
305
-
306
- for ( const peer of peers ) {
307
- cumulative += peer . weight ;
308
- if ( random < cumulative ) {
309
- return peer . ip ;
310
- }
306
+ if ( peers . length === 0 ) {
307
+ throw new Error ( "All fullnode peers are in cooldown." ) ;
311
308
}
312
309
313
- // Fallback
314
- return peers [ peers . length - 1 ] . ip ;
310
+ // Round-robin selection
311
+ const selectedPeer = peers [ FullNodePeer . roundRobinIndex % peers . length ] ;
312
+ FullNodePeer . roundRobinIndex += 1 ;
313
+ return selectedPeer ;
315
314
}
316
315
317
316
/**
@@ -332,32 +331,26 @@ export class FullNodePeer {
332
331
}
333
332
334
333
/**
335
- * Connects to the best available peer based on weighted selection and reliability.
334
+ * Connects to the best available peer based on round-robin selection and reliability.
336
335
* @returns {Promise<Peer> } The connected Peer instance.
337
336
*/
338
337
private async getBestPeer ( ) : Promise < Peer > {
339
338
const now = Date . now ( ) ;
340
339
341
- // Refresh cachedPeer if expired
342
- if (
343
- FullNodePeer . cachedPeer &&
344
- now - FullNodePeer . cachedPeer . timestamp < CACHE_DURATION
345
- ) {
346
- return FullNodePeer . cachedPeer . peer ;
347
- }
340
+ // Removed cachedPeer logic to ensure a new connection each time
348
341
349
342
// Fetch peer IPs
350
343
const peerIPs = await FullNodePeer . getPeerIPs ( ) ;
351
344
352
345
// Setup peer weights with prioritization
353
346
FullNodePeer . setupPeers ( peerIPs ) ;
354
347
355
- // Weighted random selection
348
+ // Round-robin selection
356
349
let selectedPeerIP : string ;
357
350
try {
358
- selectedPeerIP = FullNodePeer . selectPeerByWeight ( ) ;
351
+ selectedPeerIP = FullNodePeer . selectPeerRoundRobin ( ) ;
359
352
} catch ( error : any ) {
360
- throw new Error ( `Failed to select a peer: ${ error . message } ` ) ;
353
+ throw new Error ( `Failed to select a fullnode peer: ${ error . message } ` ) ;
361
354
}
362
355
363
356
// Attempt to create a peer connection
@@ -376,7 +369,7 @@ export class FullNodePeer {
376
369
peer = await Peer . new ( `${ selectedPeerIP } :${ FULLNODE_PORT } ` , false , tls ) ;
377
370
} catch ( error : any ) {
378
371
console . error (
379
- `Failed to create peer for IP ${ selectedPeerIP } : ${ error . message } `
372
+ `Failed to create fullnode peer for IP ${ selectedPeerIP } : ${ error . message } `
380
373
) ;
381
374
// Add to cooldown
382
375
FullNodePeer . cooldownCache . set ( selectedPeerIP , true ) ;
@@ -387,7 +380,7 @@ export class FullNodePeer {
387
380
} else {
388
381
FullNodePeer . peerWeights . delete ( selectedPeerIP ) ;
389
382
}
390
- throw new Error ( `Unable to connect to peer ${ selectedPeerIP } ` ) ;
383
+ throw new Error ( `Unable to connect to fullnode peer ${ selectedPeerIP } ` ) ;
391
384
}
392
385
393
386
// Create a Bottleneck limiter for this peer
@@ -407,9 +400,6 @@ export class FullNodePeer {
407
400
FullNodePeer . peerLimiters . set ( selectedPeerIP , limiter ) ;
408
401
const proxiedPeer = this . createPeerProxy ( peer , selectedPeerIP ) ;
409
402
410
- // Cache the peer
411
- FullNodePeer . cachedPeer = { peer : peer , timestamp : now } ;
412
-
413
403
console . log ( `Using Fullnode Peer: ${ selectedPeerIP } ` ) ;
414
404
415
405
return proxiedPeer ;
@@ -427,9 +417,8 @@ export class FullNodePeer {
427
417
// Start the timeout to forget the peer after 1 minute
428
418
const timeoutPromise = new Promise < null > ( ( _ , reject ) => {
429
419
timeoutId = setTimeout ( ( ) => {
430
- FullNodePeer . cachedPeer = null ;
431
420
reject (
432
- new Error ( "Operation timed out. Reconnecting to a new peer." )
421
+ new Error ( "Operation timed out. Reconnecting to a new fullnode peer." )
433
422
) ;
434
423
} , 60000 ) ; // 1 minute
435
424
} ) ;
@@ -453,9 +442,7 @@ export class FullNodePeer {
453
442
error . message . includes ( "WebSocket" ) ||
454
443
error . message . includes ( "Operation timed out" )
455
444
) {
456
- FullNodePeer . cachedPeer = null ;
457
-
458
- this . handlePeerDisconnection ( peerIP ) ;
445
+ FullNodePeer . handlePeerDisconnection ( peerIP ) ;
459
446
const newPeer = await this . getBestPeer ( ) ;
460
447
return ( newPeer as any ) [ prop ] ( ...args ) ;
461
448
}
@@ -472,7 +459,7 @@ export class FullNodePeer {
472
459
* Handles peer disconnection by marking it in cooldown and updating internal states.
473
460
* @param {string } peerIP - The IP address of the disconnected peer.
474
461
*/
475
- public handlePeerDisconnection ( peerIP : string ) : void {
462
+ public static handlePeerDisconnection ( peerIP : string ) : void {
476
463
// Add the faulty peer to the cooldown cache
477
464
FullNodePeer . cooldownCache . set ( peerIP , true ) ;
478
465
@@ -490,7 +477,7 @@ export class FullNodePeer {
490
477
// Remove the limiter
491
478
FullNodePeer . peerLimiters . delete ( peerIP ) ;
492
479
493
- console . warn ( `Peer ${ peerIP } has been marked as disconnected and is in cooldown.` ) ;
480
+ console . warn ( `Fullnode Peer ${ peerIP } has been marked as disconnected and is in cooldown.` ) ;
494
481
}
495
482
496
483
/**
@@ -521,21 +508,21 @@ export class FullNodePeer {
521
508
try {
522
509
peer = await FullNodePeer . connect ( ) ;
523
510
} catch ( error : any ) {
524
- spinner . error ( { text : "Failed to connect to a peer." } ) ;
511
+ spinner . error ( { text : "Failed to connect to a fullnode peer." } ) ;
525
512
console . error ( `waitForConfirmation connection error: ${ error . message } ` ) ;
526
513
throw error ;
527
514
}
528
515
529
516
// Extract peer IP to access the corresponding limiter
530
517
const peerIP = FullNodePeer . extractPeerIP ( peer ) ;
531
518
if ( ! peerIP ) {
532
- spinner . error ( { text : "Failed to extract peer IP." } ) ;
519
+ spinner . error ( { text : "Failed to extract fullnode peer IP." } ) ;
533
520
throw new Error ( "Failed to extract peer IP." ) ;
534
521
}
535
522
536
523
const limiter = FullNodePeer . peerLimiters . get ( peerIP ) ;
537
524
if ( ! limiter ) {
538
- spinner . error ( { text : "No rate limiter found for the peer." } ) ;
525
+ spinner . error ( { text : "No rate limiter found for the fullnode peer." } ) ;
539
526
throw new Error ( "No rate limiter found for the peer." ) ;
540
527
}
541
528
0 commit comments