@dignetwork/dig-sdk 0.0.1-alpha.139 → 0.0.1-alpha.141
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.
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import { Peer } from "@dignetwork/datalayer-driver";
|
|
2
|
+
import NodeCache from "node-cache";
|
|
3
|
+
/**
|
|
4
|
+
* Module Augmentation to extend the Peer interface with the 'on' method.
|
|
5
|
+
* This resolves the TypeScript error: Property 'on' does not exist on type 'Peer'.
|
|
6
|
+
*/
|
|
7
|
+
declare module "@dignetwork/datalayer-driver" {
|
|
8
|
+
interface Peer {
|
|
9
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* FullNodePeer manages connections to full nodes, prioritizing certain peers and handling reliability.
|
|
14
|
+
* It implements a singleton pattern to ensure a single instance throughout the application.
|
|
4
15
|
*/
|
|
5
16
|
export declare class FullNodePeer {
|
|
6
|
-
private peer;
|
|
7
17
|
private static instance;
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
private
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
18
|
+
private cachedPeer;
|
|
19
|
+
cooldownCache: NodeCache;
|
|
20
|
+
peerWeights: Map<string, number>;
|
|
21
|
+
private prioritizedPeers;
|
|
22
|
+
private peerInfos;
|
|
23
|
+
private peerIPCache;
|
|
24
|
+
private availablePeers;
|
|
25
|
+
private currentPeerIndex;
|
|
26
|
+
/**
|
|
27
|
+
* Private constructor for singleton pattern.
|
|
28
|
+
*/
|
|
16
29
|
private constructor();
|
|
17
30
|
/**
|
|
18
31
|
* Retrieves the singleton instance of FullNodePeer.
|
|
@@ -36,75 +49,67 @@ export declare class FullNodePeer {
|
|
|
36
49
|
* @param {number} timeout - Connection timeout in milliseconds.
|
|
37
50
|
* @returns {Promise<boolean>} Whether the port is reachable.
|
|
38
51
|
*/
|
|
39
|
-
private
|
|
52
|
+
private isPortReachable;
|
|
40
53
|
/**
|
|
41
54
|
* Validates an IPv4 address.
|
|
42
55
|
* @param {string} ip - The IP address to validate.
|
|
43
56
|
* @returns {boolean} Whether the IP address is valid.
|
|
44
57
|
*/
|
|
45
|
-
private
|
|
58
|
+
private isValidIpAddress;
|
|
46
59
|
/**
|
|
47
60
|
* Retrieves the TRUSTED_FULLNODE IP from the environment and verifies its validity.
|
|
48
61
|
* @returns {string | null} The trusted full node IP or null if invalid.
|
|
49
62
|
*/
|
|
50
|
-
private
|
|
63
|
+
private getTrustedFullNode;
|
|
51
64
|
/**
|
|
52
65
|
* Fetches new peer IPs from DNS introducers and prioritized hosts.
|
|
53
66
|
* Utilizes caching to avoid redundant DNS resolutions.
|
|
54
67
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
55
68
|
*/
|
|
56
|
-
private
|
|
69
|
+
private fetchNewPeerIPs;
|
|
57
70
|
/**
|
|
58
71
|
* Shuffles an array using the Fisher-Yates algorithm.
|
|
59
72
|
* @param {T[]} array - The array to shuffle.
|
|
60
73
|
* @returns {T[]} The shuffled array.
|
|
61
74
|
*/
|
|
62
|
-
private
|
|
75
|
+
private shuffleArray;
|
|
63
76
|
/**
|
|
64
77
|
* Initializes the peer weights map.
|
|
65
78
|
* Assigns higher initial weights to prioritized peers.
|
|
66
79
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
67
80
|
*/
|
|
68
|
-
private
|
|
81
|
+
private initializePeerWeights;
|
|
69
82
|
/**
|
|
70
83
|
* Selects the next peer based on round-robin selection.
|
|
71
|
-
* @returns {
|
|
84
|
+
* @returns {Promise<Peer>} The selected Peer instance.
|
|
72
85
|
*/
|
|
73
|
-
private
|
|
86
|
+
private selectNextPeer;
|
|
74
87
|
/**
|
|
75
88
|
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
76
89
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
77
90
|
*/
|
|
78
|
-
private
|
|
91
|
+
private getPeerIPs;
|
|
79
92
|
/**
|
|
80
93
|
* Initializes the peer weights based on prioritization and reliability.
|
|
81
94
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
82
95
|
*/
|
|
83
|
-
private
|
|
96
|
+
private setupPeers;
|
|
84
97
|
/**
|
|
85
98
|
* Connects to the best available peer based on round-robin selection and reliability.
|
|
86
99
|
* @returns {Promise<Peer>} The connected Peer instance.
|
|
87
100
|
*/
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Creates a proxy for the peer to handle errors, implement retries, and enforce round-robin selection.
|
|
91
|
-
* @param {Peer} peer - The Peer instance.
|
|
92
|
-
* @param {string} peerIP - The IP address of the peer.
|
|
93
|
-
* @param {number} [retryCount=0] - The current retry attempt.
|
|
94
|
-
* @returns {Peer} The proxied Peer instance.
|
|
95
|
-
*/
|
|
96
|
-
private static createPeerProxy;
|
|
101
|
+
getBestPeer(): Promise<Peer>;
|
|
97
102
|
/**
|
|
98
103
|
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
99
104
|
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
100
105
|
*/
|
|
101
|
-
|
|
106
|
+
handlePeerDisconnection(peerIP: string): void;
|
|
102
107
|
/**
|
|
103
108
|
* Extracts the IP address from a Peer instance.
|
|
104
109
|
* @param {Peer} peer - The Peer instance.
|
|
105
110
|
* @returns {string | null} The extracted IP address or null if not found.
|
|
106
111
|
*/
|
|
107
|
-
|
|
112
|
+
extractPeerIP(peer: Peer): string | null;
|
|
108
113
|
/**
|
|
109
114
|
* Waits for a coin to be confirmed (spent) on the blockchain.
|
|
110
115
|
* @param {Buffer} parentCoinInfo - The parent coin information.
|
|
@@ -116,6 +121,6 @@ export declare class FullNodePeer {
|
|
|
116
121
|
* @param {number} ms - Milliseconds to delay.
|
|
117
122
|
* @returns {Promise<void>} A promise that resolves after the delay.
|
|
118
123
|
*/
|
|
119
|
-
|
|
124
|
+
static delay(ms: number): Promise<void>;
|
|
120
125
|
}
|
|
121
126
|
//# sourceMappingURL=FullNodePeer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FullNodePeer.d.ts","sourceRoot":"","sources":["../../src/blockchain/FullNodePeer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FullNodePeer.d.ts","sourceRoot":"","sources":["../../src/blockchain/FullNodePeer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAO,MAAM,8BAA8B,CAAC;AAMzD,OAAO,SAAS,MAAM,YAAY,CAAC;AAGnC;;;GAGG;AACH,OAAO,QAAQ,8BAA8B,CAAC;IAC5C,UAAU,IAAI;QACZ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;KAC7D;CACF;AA6GD;;;GAGG;AACH,qBAAa,YAAY;IAEvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAGpD,OAAO,CAAC,UAAU,CAAkD;IAG7D,aAAa,YAAuD;IAGpE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAa;IAGpD,OAAO,CAAC,gBAAgB,CAAgB;IAGxC,OAAO,CAAC,SAAS,CAAoC;IAGrD,OAAO,CAAC,WAAW,CAAoD;IAGvE,OAAO,CAAC,cAAc,CAAgB;IAGtC,OAAO,CAAC,gBAAgB,CAAa;IAErC;;OAEG;IACH,OAAO;IAEP;;;OAGG;WACW,WAAW,IAAI,YAAY;IAOzC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYxC;;;;OAIG;WACiB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5C;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAuBvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAMxB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;;;OAIG;YACW,eAAe;IAoF7B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IASpB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;;OAGG;YACW,cAAc;IAW5B;;;OAGG;YACW,UAAU;IAKxB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAIlB;;;OAGG;IACU,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA0HzC;;;OAGG;IACI,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAwCpD;;;;OAIG;IACI,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI;IAS/C;;;;OAIG;WACiB,mBAAmB,CACrC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC;IAkCnB;;;;OAIG;WACW,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG/C"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// FullNodePeer.ts
|
|
2
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
5
|
};
|
|
@@ -14,7 +15,10 @@ const nanospinner_1 = require("nanospinner");
|
|
|
14
15
|
const config_1 = require("../utils/config");
|
|
15
16
|
const Environment_1 = require("../utils/Environment");
|
|
16
17
|
const node_cache_1 = __importDefault(require("node-cache"));
|
|
17
|
-
|
|
18
|
+
const bottleneck_1 = __importDefault(require("bottleneck"));
|
|
19
|
+
/**
|
|
20
|
+
* Constants defining configuration parameters.
|
|
21
|
+
*/
|
|
18
22
|
const FULLNODE_PORT = 8444;
|
|
19
23
|
const LOCALHOST = "127.0.0.1";
|
|
20
24
|
const CHIA_NODES_HOST = "chia-nodes";
|
|
@@ -29,13 +33,101 @@ const CACHE_DURATION = 30000; // in milliseconds
|
|
|
29
33
|
const COOLDOWN_DURATION = 300000; // 5 minutes in milliseconds
|
|
30
34
|
const MAX_PEERS_TO_FETCH = 5; // Maximum number of peers to fetch from DNS
|
|
31
35
|
const MAX_RETRIES = 3; // Maximum number of retry attempts
|
|
36
|
+
const MAX_REQUESTS_PER_MINUTE = 100; // Per-peer rate limit
|
|
37
|
+
/**
|
|
38
|
+
* Creates a proxy for the Peer instance to handle errors, retries, and rate limiting.
|
|
39
|
+
* @param peer - The original Peer instance.
|
|
40
|
+
* @param peerIP - The IP address of the peer.
|
|
41
|
+
* @param retryCount - The current retry attempt.
|
|
42
|
+
* @returns {Peer} - The proxied Peer instance.
|
|
43
|
+
*/
|
|
44
|
+
function createPeerProxy(peer, peerIP, retryCount = 0) {
|
|
45
|
+
// Initialize Bottleneck limiter for rate limiting
|
|
46
|
+
const limiter = new bottleneck_1.default({
|
|
47
|
+
maxConcurrent: 1, // One request at a time per peer
|
|
48
|
+
minTime: 60000 / MAX_REQUESTS_PER_MINUTE, // e.g., 600 ms between requests for 100 requests/min
|
|
49
|
+
});
|
|
50
|
+
return new Proxy(peer, {
|
|
51
|
+
get(target, prop, receiver) {
|
|
52
|
+
const originalMethod = target[prop];
|
|
53
|
+
if (typeof originalMethod === "function") {
|
|
54
|
+
return async (...args) => {
|
|
55
|
+
try {
|
|
56
|
+
// Schedule the method call with Bottleneck's limiter
|
|
57
|
+
const result = await limiter.schedule(() => originalMethod.apply(target, args));
|
|
58
|
+
// On successful operation, increase the weight slightly
|
|
59
|
+
const fullNodePeer = FullNodePeer.getInstance();
|
|
60
|
+
const currentWeight = fullNodePeer.peerWeights.get(peerIP) || 1;
|
|
61
|
+
fullNodePeer.peerWeights.set(peerIP, currentWeight + 0.1); // Increment weight
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`Peer ${peerIP} encountered an error: ${error.message}`);
|
|
66
|
+
// Check if the error is related to WebSocket or Operation timed out
|
|
67
|
+
if (error.message.includes("WebSocket") ||
|
|
68
|
+
error.message.includes("Operation timed out")) {
|
|
69
|
+
// Handle the disconnection and mark the peer accordingly
|
|
70
|
+
FullNodePeer.getInstance().handlePeerDisconnection(peerIP);
|
|
71
|
+
// If maximum retries reached, throw the error
|
|
72
|
+
if (retryCount >= MAX_RETRIES) {
|
|
73
|
+
console.error(`Max retries reached for method ${String(prop)} on peer ${peerIP}.`);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
// Attempt to select a new peer and retry the method
|
|
77
|
+
try {
|
|
78
|
+
console.info(`Selecting a new peer to retry method ${String(prop)}...`);
|
|
79
|
+
const newPeer = await FullNodePeer.getInstance().getBestPeer();
|
|
80
|
+
// Extract new peer's IP address
|
|
81
|
+
const newPeerIP = FullNodePeer.getInstance().extractPeerIP(newPeer);
|
|
82
|
+
if (!newPeerIP) {
|
|
83
|
+
throw new Error("Unable to extract IP from the new peer.");
|
|
84
|
+
}
|
|
85
|
+
// Wrap the new peer with a proxy, incrementing the retry count
|
|
86
|
+
const proxiedNewPeer = createPeerProxy(newPeer, newPeerIP, retryCount + 1);
|
|
87
|
+
// Retry the method on the new peer
|
|
88
|
+
return await proxiedNewPeer[prop](...args);
|
|
89
|
+
}
|
|
90
|
+
catch (retryError) {
|
|
91
|
+
console.error(`Retry failed on a new peer: ${retryError.message}`);
|
|
92
|
+
throw retryError;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// For other errors, handle normally
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return originalMethod;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
32
106
|
/**
|
|
33
107
|
* FullNodePeer manages connections to full nodes, prioritizing certain peers and handling reliability.
|
|
108
|
+
* It implements a singleton pattern to ensure a single instance throughout the application.
|
|
34
109
|
*/
|
|
35
110
|
class FullNodePeer {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Private constructor for singleton pattern.
|
|
113
|
+
*/
|
|
114
|
+
constructor() {
|
|
115
|
+
// Cached peer with timestamp
|
|
116
|
+
this.cachedPeer = null;
|
|
117
|
+
// Cooldown cache to exclude faulty peers temporarily
|
|
118
|
+
this.cooldownCache = new node_cache_1.default({ stdTTL: COOLDOWN_DURATION / 1000 });
|
|
119
|
+
// Peer reliability weights
|
|
120
|
+
this.peerWeights = new Map();
|
|
121
|
+
// List of prioritized peers
|
|
122
|
+
this.prioritizedPeers = [];
|
|
123
|
+
// Map to store PeerInfo
|
|
124
|
+
this.peerInfos = new Map();
|
|
125
|
+
// Cache for fetched peer IPs
|
|
126
|
+
this.peerIPCache = new node_cache_1.default({ stdTTL: CACHE_DURATION / 1000 });
|
|
127
|
+
// List of available peers for round-robin
|
|
128
|
+
this.availablePeers = [];
|
|
129
|
+
// Current index for round-robin selection
|
|
130
|
+
this.currentPeerIndex = 0;
|
|
39
131
|
}
|
|
40
132
|
/**
|
|
41
133
|
* Retrieves the singleton instance of FullNodePeer.
|
|
@@ -43,7 +135,7 @@ class FullNodePeer {
|
|
|
43
135
|
*/
|
|
44
136
|
static getInstance() {
|
|
45
137
|
if (!FullNodePeer.instance) {
|
|
46
|
-
FullNodePeer.instance = new FullNodePeer(
|
|
138
|
+
FullNodePeer.instance = new FullNodePeer();
|
|
47
139
|
}
|
|
48
140
|
return FullNodePeer.instance;
|
|
49
141
|
}
|
|
@@ -51,12 +143,11 @@ class FullNodePeer {
|
|
|
51
143
|
* Initializes the singleton instance by connecting to the best peer.
|
|
52
144
|
*/
|
|
53
145
|
async initialize() {
|
|
54
|
-
if (this.
|
|
146
|
+
if (this.cachedPeer)
|
|
55
147
|
return; // Already initialized
|
|
56
148
|
try {
|
|
57
|
-
const bestPeer = await
|
|
58
|
-
this.
|
|
59
|
-
FullNodePeer.instance = this; // Assign the initialized instance
|
|
149
|
+
const bestPeer = await this.getBestPeer();
|
|
150
|
+
this.cachedPeer = { peer: bestPeer, timestamp: Date.now() };
|
|
60
151
|
}
|
|
61
152
|
catch (error) {
|
|
62
153
|
console.error(`Initialization failed: ${error.message}`);
|
|
@@ -71,7 +162,7 @@ class FullNodePeer {
|
|
|
71
162
|
static async connect() {
|
|
72
163
|
const instance = FullNodePeer.getInstance();
|
|
73
164
|
await instance.initialize();
|
|
74
|
-
return instance.peer;
|
|
165
|
+
return instance.cachedPeer.peer;
|
|
75
166
|
}
|
|
76
167
|
/**
|
|
77
168
|
* Checks if a given port on a host is reachable.
|
|
@@ -80,7 +171,7 @@ class FullNodePeer {
|
|
|
80
171
|
* @param {number} timeout - Connection timeout in milliseconds.
|
|
81
172
|
* @returns {Promise<boolean>} Whether the port is reachable.
|
|
82
173
|
*/
|
|
83
|
-
|
|
174
|
+
isPortReachable(host, port, timeout = CONNECTION_TIMEOUT) {
|
|
84
175
|
return new Promise((resolve) => {
|
|
85
176
|
const socket = new net_1.default.Socket()
|
|
86
177
|
.setTimeout(timeout)
|
|
@@ -103,7 +194,7 @@ class FullNodePeer {
|
|
|
103
194
|
* @param {string} ip - The IP address to validate.
|
|
104
195
|
* @returns {boolean} Whether the IP address is valid.
|
|
105
196
|
*/
|
|
106
|
-
|
|
197
|
+
isValidIpAddress(ip) {
|
|
107
198
|
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/;
|
|
108
199
|
return ipv4Regex.test(ip);
|
|
109
200
|
}
|
|
@@ -111,9 +202,9 @@ class FullNodePeer {
|
|
|
111
202
|
* Retrieves the TRUSTED_FULLNODE IP from the environment and verifies its validity.
|
|
112
203
|
* @returns {string | null} The trusted full node IP or null if invalid.
|
|
113
204
|
*/
|
|
114
|
-
|
|
205
|
+
getTrustedFullNode() {
|
|
115
206
|
const trustedNodeIp = Environment_1.Environment.TRUSTED_FULLNODE || null;
|
|
116
|
-
if (trustedNodeIp &&
|
|
207
|
+
if (trustedNodeIp && this.isValidIpAddress(trustedNodeIp)) {
|
|
117
208
|
return trustedNodeIp;
|
|
118
209
|
}
|
|
119
210
|
return null;
|
|
@@ -123,39 +214,36 @@ class FullNodePeer {
|
|
|
123
214
|
* Utilizes caching to avoid redundant DNS resolutions.
|
|
124
215
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
125
216
|
*/
|
|
126
|
-
|
|
127
|
-
const trustedNodeIp =
|
|
217
|
+
async fetchNewPeerIPs() {
|
|
218
|
+
const trustedNodeIp = this.getTrustedFullNode();
|
|
128
219
|
const priorityIps = [];
|
|
129
220
|
// Define prioritized peers
|
|
130
|
-
|
|
131
|
-
...DNS_HOSTS, // Assuming CHIA_NODES_HOST is included in DNS_HOSTS
|
|
132
|
-
LOCALHOST,
|
|
133
|
-
];
|
|
221
|
+
this.prioritizedPeers = [CHIA_NODES_HOST, LOCALHOST];
|
|
134
222
|
// Add trustedNodeIp if available
|
|
135
223
|
if (trustedNodeIp) {
|
|
136
|
-
|
|
224
|
+
this.prioritizedPeers.unshift(trustedNodeIp);
|
|
137
225
|
}
|
|
138
226
|
// Prioritize trustedNodeIp
|
|
139
227
|
if (trustedNodeIp &&
|
|
140
|
-
!
|
|
141
|
-
(await
|
|
228
|
+
!this.cooldownCache.has(trustedNodeIp) &&
|
|
229
|
+
(await this.isPortReachable(trustedNodeIp, FULLNODE_PORT))) {
|
|
142
230
|
priorityIps.push(trustedNodeIp);
|
|
143
231
|
}
|
|
144
232
|
// Prioritize LOCALHOST
|
|
145
|
-
if (!
|
|
146
|
-
(await
|
|
233
|
+
if (!this.cooldownCache.has(LOCALHOST) &&
|
|
234
|
+
(await this.isPortReachable(LOCALHOST, FULLNODE_PORT))) {
|
|
147
235
|
priorityIps.push(LOCALHOST);
|
|
148
236
|
}
|
|
149
237
|
// Prioritize CHIA_NODES_HOST
|
|
150
|
-
if (!
|
|
151
|
-
(await
|
|
238
|
+
if (!this.cooldownCache.has(CHIA_NODES_HOST) &&
|
|
239
|
+
(await this.isPortReachable(CHIA_NODES_HOST, FULLNODE_PORT))) {
|
|
152
240
|
priorityIps.push(CHIA_NODES_HOST);
|
|
153
241
|
}
|
|
154
242
|
if (priorityIps.length > 0) {
|
|
155
243
|
return priorityIps;
|
|
156
244
|
}
|
|
157
245
|
// Check if cached peer IPs exist
|
|
158
|
-
const cachedPeerIPs =
|
|
246
|
+
const cachedPeerIPs = this.peerIPCache.get("peerIPs");
|
|
159
247
|
if (cachedPeerIPs) {
|
|
160
248
|
return cachedPeerIPs;
|
|
161
249
|
}
|
|
@@ -165,11 +253,11 @@ class FullNodePeer {
|
|
|
165
253
|
try {
|
|
166
254
|
const ips = await (0, promises_1.resolve4)(DNS_HOST);
|
|
167
255
|
if (ips && ips.length > 0) {
|
|
168
|
-
const shuffledIps =
|
|
256
|
+
const shuffledIps = this.shuffleArray(ips);
|
|
169
257
|
const reachableIps = [];
|
|
170
258
|
for (const ip of shuffledIps) {
|
|
171
|
-
if (!
|
|
172
|
-
(await
|
|
259
|
+
if (!this.cooldownCache.has(ip) &&
|
|
260
|
+
(await this.isPortReachable(ip, FULLNODE_PORT))) {
|
|
173
261
|
reachableIps.push(ip);
|
|
174
262
|
}
|
|
175
263
|
if (reachableIps.length === MAX_PEERS_TO_FETCH)
|
|
@@ -186,7 +274,7 @@ class FullNodePeer {
|
|
|
186
274
|
}
|
|
187
275
|
// Cache the fetched peer IPs
|
|
188
276
|
if (fetchedPeers.length > 0) {
|
|
189
|
-
|
|
277
|
+
this.peerIPCache.set("peerIPs", fetchedPeers);
|
|
190
278
|
return fetchedPeers;
|
|
191
279
|
}
|
|
192
280
|
throw new Error("No reachable IPs found in any DNS records.");
|
|
@@ -196,7 +284,7 @@ class FullNodePeer {
|
|
|
196
284
|
* @param {T[]} array - The array to shuffle.
|
|
197
285
|
* @returns {T[]} The shuffled array.
|
|
198
286
|
*/
|
|
199
|
-
|
|
287
|
+
shuffleArray(array) {
|
|
200
288
|
const shuffled = [...array];
|
|
201
289
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
202
290
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -209,66 +297,67 @@ class FullNodePeer {
|
|
|
209
297
|
* Assigns higher initial weights to prioritized peers.
|
|
210
298
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
211
299
|
*/
|
|
212
|
-
|
|
300
|
+
initializePeerWeights(peerIPs) {
|
|
213
301
|
for (const ip of peerIPs) {
|
|
214
|
-
if (!
|
|
302
|
+
if (!this.peerWeights.has(ip)) {
|
|
215
303
|
if (ip === CHIA_NODES_HOST ||
|
|
216
304
|
ip === LOCALHOST ||
|
|
217
|
-
ip ===
|
|
218
|
-
|
|
305
|
+
ip === this.getTrustedFullNode()) {
|
|
306
|
+
this.peerWeights.set(ip, 5); // Higher weight for prioritized peers
|
|
219
307
|
}
|
|
220
308
|
else {
|
|
221
|
-
|
|
309
|
+
this.peerWeights.set(ip, 1); // Default weight
|
|
222
310
|
}
|
|
223
311
|
}
|
|
224
312
|
}
|
|
225
313
|
}
|
|
226
314
|
/**
|
|
227
315
|
* Selects the next peer based on round-robin selection.
|
|
228
|
-
* @returns {
|
|
316
|
+
* @returns {Promise<Peer>} The selected Peer instance.
|
|
229
317
|
*/
|
|
230
|
-
|
|
231
|
-
if (
|
|
318
|
+
async selectNextPeer() {
|
|
319
|
+
if (this.availablePeers.length === 0) {
|
|
232
320
|
throw new Error("No available peers to select.");
|
|
233
321
|
}
|
|
234
|
-
const peerIP =
|
|
235
|
-
|
|
236
|
-
|
|
322
|
+
const peerIP = this.availablePeers[this.currentPeerIndex];
|
|
323
|
+
this.currentPeerIndex = (this.currentPeerIndex + 1) % this.availablePeers.length;
|
|
324
|
+
const peerInfo = this.peerInfos.get(peerIP);
|
|
325
|
+
return peerInfo.peer;
|
|
237
326
|
}
|
|
238
327
|
/**
|
|
239
328
|
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
240
329
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
241
330
|
*/
|
|
242
|
-
|
|
243
|
-
const peerIPs = await
|
|
331
|
+
async getPeerIPs() {
|
|
332
|
+
const peerIPs = await this.fetchNewPeerIPs();
|
|
244
333
|
return peerIPs;
|
|
245
334
|
}
|
|
246
335
|
/**
|
|
247
336
|
* Initializes the peer weights based on prioritization and reliability.
|
|
248
337
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
249
338
|
*/
|
|
250
|
-
|
|
251
|
-
|
|
339
|
+
setupPeers(peerIPs) {
|
|
340
|
+
this.initializePeerWeights(peerIPs);
|
|
252
341
|
}
|
|
253
342
|
/**
|
|
254
343
|
* Connects to the best available peer based on round-robin selection and reliability.
|
|
255
344
|
* @returns {Promise<Peer>} The connected Peer instance.
|
|
256
345
|
*/
|
|
257
|
-
|
|
346
|
+
async getBestPeer() {
|
|
258
347
|
const now = Date.now();
|
|
259
348
|
// Refresh cachedPeer if expired or disconnected
|
|
260
|
-
if (
|
|
261
|
-
now -
|
|
262
|
-
|
|
263
|
-
return
|
|
349
|
+
if (this.cachedPeer &&
|
|
350
|
+
now - this.cachedPeer.timestamp < CACHE_DURATION &&
|
|
351
|
+
this.peerInfos.get(this.extractPeerIP(this.cachedPeer.peer) || "")?.isConnected) {
|
|
352
|
+
return this.cachedPeer.peer;
|
|
264
353
|
}
|
|
265
354
|
// Fetch peer IPs
|
|
266
|
-
const peerIPs = await
|
|
355
|
+
const peerIPs = await this.getPeerIPs();
|
|
267
356
|
// Setup peer weights with prioritization
|
|
268
|
-
|
|
357
|
+
this.setupPeers(peerIPs);
|
|
269
358
|
// Initialize or update peerInfos and availablePeers
|
|
270
359
|
for (const ip of peerIPs) {
|
|
271
|
-
if (!
|
|
360
|
+
if (!this.peerInfos.has(ip)) {
|
|
272
361
|
// Attempt to create a peer connection
|
|
273
362
|
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
274
363
|
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
@@ -284,31 +373,32 @@ class FullNodePeer {
|
|
|
284
373
|
catch (error) {
|
|
285
374
|
console.error(`Failed to create peer for IP ${ip}: ${error.message}`);
|
|
286
375
|
// Add to cooldown
|
|
287
|
-
|
|
376
|
+
this.cooldownCache.set(ip, true);
|
|
288
377
|
// Decrease weight or remove peer
|
|
289
|
-
const currentWeight =
|
|
378
|
+
const currentWeight = this.peerWeights.get(ip) || 1;
|
|
290
379
|
if (currentWeight > 1) {
|
|
291
|
-
|
|
380
|
+
this.peerWeights.set(ip, currentWeight - 1);
|
|
292
381
|
}
|
|
293
382
|
else {
|
|
294
|
-
|
|
383
|
+
this.peerWeights.delete(ip);
|
|
295
384
|
}
|
|
296
385
|
continue; // Skip adding this peer
|
|
297
386
|
}
|
|
298
387
|
// Wrap the peer with proxy to handle errors and retries
|
|
299
|
-
const proxiedPeer =
|
|
388
|
+
const proxiedPeer = createPeerProxy(peer, ip);
|
|
300
389
|
// Store PeerInfo
|
|
301
|
-
|
|
390
|
+
this.peerInfos.set(ip, {
|
|
302
391
|
peer: proxiedPeer,
|
|
303
|
-
weight:
|
|
392
|
+
weight: this.peerWeights.get(ip) || 1,
|
|
304
393
|
address: ip,
|
|
305
394
|
isConnected: true, // Mark as connected
|
|
395
|
+
limiter: proxiedPeer.limiter, // Assign the limiter from Proxy
|
|
306
396
|
});
|
|
307
397
|
// Add to availablePeers
|
|
308
|
-
|
|
398
|
+
this.availablePeers.push(ip);
|
|
309
399
|
}
|
|
310
400
|
else {
|
|
311
|
-
const peerInfo =
|
|
401
|
+
const peerInfo = this.peerInfos.get(ip);
|
|
312
402
|
if (!peerInfo.isConnected) {
|
|
313
403
|
// Peer is back from cooldown, re-establish connection
|
|
314
404
|
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
@@ -325,149 +415,71 @@ class FullNodePeer {
|
|
|
325
415
|
catch (error) {
|
|
326
416
|
console.error(`Failed to reconnect peer for IP ${ip}: ${error.message}`);
|
|
327
417
|
// Re-add to cooldown
|
|
328
|
-
|
|
418
|
+
this.cooldownCache.set(ip, true);
|
|
329
419
|
// Decrease weight or remove peer
|
|
330
|
-
const currentWeight =
|
|
420
|
+
const currentWeight = this.peerWeights.get(ip) || 1;
|
|
331
421
|
if (currentWeight > 1) {
|
|
332
|
-
|
|
422
|
+
this.peerWeights.set(ip, currentWeight - 1);
|
|
333
423
|
}
|
|
334
424
|
else {
|
|
335
|
-
|
|
425
|
+
this.peerWeights.delete(ip);
|
|
336
426
|
}
|
|
337
427
|
continue; // Skip adding this peer
|
|
338
428
|
}
|
|
339
429
|
// Wrap the peer with proxy to handle errors and retries
|
|
340
|
-
const proxiedPeer =
|
|
430
|
+
const proxiedPeer = createPeerProxy(peer, ip);
|
|
341
431
|
// Update PeerInfo
|
|
342
432
|
peerInfo.peer = proxiedPeer;
|
|
343
433
|
peerInfo.isConnected = true;
|
|
344
434
|
// Add back to availablePeers
|
|
345
|
-
|
|
435
|
+
this.availablePeers.push(ip);
|
|
346
436
|
}
|
|
347
437
|
}
|
|
348
438
|
}
|
|
349
|
-
if (
|
|
439
|
+
if (this.availablePeers.length === 0) {
|
|
350
440
|
throw new Error("No available peers to connect.");
|
|
351
441
|
}
|
|
352
442
|
// Select the next peer in round-robin
|
|
353
|
-
const
|
|
354
|
-
const selectedPeerInfo = FullNodePeer.peerInfos.get(selectedPeerIP);
|
|
443
|
+
const selectedPeer = await this.selectNextPeer();
|
|
355
444
|
// Cache the peer
|
|
356
|
-
|
|
357
|
-
console.log(`Using Fullnode Peer: ${
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Creates a proxy for the peer to handle errors, implement retries, and enforce round-robin selection.
|
|
362
|
-
* @param {Peer} peer - The Peer instance.
|
|
363
|
-
* @param {string} peerIP - The IP address of the peer.
|
|
364
|
-
* @param {number} [retryCount=0] - The current retry attempt.
|
|
365
|
-
* @returns {Peer} The proxied Peer instance.
|
|
366
|
-
*/
|
|
367
|
-
static createPeerProxy(peer, peerIP, retryCount = 0) {
|
|
368
|
-
// Listen for close events if the Peer class supports it
|
|
369
|
-
// This assumes that the Peer class emits a 'close' event when the connection is closed
|
|
370
|
-
// Adjust accordingly based on the actual Peer implementation
|
|
371
|
-
if (typeof peer.on === "function") {
|
|
372
|
-
peer.on("close", () => {
|
|
373
|
-
console.warn(`Peer ${peerIP} connection closed.`);
|
|
374
|
-
FullNodePeer.handlePeerDisconnection(peerIP);
|
|
375
|
-
});
|
|
376
|
-
peer.on("error", (error) => {
|
|
377
|
-
console.error(`Peer ${peerIP} encountered an error: ${error.message}`);
|
|
378
|
-
FullNodePeer.handlePeerDisconnection(peerIP);
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
return new Proxy(peer, {
|
|
382
|
-
get: (target, prop) => {
|
|
383
|
-
const originalMethod = target[prop];
|
|
384
|
-
if (typeof originalMethod === "function") {
|
|
385
|
-
return (...args) => {
|
|
386
|
-
// Select the next peer in round-robin
|
|
387
|
-
let selectedPeerIP;
|
|
388
|
-
try {
|
|
389
|
-
selectedPeerIP = FullNodePeer.getNextPeerIP();
|
|
390
|
-
}
|
|
391
|
-
catch (error) {
|
|
392
|
-
return Promise.reject(error);
|
|
393
|
-
}
|
|
394
|
-
const selectedPeerInfo = FullNodePeer.peerInfos.get(selectedPeerIP);
|
|
395
|
-
if (!selectedPeerInfo || !selectedPeerInfo.isConnected) {
|
|
396
|
-
return Promise.reject(new Error(`Peer ${selectedPeerIP} is disconnected.`));
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
// Bind the original method to the peer instance to preserve 'this'
|
|
400
|
-
const boundMethod = originalMethod.bind(selectedPeerInfo.peer);
|
|
401
|
-
const result = boundMethod(...args);
|
|
402
|
-
return result;
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
console.error(`Peer ${selectedPeerIP} encountered an error: ${error.message}`);
|
|
406
|
-
// Handle disconnection and retries
|
|
407
|
-
FullNodePeer.handlePeerDisconnection(selectedPeerIP);
|
|
408
|
-
// If maximum retries reached, throw the error
|
|
409
|
-
if (retryCount >= MAX_RETRIES) {
|
|
410
|
-
console.error(`Max retries reached for method ${String(prop)} on peer ${selectedPeerIP}.`);
|
|
411
|
-
return Promise.reject(error);
|
|
412
|
-
}
|
|
413
|
-
// Attempt to select a new peer and retry the method
|
|
414
|
-
return FullNodePeer.getBestPeer()
|
|
415
|
-
.then((newPeer) => {
|
|
416
|
-
// Extract new peer's IP address
|
|
417
|
-
const newPeerIP = FullNodePeer.extractPeerIP(newPeer);
|
|
418
|
-
if (!newPeerIP) {
|
|
419
|
-
throw new Error("Unable to extract IP from the new peer.");
|
|
420
|
-
}
|
|
421
|
-
// Wrap the new peer with a proxy, incrementing the retry count
|
|
422
|
-
const proxiedNewPeer = FullNodePeer.createPeerProxy(newPeer, newPeerIP, retryCount + 1);
|
|
423
|
-
// Retry the method on the new peer
|
|
424
|
-
return proxiedNewPeer[prop](...args);
|
|
425
|
-
})
|
|
426
|
-
.catch((retryError) => {
|
|
427
|
-
console.error(`Retry failed on a new peer: ${retryError.message}`);
|
|
428
|
-
return Promise.reject(retryError);
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
return originalMethod;
|
|
434
|
-
},
|
|
435
|
-
});
|
|
445
|
+
this.cachedPeer = { peer: selectedPeer, timestamp: now };
|
|
446
|
+
console.log(`Using Fullnode Peer: ${this.extractPeerIP(selectedPeer)}`);
|
|
447
|
+
return selectedPeer;
|
|
436
448
|
}
|
|
437
449
|
/**
|
|
438
450
|
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
439
451
|
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
440
452
|
*/
|
|
441
|
-
|
|
453
|
+
handlePeerDisconnection(peerIP) {
|
|
442
454
|
// Mark the peer in cooldown
|
|
443
|
-
|
|
455
|
+
this.cooldownCache.set(peerIP, true);
|
|
444
456
|
// Decrease weight or remove peer
|
|
445
|
-
const currentWeight =
|
|
457
|
+
const currentWeight = this.peerWeights.get(peerIP) || 1;
|
|
446
458
|
if (currentWeight > 1) {
|
|
447
|
-
|
|
459
|
+
this.peerWeights.set(peerIP, currentWeight - 1);
|
|
448
460
|
}
|
|
449
461
|
else {
|
|
450
|
-
|
|
462
|
+
this.peerWeights.delete(peerIP);
|
|
451
463
|
}
|
|
452
464
|
// Update the peer's connection status
|
|
453
|
-
const peerInfo =
|
|
465
|
+
const peerInfo = this.peerInfos.get(peerIP);
|
|
454
466
|
if (peerInfo) {
|
|
455
467
|
peerInfo.isConnected = false;
|
|
456
|
-
|
|
468
|
+
this.peerInfos.set(peerIP, peerInfo);
|
|
457
469
|
}
|
|
458
470
|
// Remove from availablePeers if present
|
|
459
|
-
const index =
|
|
471
|
+
const index = this.availablePeers.indexOf(peerIP);
|
|
460
472
|
if (index !== -1) {
|
|
461
|
-
|
|
473
|
+
this.availablePeers.splice(index, 1);
|
|
462
474
|
// Adjust currentPeerIndex if necessary
|
|
463
|
-
if (
|
|
464
|
-
|
|
475
|
+
if (this.currentPeerIndex >= this.availablePeers.length) {
|
|
476
|
+
this.currentPeerIndex = 0;
|
|
465
477
|
}
|
|
466
478
|
}
|
|
467
479
|
// If the disconnected peer was the cached peer, invalidate the cache
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
480
|
+
if (this.cachedPeer &&
|
|
481
|
+
this.extractPeerIP(this.cachedPeer.peer) === peerIP) {
|
|
482
|
+
this.cachedPeer = null;
|
|
471
483
|
}
|
|
472
484
|
console.warn(`Peer ${peerIP} has been marked as disconnected and is in cooldown.`);
|
|
473
485
|
}
|
|
@@ -476,8 +488,8 @@ class FullNodePeer {
|
|
|
476
488
|
* @param {Peer} peer - The Peer instance.
|
|
477
489
|
* @returns {string | null} The extracted IP address or null if not found.
|
|
478
490
|
*/
|
|
479
|
-
|
|
480
|
-
for (const [ip, info] of
|
|
491
|
+
extractPeerIP(peer) {
|
|
492
|
+
for (const [ip, info] of this.peerInfos.entries()) {
|
|
481
493
|
if (info.peer === peer) {
|
|
482
494
|
return ip;
|
|
483
495
|
}
|
|
@@ -528,19 +540,3 @@ class FullNodePeer {
|
|
|
528
540
|
exports.FullNodePeer = FullNodePeer;
|
|
529
541
|
// Singleton instance
|
|
530
542
|
FullNodePeer.instance = null;
|
|
531
|
-
// Cached peer with timestamp
|
|
532
|
-
FullNodePeer.cachedPeer = null;
|
|
533
|
-
// Cooldown cache to exclude faulty peers temporarily
|
|
534
|
-
FullNodePeer.cooldownCache = new node_cache_1.default({ stdTTL: COOLDOWN_DURATION / 1000 });
|
|
535
|
-
// Peer reliability weights
|
|
536
|
-
FullNodePeer.peerWeights = new Map();
|
|
537
|
-
// List of prioritized peers
|
|
538
|
-
FullNodePeer.prioritizedPeers = [];
|
|
539
|
-
// Map to store PeerInfo
|
|
540
|
-
FullNodePeer.peerInfos = new Map();
|
|
541
|
-
// Cache for fetched peer IPs
|
|
542
|
-
FullNodePeer.peerIPCache = new node_cache_1.default({ stdTTL: CACHE_DURATION / 1000 });
|
|
543
|
-
// List of available peers for round-robin
|
|
544
|
-
FullNodePeer.availablePeers = [];
|
|
545
|
-
// Current index for round-robin selection
|
|
546
|
-
FullNodePeer.currentPeerIndex = 0;
|