@dignetwork/dig-sdk 0.0.1-alpha.141 → 0.0.1-alpha.143
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/dist/DigNetwork/DigNetwork.d.ts.map +1 -1
- package/dist/DigNetwork/DigNetwork.js +1 -0
- package/dist/blockchain/FullNodePeer.d.ts +23 -42
- package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.js +154 -268
- package/dist/utils/PeerRanker.d.ts +69 -0
- package/dist/utils/PeerRanker.d.ts.map +1 -0
- package/dist/utils/PeerRanker.js +173 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DigNetwork.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/DigNetwork.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,MAAM,CAAC,cAAc,CAAmC;gBAEpD,OAAO,EAAE,MAAM;WAOP,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;WAQhD,aAAa,CAAC,GAAG,EAAE,MAAM;WAIzB,oBAAoB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,MAAM,EACZ,gBAAgB,GAAE,MAAM,EAAO,GAC9B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"DigNetwork.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/DigNetwork.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,MAAM,CAAC,cAAc,CAAmC;gBAEpD,OAAO,EAAE,MAAM;WAOP,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;WAQhD,aAAa,CAAC,GAAG,EAAE,MAAM;WAIzB,oBAAoB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,MAAM,EACZ,gBAAgB,GAAE,MAAM,EAAO,GAC9B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;WA2EZ,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;WAOrC,mBAAmB,CACrC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAcH,kBAAkB,CAC7B,eAAe,CAAC,EAAE,OAAO,EACzB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC;IA8GH,mBAAmB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;CAUvD"}
|
|
@@ -50,6 +50,7 @@ class DigNetwork {
|
|
|
50
50
|
static async findPeerWithStoreKey(storeId, rootHash, key, initialBlackList = []) {
|
|
51
51
|
const peerBlackList = initialBlackList;
|
|
52
52
|
const serverCoin = new blockchain_1.ServerCoin(storeId);
|
|
53
|
+
const allPeers = await serverCoin.getActiveEpochPeers();
|
|
53
54
|
while (true) {
|
|
54
55
|
try {
|
|
55
56
|
// Sample 10 peers from the current epoch
|
|
@@ -1,31 +1,17 @@
|
|
|
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
|
-
}
|
|
12
2
|
/**
|
|
13
3
|
* 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.
|
|
15
4
|
*/
|
|
16
5
|
export declare class FullNodePeer {
|
|
6
|
+
private peer;
|
|
17
7
|
private static instance;
|
|
18
|
-
private cachedPeer;
|
|
19
|
-
cooldownCache
|
|
20
|
-
|
|
21
|
-
private prioritizedPeers;
|
|
22
|
-
private peerInfos;
|
|
23
|
-
private peerIPCache;
|
|
24
|
-
private
|
|
25
|
-
private currentPeerIndex;
|
|
26
|
-
/**
|
|
27
|
-
* Private constructor for singleton pattern.
|
|
28
|
-
*/
|
|
8
|
+
private static cachedPeer;
|
|
9
|
+
private static cooldownCache;
|
|
10
|
+
private static peerWeights;
|
|
11
|
+
private static prioritizedPeers;
|
|
12
|
+
private static peerInfos;
|
|
13
|
+
private static peerIPCache;
|
|
14
|
+
private static peerLimiters;
|
|
29
15
|
private constructor();
|
|
30
16
|
/**
|
|
31
17
|
* Retrieves the singleton instance of FullNodePeer.
|
|
@@ -49,56 +35,57 @@ export declare class FullNodePeer {
|
|
|
49
35
|
* @param {number} timeout - Connection timeout in milliseconds.
|
|
50
36
|
* @returns {Promise<boolean>} Whether the port is reachable.
|
|
51
37
|
*/
|
|
52
|
-
private isPortReachable;
|
|
38
|
+
private static isPortReachable;
|
|
53
39
|
/**
|
|
54
40
|
* Validates an IPv4 address.
|
|
55
41
|
* @param {string} ip - The IP address to validate.
|
|
56
42
|
* @returns {boolean} Whether the IP address is valid.
|
|
57
43
|
*/
|
|
58
|
-
private isValidIpAddress;
|
|
44
|
+
private static isValidIpAddress;
|
|
59
45
|
/**
|
|
60
46
|
* Retrieves the TRUSTED_FULLNODE IP from the environment and verifies its validity.
|
|
61
47
|
* @returns {string | null} The trusted full node IP or null if invalid.
|
|
62
48
|
*/
|
|
63
|
-
private getTrustedFullNode;
|
|
49
|
+
private static getTrustedFullNode;
|
|
64
50
|
/**
|
|
65
51
|
* Fetches new peer IPs from DNS introducers and prioritized hosts.
|
|
66
52
|
* Utilizes caching to avoid redundant DNS resolutions.
|
|
67
53
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
68
54
|
*/
|
|
69
|
-
private fetchNewPeerIPs;
|
|
55
|
+
private static fetchNewPeerIPs;
|
|
70
56
|
/**
|
|
71
57
|
* Shuffles an array using the Fisher-Yates algorithm.
|
|
72
58
|
* @param {T[]} array - The array to shuffle.
|
|
73
59
|
* @returns {T[]} The shuffled array.
|
|
74
60
|
*/
|
|
75
|
-
private shuffleArray;
|
|
61
|
+
private static shuffleArray;
|
|
76
62
|
/**
|
|
77
63
|
* Initializes the peer weights map.
|
|
78
64
|
* Assigns higher initial weights to prioritized peers.
|
|
79
65
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
80
66
|
*/
|
|
81
|
-
private initializePeerWeights;
|
|
67
|
+
private static initializePeerWeights;
|
|
82
68
|
/**
|
|
83
|
-
* Selects
|
|
84
|
-
*
|
|
69
|
+
* Selects a peer based on weighted random selection.
|
|
70
|
+
* Prioritized peers have higher weights.
|
|
71
|
+
* @returns {string} The selected peer IP.
|
|
85
72
|
*/
|
|
86
|
-
private
|
|
73
|
+
private static selectPeerByWeight;
|
|
87
74
|
/**
|
|
88
75
|
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
89
76
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
90
77
|
*/
|
|
91
|
-
private getPeerIPs;
|
|
78
|
+
private static getPeerIPs;
|
|
92
79
|
/**
|
|
93
80
|
* Initializes the peer weights based on prioritization and reliability.
|
|
94
81
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
95
82
|
*/
|
|
96
|
-
private setupPeers;
|
|
83
|
+
private static setupPeers;
|
|
97
84
|
/**
|
|
98
|
-
* Connects to the best available peer based on
|
|
85
|
+
* Connects to the best available peer based on weighted selection and reliability.
|
|
99
86
|
* @returns {Promise<Peer>} The connected Peer instance.
|
|
100
87
|
*/
|
|
101
|
-
getBestPeer
|
|
88
|
+
private static getBestPeer;
|
|
102
89
|
/**
|
|
103
90
|
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
104
91
|
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
@@ -109,18 +96,12 @@ export declare class FullNodePeer {
|
|
|
109
96
|
* @param {Peer} peer - The Peer instance.
|
|
110
97
|
* @returns {string | null} The extracted IP address or null if not found.
|
|
111
98
|
*/
|
|
112
|
-
|
|
99
|
+
private static extractPeerIP;
|
|
113
100
|
/**
|
|
114
101
|
* Waits for a coin to be confirmed (spent) on the blockchain.
|
|
115
102
|
* @param {Buffer} parentCoinInfo - The parent coin information.
|
|
116
103
|
* @returns {Promise<boolean>} Whether the coin was confirmed.
|
|
117
104
|
*/
|
|
118
105
|
static waitForConfirmation(parentCoinInfo: Buffer): Promise<boolean>;
|
|
119
|
-
/**
|
|
120
|
-
* Delays execution for a specified amount of time.
|
|
121
|
-
* @param {number} ms - Milliseconds to delay.
|
|
122
|
-
* @returns {Promise<void>} A promise that resolves after the delay.
|
|
123
|
-
*/
|
|
124
|
-
static delay(ms: number): Promise<void>;
|
|
125
106
|
}
|
|
126
107
|
//# sourceMappingURL=FullNodePeer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FullNodePeer.d.ts","sourceRoot":"","sources":["../../src/blockchain/FullNodePeer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAO,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"FullNodePeer.d.ts","sourceRoot":"","sources":["../../src/blockchain/FullNodePeer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAO,MAAM,8BAA8B,CAAC;AAmCzD;;GAEG;AACH,qBAAa,YAAY;IA0BH,OAAO,CAAC,IAAI;IAxBhC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAGpD,OAAO,CAAC,MAAM,CAAC,UAAU,CAAkD;IAG3E,OAAO,CAAC,MAAM,CAAC,aAAa,CAAuD;IAGnF,OAAO,CAAC,MAAM,CAAC,WAAW,CAAkC;IAG5D,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAgB;IAG/C,OAAO,CAAC,MAAM,CAAC,SAAS,CAAoC;IAG5D,OAAO,CAAC,MAAM,CAAC,WAAW,CAAoD;IAG9E,OAAO,CAAC,MAAM,CAAC,YAAY,CAAsC;IAGjE,OAAO;IAEP;;;OAGG;WACW,WAAW,IAAI,YAAY;IAOzC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAaxC;;;;OAIG;WACiB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5C;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAuB9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAM/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IASjC;;;;OAIG;mBACkB,eAAe;IAoFpC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IAS3B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAgBpC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAwBjC;;;OAGG;mBACkB,UAAU;IAK/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAIzB;;;OAGG;mBACkB,WAAW;IA+EhC;;;OAGG;IACI,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAqBpD;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAS5B;;;;OAIG;WACiB,mBAAmB,CACrC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC;CAkDpB"}
|
|
@@ -16,9 +16,7 @@ const config_1 = require("../utils/config");
|
|
|
16
16
|
const Environment_1 = require("../utils/Environment");
|
|
17
17
|
const node_cache_1 = __importDefault(require("node-cache"));
|
|
18
18
|
const bottleneck_1 = __importDefault(require("bottleneck"));
|
|
19
|
-
|
|
20
|
-
* Constants defining configuration parameters.
|
|
21
|
-
*/
|
|
19
|
+
// Constants
|
|
22
20
|
const FULLNODE_PORT = 8444;
|
|
23
21
|
const LOCALHOST = "127.0.0.1";
|
|
24
22
|
const CHIA_NODES_HOST = "chia-nodes";
|
|
@@ -30,104 +28,17 @@ const DNS_HOSTS = [
|
|
|
30
28
|
];
|
|
31
29
|
const CONNECTION_TIMEOUT = 2000; // in milliseconds
|
|
32
30
|
const CACHE_DURATION = 30000; // in milliseconds
|
|
33
|
-
const COOLDOWN_DURATION =
|
|
31
|
+
const COOLDOWN_DURATION = 60000; // in milliseconds
|
|
34
32
|
const MAX_PEERS_TO_FETCH = 5; // Maximum number of peers to fetch from DNS
|
|
35
33
|
const MAX_RETRIES = 3; // Maximum number of retry attempts
|
|
36
34
|
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
|
-
}
|
|
106
35
|
/**
|
|
107
36
|
* 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.
|
|
109
37
|
*/
|
|
110
38
|
class FullNodePeer {
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
// Private constructor for singleton pattern
|
|
40
|
+
constructor(peer) {
|
|
41
|
+
this.peer = peer;
|
|
131
42
|
}
|
|
132
43
|
/**
|
|
133
44
|
* Retrieves the singleton instance of FullNodePeer.
|
|
@@ -135,7 +46,7 @@ class FullNodePeer {
|
|
|
135
46
|
*/
|
|
136
47
|
static getInstance() {
|
|
137
48
|
if (!FullNodePeer.instance) {
|
|
138
|
-
FullNodePeer.instance = new FullNodePeer();
|
|
49
|
+
FullNodePeer.instance = new FullNodePeer(null); // Temporarily set to null
|
|
139
50
|
}
|
|
140
51
|
return FullNodePeer.instance;
|
|
141
52
|
}
|
|
@@ -143,11 +54,12 @@ class FullNodePeer {
|
|
|
143
54
|
* Initializes the singleton instance by connecting to the best peer.
|
|
144
55
|
*/
|
|
145
56
|
async initialize() {
|
|
146
|
-
if (this.
|
|
57
|
+
if (this.peer)
|
|
147
58
|
return; // Already initialized
|
|
148
59
|
try {
|
|
149
|
-
const bestPeer = await
|
|
150
|
-
this.
|
|
60
|
+
const bestPeer = await FullNodePeer.getBestPeer();
|
|
61
|
+
this.peer = bestPeer;
|
|
62
|
+
FullNodePeer.instance = this; // Assign the initialized instance
|
|
151
63
|
}
|
|
152
64
|
catch (error) {
|
|
153
65
|
console.error(`Initialization failed: ${error.message}`);
|
|
@@ -162,7 +74,7 @@ class FullNodePeer {
|
|
|
162
74
|
static async connect() {
|
|
163
75
|
const instance = FullNodePeer.getInstance();
|
|
164
76
|
await instance.initialize();
|
|
165
|
-
return instance.
|
|
77
|
+
return instance.peer;
|
|
166
78
|
}
|
|
167
79
|
/**
|
|
168
80
|
* Checks if a given port on a host is reachable.
|
|
@@ -171,7 +83,7 @@ class FullNodePeer {
|
|
|
171
83
|
* @param {number} timeout - Connection timeout in milliseconds.
|
|
172
84
|
* @returns {Promise<boolean>} Whether the port is reachable.
|
|
173
85
|
*/
|
|
174
|
-
isPortReachable(host, port, timeout = CONNECTION_TIMEOUT) {
|
|
86
|
+
static isPortReachable(host, port, timeout = CONNECTION_TIMEOUT) {
|
|
175
87
|
return new Promise((resolve) => {
|
|
176
88
|
const socket = new net_1.default.Socket()
|
|
177
89
|
.setTimeout(timeout)
|
|
@@ -194,7 +106,7 @@ class FullNodePeer {
|
|
|
194
106
|
* @param {string} ip - The IP address to validate.
|
|
195
107
|
* @returns {boolean} Whether the IP address is valid.
|
|
196
108
|
*/
|
|
197
|
-
isValidIpAddress(ip) {
|
|
109
|
+
static isValidIpAddress(ip) {
|
|
198
110
|
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}$/;
|
|
199
111
|
return ipv4Regex.test(ip);
|
|
200
112
|
}
|
|
@@ -202,9 +114,9 @@ class FullNodePeer {
|
|
|
202
114
|
* Retrieves the TRUSTED_FULLNODE IP from the environment and verifies its validity.
|
|
203
115
|
* @returns {string | null} The trusted full node IP or null if invalid.
|
|
204
116
|
*/
|
|
205
|
-
getTrustedFullNode() {
|
|
117
|
+
static getTrustedFullNode() {
|
|
206
118
|
const trustedNodeIp = Environment_1.Environment.TRUSTED_FULLNODE || null;
|
|
207
|
-
if (trustedNodeIp &&
|
|
119
|
+
if (trustedNodeIp && FullNodePeer.isValidIpAddress(trustedNodeIp)) {
|
|
208
120
|
return trustedNodeIp;
|
|
209
121
|
}
|
|
210
122
|
return null;
|
|
@@ -214,36 +126,36 @@ class FullNodePeer {
|
|
|
214
126
|
* Utilizes caching to avoid redundant DNS resolutions.
|
|
215
127
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
216
128
|
*/
|
|
217
|
-
async fetchNewPeerIPs() {
|
|
218
|
-
const trustedNodeIp =
|
|
129
|
+
static async fetchNewPeerIPs() {
|
|
130
|
+
const trustedNodeIp = FullNodePeer.getTrustedFullNode();
|
|
219
131
|
const priorityIps = [];
|
|
220
132
|
// Define prioritized peers
|
|
221
|
-
|
|
133
|
+
FullNodePeer.prioritizedPeers = [CHIA_NODES_HOST, LOCALHOST];
|
|
222
134
|
// Add trustedNodeIp if available
|
|
223
135
|
if (trustedNodeIp) {
|
|
224
|
-
|
|
136
|
+
FullNodePeer.prioritizedPeers.unshift(trustedNodeIp);
|
|
225
137
|
}
|
|
226
138
|
// Prioritize trustedNodeIp
|
|
227
139
|
if (trustedNodeIp &&
|
|
228
|
-
!
|
|
229
|
-
(await
|
|
140
|
+
!FullNodePeer.cooldownCache.has(trustedNodeIp) &&
|
|
141
|
+
(await FullNodePeer.isPortReachable(trustedNodeIp, FULLNODE_PORT))) {
|
|
230
142
|
priorityIps.push(trustedNodeIp);
|
|
231
143
|
}
|
|
232
144
|
// Prioritize LOCALHOST
|
|
233
|
-
if (!
|
|
234
|
-
(await
|
|
145
|
+
if (!FullNodePeer.cooldownCache.has(LOCALHOST) &&
|
|
146
|
+
(await FullNodePeer.isPortReachable(LOCALHOST, FULLNODE_PORT))) {
|
|
235
147
|
priorityIps.push(LOCALHOST);
|
|
236
148
|
}
|
|
237
149
|
// Prioritize CHIA_NODES_HOST
|
|
238
|
-
if (!
|
|
239
|
-
(await
|
|
150
|
+
if (!FullNodePeer.cooldownCache.has(CHIA_NODES_HOST) &&
|
|
151
|
+
(await FullNodePeer.isPortReachable(CHIA_NODES_HOST, FULLNODE_PORT))) {
|
|
240
152
|
priorityIps.push(CHIA_NODES_HOST);
|
|
241
153
|
}
|
|
242
154
|
if (priorityIps.length > 0) {
|
|
243
155
|
return priorityIps;
|
|
244
156
|
}
|
|
245
157
|
// Check if cached peer IPs exist
|
|
246
|
-
const cachedPeerIPs =
|
|
158
|
+
const cachedPeerIPs = FullNodePeer.peerIPCache.get("peerIPs");
|
|
247
159
|
if (cachedPeerIPs) {
|
|
248
160
|
return cachedPeerIPs;
|
|
249
161
|
}
|
|
@@ -253,11 +165,11 @@ class FullNodePeer {
|
|
|
253
165
|
try {
|
|
254
166
|
const ips = await (0, promises_1.resolve4)(DNS_HOST);
|
|
255
167
|
if (ips && ips.length > 0) {
|
|
256
|
-
const shuffledIps =
|
|
168
|
+
const shuffledIps = FullNodePeer.shuffleArray(ips);
|
|
257
169
|
const reachableIps = [];
|
|
258
170
|
for (const ip of shuffledIps) {
|
|
259
|
-
if (!
|
|
260
|
-
(await
|
|
171
|
+
if (!FullNodePeer.cooldownCache.has(ip) &&
|
|
172
|
+
(await FullNodePeer.isPortReachable(ip, FULLNODE_PORT))) {
|
|
261
173
|
reachableIps.push(ip);
|
|
262
174
|
}
|
|
263
175
|
if (reachableIps.length === MAX_PEERS_TO_FETCH)
|
|
@@ -274,7 +186,7 @@ class FullNodePeer {
|
|
|
274
186
|
}
|
|
275
187
|
// Cache the fetched peer IPs
|
|
276
188
|
if (fetchedPeers.length > 0) {
|
|
277
|
-
|
|
189
|
+
FullNodePeer.peerIPCache.set("peerIPs", fetchedPeers);
|
|
278
190
|
return fetchedPeers;
|
|
279
191
|
}
|
|
280
192
|
throw new Error("No reachable IPs found in any DNS records.");
|
|
@@ -284,7 +196,7 @@ class FullNodePeer {
|
|
|
284
196
|
* @param {T[]} array - The array to shuffle.
|
|
285
197
|
* @returns {T[]} The shuffled array.
|
|
286
198
|
*/
|
|
287
|
-
shuffleArray(array) {
|
|
199
|
+
static shuffleArray(array) {
|
|
288
200
|
const shuffled = [...array];
|
|
289
201
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
290
202
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -297,190 +209,145 @@ class FullNodePeer {
|
|
|
297
209
|
* Assigns higher initial weights to prioritized peers.
|
|
298
210
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
299
211
|
*/
|
|
300
|
-
initializePeerWeights(peerIPs) {
|
|
212
|
+
static initializePeerWeights(peerIPs) {
|
|
301
213
|
for (const ip of peerIPs) {
|
|
302
|
-
if (!
|
|
214
|
+
if (!FullNodePeer.peerWeights.has(ip)) {
|
|
303
215
|
if (ip === CHIA_NODES_HOST ||
|
|
304
216
|
ip === LOCALHOST ||
|
|
305
|
-
ip ===
|
|
306
|
-
|
|
217
|
+
ip === FullNodePeer.getTrustedFullNode()) {
|
|
218
|
+
FullNodePeer.peerWeights.set(ip, 5); // Higher weight for prioritized peers
|
|
307
219
|
}
|
|
308
220
|
else {
|
|
309
|
-
|
|
221
|
+
FullNodePeer.peerWeights.set(ip, 1); // Default weight
|
|
310
222
|
}
|
|
311
223
|
}
|
|
312
224
|
}
|
|
313
225
|
}
|
|
314
226
|
/**
|
|
315
|
-
* Selects
|
|
316
|
-
*
|
|
227
|
+
* Selects a peer based on weighted random selection.
|
|
228
|
+
* Prioritized peers have higher weights.
|
|
229
|
+
* @returns {string} The selected peer IP.
|
|
317
230
|
*/
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
231
|
+
static selectPeerByWeight() {
|
|
232
|
+
const peers = Array.from(FullNodePeer.peerWeights.entries())
|
|
233
|
+
.filter(([ip, _]) => !FullNodePeer.cooldownCache.has(ip))
|
|
234
|
+
.map(([ip, weight]) => ({ ip, weight }));
|
|
235
|
+
const totalWeight = peers.reduce((sum, peer) => sum + peer.weight, 0);
|
|
236
|
+
if (totalWeight === 0) {
|
|
237
|
+
throw new Error("All peers are in cooldown.");
|
|
321
238
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
239
|
+
const random = Math.random() * totalWeight;
|
|
240
|
+
let cumulative = 0;
|
|
241
|
+
for (const peer of peers) {
|
|
242
|
+
cumulative += peer.weight;
|
|
243
|
+
if (random < cumulative) {
|
|
244
|
+
return peer.ip;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Fallback
|
|
248
|
+
return peers[peers.length - 1].ip;
|
|
326
249
|
}
|
|
327
250
|
/**
|
|
328
251
|
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
329
252
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
330
253
|
*/
|
|
331
|
-
async getPeerIPs() {
|
|
332
|
-
const peerIPs = await
|
|
254
|
+
static async getPeerIPs() {
|
|
255
|
+
const peerIPs = await FullNodePeer.fetchNewPeerIPs();
|
|
333
256
|
return peerIPs;
|
|
334
257
|
}
|
|
335
258
|
/**
|
|
336
259
|
* Initializes the peer weights based on prioritization and reliability.
|
|
337
260
|
* @param {string[]} peerIPs - An array of peer IPs.
|
|
338
261
|
*/
|
|
339
|
-
setupPeers(peerIPs) {
|
|
340
|
-
|
|
262
|
+
static setupPeers(peerIPs) {
|
|
263
|
+
FullNodePeer.initializePeerWeights(peerIPs);
|
|
341
264
|
}
|
|
342
265
|
/**
|
|
343
|
-
* Connects to the best available peer based on
|
|
266
|
+
* Connects to the best available peer based on weighted selection and reliability.
|
|
344
267
|
* @returns {Promise<Peer>} The connected Peer instance.
|
|
345
268
|
*/
|
|
346
|
-
async getBestPeer() {
|
|
269
|
+
static async getBestPeer() {
|
|
347
270
|
const now = Date.now();
|
|
348
|
-
// Refresh cachedPeer if expired
|
|
349
|
-
if (
|
|
350
|
-
now -
|
|
351
|
-
|
|
352
|
-
return this.cachedPeer.peer;
|
|
271
|
+
// Refresh cachedPeer if expired
|
|
272
|
+
if (FullNodePeer.cachedPeer &&
|
|
273
|
+
now - FullNodePeer.cachedPeer.timestamp < CACHE_DURATION) {
|
|
274
|
+
return FullNodePeer.cachedPeer.peer;
|
|
353
275
|
}
|
|
354
276
|
// Fetch peer IPs
|
|
355
|
-
const peerIPs = await
|
|
277
|
+
const peerIPs = await FullNodePeer.getPeerIPs();
|
|
356
278
|
// Setup peer weights with prioritization
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
// Wrap the peer with proxy to handle errors and retries
|
|
388
|
-
const proxiedPeer = createPeerProxy(peer, ip);
|
|
389
|
-
// Store PeerInfo
|
|
390
|
-
this.peerInfos.set(ip, {
|
|
391
|
-
peer: proxiedPeer,
|
|
392
|
-
weight: this.peerWeights.get(ip) || 1,
|
|
393
|
-
address: ip,
|
|
394
|
-
isConnected: true, // Mark as connected
|
|
395
|
-
limiter: proxiedPeer.limiter, // Assign the limiter from Proxy
|
|
396
|
-
});
|
|
397
|
-
// Add to availablePeers
|
|
398
|
-
this.availablePeers.push(ip);
|
|
279
|
+
FullNodePeer.setupPeers(peerIPs);
|
|
280
|
+
// Weighted random selection
|
|
281
|
+
let selectedPeerIP;
|
|
282
|
+
try {
|
|
283
|
+
selectedPeerIP = FullNodePeer.selectPeerByWeight();
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
throw new Error(`Failed to select a peer: ${error.message}`);
|
|
287
|
+
}
|
|
288
|
+
// Attempt to create a peer connection
|
|
289
|
+
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
290
|
+
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
291
|
+
const keyFile = path_1.default.join(sslFolder, "public_dig.key");
|
|
292
|
+
if (!fs_1.default.existsSync(sslFolder)) {
|
|
293
|
+
fs_1.default.mkdirSync(sslFolder, { recursive: true });
|
|
294
|
+
}
|
|
295
|
+
const tls = new datalayer_driver_1.Tls(certFile, keyFile);
|
|
296
|
+
let peer;
|
|
297
|
+
try {
|
|
298
|
+
peer = await datalayer_driver_1.Peer.new(`${selectedPeerIP}:${FULLNODE_PORT}`, false, tls);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
console.error(`Failed to create peer for IP ${selectedPeerIP}: ${error.message}`);
|
|
302
|
+
// Add to cooldown
|
|
303
|
+
FullNodePeer.cooldownCache.set(selectedPeerIP, true);
|
|
304
|
+
// Decrease weight or remove peer
|
|
305
|
+
const currentWeight = FullNodePeer.peerWeights.get(selectedPeerIP) || 1;
|
|
306
|
+
if (currentWeight > 1) {
|
|
307
|
+
FullNodePeer.peerWeights.set(selectedPeerIP, currentWeight - 1);
|
|
399
308
|
}
|
|
400
309
|
else {
|
|
401
|
-
|
|
402
|
-
if (!peerInfo.isConnected) {
|
|
403
|
-
// Peer is back from cooldown, re-establish connection
|
|
404
|
-
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
405
|
-
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
406
|
-
const keyFile = path_1.default.join(sslFolder, "public_dig.key");
|
|
407
|
-
if (!fs_1.default.existsSync(sslFolder)) {
|
|
408
|
-
fs_1.default.mkdirSync(sslFolder, { recursive: true });
|
|
409
|
-
}
|
|
410
|
-
const tls = new datalayer_driver_1.Tls(certFile, keyFile);
|
|
411
|
-
let peer;
|
|
412
|
-
try {
|
|
413
|
-
peer = await datalayer_driver_1.Peer.new(`${ip}:${FULLNODE_PORT}`, false, tls);
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
console.error(`Failed to reconnect peer for IP ${ip}: ${error.message}`);
|
|
417
|
-
// Re-add to cooldown
|
|
418
|
-
this.cooldownCache.set(ip, true);
|
|
419
|
-
// Decrease weight or remove peer
|
|
420
|
-
const currentWeight = this.peerWeights.get(ip) || 1;
|
|
421
|
-
if (currentWeight > 1) {
|
|
422
|
-
this.peerWeights.set(ip, currentWeight - 1);
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
this.peerWeights.delete(ip);
|
|
426
|
-
}
|
|
427
|
-
continue; // Skip adding this peer
|
|
428
|
-
}
|
|
429
|
-
// Wrap the peer with proxy to handle errors and retries
|
|
430
|
-
const proxiedPeer = createPeerProxy(peer, ip);
|
|
431
|
-
// Update PeerInfo
|
|
432
|
-
peerInfo.peer = proxiedPeer;
|
|
433
|
-
peerInfo.isConnected = true;
|
|
434
|
-
// Add back to availablePeers
|
|
435
|
-
this.availablePeers.push(ip);
|
|
436
|
-
}
|
|
310
|
+
FullNodePeer.peerWeights.delete(selectedPeerIP);
|
|
437
311
|
}
|
|
312
|
+
throw new Error(`Unable to connect to peer ${selectedPeerIP}`);
|
|
438
313
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
314
|
+
// Create a Bottleneck limiter for this peer
|
|
315
|
+
const limiter = new bottleneck_1.default({
|
|
316
|
+
maxConcurrent: 1, // One request at a time per peer
|
|
317
|
+
minTime: 60000 / MAX_REQUESTS_PER_MINUTE, // e.g., 600 ms between requests for 100 requests/min
|
|
318
|
+
});
|
|
319
|
+
// Store PeerInfo
|
|
320
|
+
FullNodePeer.peerInfos.set(selectedPeerIP, {
|
|
321
|
+
peer: peer,
|
|
322
|
+
weight: FullNodePeer.peerWeights.get(selectedPeerIP) || 1,
|
|
323
|
+
address: selectedPeerIP,
|
|
324
|
+
});
|
|
325
|
+
// Initialize rate limiter for this peer
|
|
326
|
+
FullNodePeer.peerLimiters.set(selectedPeerIP, limiter);
|
|
444
327
|
// Cache the peer
|
|
445
|
-
|
|
446
|
-
console.log(`Using Fullnode Peer: ${
|
|
447
|
-
return
|
|
328
|
+
FullNodePeer.cachedPeer = { peer: peer, timestamp: now };
|
|
329
|
+
console.log(`Using Fullnode Peer: ${selectedPeerIP}`);
|
|
330
|
+
return peer;
|
|
448
331
|
}
|
|
449
332
|
/**
|
|
450
333
|
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
451
334
|
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
452
335
|
*/
|
|
453
336
|
handlePeerDisconnection(peerIP) {
|
|
454
|
-
//
|
|
455
|
-
|
|
337
|
+
// Add the faulty peer to the cooldown cache
|
|
338
|
+
FullNodePeer.cooldownCache.set(peerIP, true);
|
|
456
339
|
// Decrease weight or remove peer
|
|
457
|
-
const currentWeight =
|
|
340
|
+
const currentWeight = FullNodePeer.peerWeights.get(peerIP) || 1;
|
|
458
341
|
if (currentWeight > 1) {
|
|
459
|
-
|
|
342
|
+
FullNodePeer.peerWeights.set(peerIP, currentWeight - 1);
|
|
460
343
|
}
|
|
461
344
|
else {
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
// Update the peer's connection status
|
|
465
|
-
const peerInfo = this.peerInfos.get(peerIP);
|
|
466
|
-
if (peerInfo) {
|
|
467
|
-
peerInfo.isConnected = false;
|
|
468
|
-
this.peerInfos.set(peerIP, peerInfo);
|
|
469
|
-
}
|
|
470
|
-
// Remove from availablePeers if present
|
|
471
|
-
const index = this.availablePeers.indexOf(peerIP);
|
|
472
|
-
if (index !== -1) {
|
|
473
|
-
this.availablePeers.splice(index, 1);
|
|
474
|
-
// Adjust currentPeerIndex if necessary
|
|
475
|
-
if (this.currentPeerIndex >= this.availablePeers.length) {
|
|
476
|
-
this.currentPeerIndex = 0;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// If the disconnected peer was the cached peer, invalidate the cache
|
|
480
|
-
if (this.cachedPeer &&
|
|
481
|
-
this.extractPeerIP(this.cachedPeer.peer) === peerIP) {
|
|
482
|
-
this.cachedPeer = null;
|
|
345
|
+
FullNodePeer.peerWeights.delete(peerIP);
|
|
483
346
|
}
|
|
347
|
+
// Remove from peerInfos
|
|
348
|
+
FullNodePeer.peerInfos.delete(peerIP);
|
|
349
|
+
// Remove the limiter
|
|
350
|
+
FullNodePeer.peerLimiters.delete(peerIP);
|
|
484
351
|
console.warn(`Peer ${peerIP} has been marked as disconnected and is in cooldown.`);
|
|
485
352
|
}
|
|
486
353
|
/**
|
|
@@ -488,8 +355,8 @@ class FullNodePeer {
|
|
|
488
355
|
* @param {Peer} peer - The Peer instance.
|
|
489
356
|
* @returns {string | null} The extracted IP address or null if not found.
|
|
490
357
|
*/
|
|
491
|
-
extractPeerIP(peer) {
|
|
492
|
-
for (const [ip, info] of
|
|
358
|
+
static extractPeerIP(peer) {
|
|
359
|
+
for (const [ip, info] of FullNodePeer.peerInfos.entries()) {
|
|
493
360
|
if (info.peer === peer) {
|
|
494
361
|
return ip;
|
|
495
362
|
}
|
|
@@ -512,14 +379,27 @@ class FullNodePeer {
|
|
|
512
379
|
console.error(`waitForConfirmation connection error: ${error.message}`);
|
|
513
380
|
throw error;
|
|
514
381
|
}
|
|
382
|
+
// Extract peer IP to access the corresponding limiter
|
|
383
|
+
const peerIP = FullNodePeer.extractPeerIP(peer);
|
|
384
|
+
if (!peerIP) {
|
|
385
|
+
spinner.error({ text: "Failed to extract peer IP." });
|
|
386
|
+
throw new Error("Failed to extract peer IP.");
|
|
387
|
+
}
|
|
388
|
+
const limiter = FullNodePeer.peerLimiters.get(peerIP);
|
|
389
|
+
if (!limiter) {
|
|
390
|
+
spinner.error({ text: "No rate limiter found for the peer." });
|
|
391
|
+
throw new Error("No rate limiter found for the peer.");
|
|
392
|
+
}
|
|
515
393
|
try {
|
|
516
394
|
while (true) {
|
|
517
|
-
|
|
395
|
+
// Schedule the isCoinSpent method call through the limiter
|
|
396
|
+
const confirmed = await limiter.schedule(() => peer.isCoinSpent(parentCoinInfo, config_1.MIN_HEIGHT, Buffer.from(config_1.MIN_HEIGHT_HEADER_HASH, "hex")));
|
|
518
397
|
if (confirmed) {
|
|
519
398
|
spinner.success({ text: "Coin confirmed!" });
|
|
520
399
|
return true;
|
|
521
400
|
}
|
|
522
|
-
|
|
401
|
+
// Wait for 5 seconds before the next check
|
|
402
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
523
403
|
}
|
|
524
404
|
}
|
|
525
405
|
catch (error) {
|
|
@@ -528,15 +408,21 @@ class FullNodePeer {
|
|
|
528
408
|
throw error;
|
|
529
409
|
}
|
|
530
410
|
}
|
|
531
|
-
/**
|
|
532
|
-
* Delays execution for a specified amount of time.
|
|
533
|
-
* @param {number} ms - Milliseconds to delay.
|
|
534
|
-
* @returns {Promise<void>} A promise that resolves after the delay.
|
|
535
|
-
*/
|
|
536
|
-
static delay(ms) {
|
|
537
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
538
|
-
}
|
|
539
411
|
}
|
|
540
412
|
exports.FullNodePeer = FullNodePeer;
|
|
541
413
|
// Singleton instance
|
|
542
414
|
FullNodePeer.instance = null;
|
|
415
|
+
// Cached peer with timestamp
|
|
416
|
+
FullNodePeer.cachedPeer = null;
|
|
417
|
+
// Cooldown cache to exclude faulty peers temporarily
|
|
418
|
+
FullNodePeer.cooldownCache = new node_cache_1.default({ stdTTL: COOLDOWN_DURATION / 1000 });
|
|
419
|
+
// Peer reliability weights
|
|
420
|
+
FullNodePeer.peerWeights = new Map();
|
|
421
|
+
// List of prioritized peers
|
|
422
|
+
FullNodePeer.prioritizedPeers = [];
|
|
423
|
+
// Map to store PeerInfo
|
|
424
|
+
FullNodePeer.peerInfos = new Map();
|
|
425
|
+
// Cache for fetched peer IPs
|
|
426
|
+
FullNodePeer.peerIPCache = new node_cache_1.default({ stdTTL: CACHE_DURATION / 1000 });
|
|
427
|
+
// Map to store rate limiters per peer IP
|
|
428
|
+
FullNodePeer.peerLimiters = new Map();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface representing the metrics of a peer.
|
|
3
|
+
*/
|
|
4
|
+
export interface PeerMetrics {
|
|
5
|
+
ip: string;
|
|
6
|
+
latency: number;
|
|
7
|
+
bandwidth: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Configuration options for the PeerRanker.
|
|
11
|
+
*/
|
|
12
|
+
interface PeerRankerOptions {
|
|
13
|
+
clientCertPath: string;
|
|
14
|
+
clientKeyPath: string;
|
|
15
|
+
bandwidthTestPath: string;
|
|
16
|
+
pingPath?: string;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
bandwidthTestSize?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Utility class to rank peers based on latency and bandwidth using HTTPS with mTLS.
|
|
22
|
+
*/
|
|
23
|
+
export declare class PeerRanker {
|
|
24
|
+
private ipAddresses;
|
|
25
|
+
private static certPath;
|
|
26
|
+
private static keyPath;
|
|
27
|
+
private bandwidthTestPath;
|
|
28
|
+
private pingPath;
|
|
29
|
+
private timeout;
|
|
30
|
+
private bandwidthTestSize;
|
|
31
|
+
private sortedPeers;
|
|
32
|
+
private currentIndex;
|
|
33
|
+
/**
|
|
34
|
+
* Constructs a PeerRanker instance.
|
|
35
|
+
* @param ipAddresses - Array of IP addresses to rank.
|
|
36
|
+
* @param options - Configuration options including paths to client certificates.
|
|
37
|
+
*/
|
|
38
|
+
constructor(ipAddresses: string[], options: PeerRankerOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Measures the latency of a given IP address using an HTTPS request.
|
|
41
|
+
* Tries HEAD first, then falls back to GET if HEAD is not supported.
|
|
42
|
+
* @param ip - The IP address of the peer.
|
|
43
|
+
* @returns Promise resolving to the latency in milliseconds.
|
|
44
|
+
*/
|
|
45
|
+
private measureLatency;
|
|
46
|
+
/**
|
|
47
|
+
* Measures the bandwidth of a given IP address by downloading a file and calculating throughput.
|
|
48
|
+
* @param ip - The IP address of the peer.
|
|
49
|
+
* @returns Promise resolving to the bandwidth in bytes per second.
|
|
50
|
+
*/
|
|
51
|
+
private measureBandwidth;
|
|
52
|
+
/**
|
|
53
|
+
* Ranks the peers based on measured latency and bandwidth.
|
|
54
|
+
* @returns Promise resolving to an array of PeerMetrics sorted by latency and bandwidth.
|
|
55
|
+
*/
|
|
56
|
+
rankPeers(): Promise<PeerMetrics[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Returns the next best peer based on the ranking.
|
|
59
|
+
* Cycles through the list, resetting to the beginning once the end is reached.
|
|
60
|
+
* @returns The next PeerMetrics object or null if no peers are available.
|
|
61
|
+
*/
|
|
62
|
+
GetNextBestPeer(): PeerMetrics | null;
|
|
63
|
+
/**
|
|
64
|
+
* Resets the internal iterator to start from the beginning of the sorted list.
|
|
65
|
+
*/
|
|
66
|
+
resetIterator(): void;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=PeerRanker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PeerRanker.d.ts","sourceRoot":"","sources":["../../src/utils/PeerRanker.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,UAAU,iBAAiB;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,WAAW,CAAW;IAC9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAS;IAChC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAAS;IAGlC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,YAAY,CAAa;IAEjC;;;;OAIG;gBACS,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB;IAY7D;;;;;OAKG;YACW,cAAc;IA8C5B;;;;OAIG;YACW,gBAAgB;IA2C9B;;;OAGG;IACU,SAAS,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IA4BhD;;;;OAIG;IACI,eAAe,IAAI,WAAW,GAAG,IAAI;IAW5C;;OAEG;IACI,aAAa,IAAI,IAAI;CAG7B"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/PeerRanker.ts
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.PeerRanker = void 0;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const https_1 = __importDefault(require("https"));
|
|
11
|
+
const ssl_1 = require("./ssl");
|
|
12
|
+
/**
|
|
13
|
+
* Utility class to rank peers based on latency and bandwidth using HTTPS with mTLS.
|
|
14
|
+
*/
|
|
15
|
+
class PeerRanker {
|
|
16
|
+
/**
|
|
17
|
+
* Constructs a PeerRanker instance.
|
|
18
|
+
* @param ipAddresses - Array of IP addresses to rank.
|
|
19
|
+
* @param options - Configuration options including paths to client certificates.
|
|
20
|
+
*/
|
|
21
|
+
constructor(ipAddresses, options) {
|
|
22
|
+
// Internal properties for iteration
|
|
23
|
+
this.sortedPeers = [];
|
|
24
|
+
this.currentIndex = 0;
|
|
25
|
+
this.ipAddresses = ipAddresses;
|
|
26
|
+
this.bandwidthTestPath = options.bandwidthTestPath;
|
|
27
|
+
this.pingPath = options.pingPath || '/'; // Default to root path if not provided
|
|
28
|
+
this.timeout = options.timeout || 5000; // Default timeout: 5 seconds
|
|
29
|
+
this.bandwidthTestSize = options.bandwidthTestSize || 1024 * 1024; // Default: 1MB
|
|
30
|
+
const { certPath, keyPath } = (0, ssl_1.getOrCreateSSLCerts)();
|
|
31
|
+
PeerRanker.certPath = certPath;
|
|
32
|
+
PeerRanker.keyPath = keyPath;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Measures the latency of a given IP address using an HTTPS request.
|
|
36
|
+
* Tries HEAD first, then falls back to GET if HEAD is not supported.
|
|
37
|
+
* @param ip - The IP address of the peer.
|
|
38
|
+
* @returns Promise resolving to the latency in milliseconds.
|
|
39
|
+
*/
|
|
40
|
+
async measureLatency(ip) {
|
|
41
|
+
const path = this.pingPath;
|
|
42
|
+
const url = `https://${ip}${path}`;
|
|
43
|
+
// Configuration for HEAD request
|
|
44
|
+
const configHead = {
|
|
45
|
+
url: url,
|
|
46
|
+
method: 'HEAD',
|
|
47
|
+
httpsAgent: new https_1.default.Agent({
|
|
48
|
+
cert: fs_1.default.readFileSync(PeerRanker.certPath),
|
|
49
|
+
key: fs_1.default.readFileSync(PeerRanker.keyPath),
|
|
50
|
+
rejectUnauthorized: false, // Set to true in production
|
|
51
|
+
}),
|
|
52
|
+
timeout: this.timeout,
|
|
53
|
+
validateStatus: (status) => status < 500, // Resolve only if status is less than 500
|
|
54
|
+
};
|
|
55
|
+
const startTime = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
const response = await (0, axios_1.default)(configHead);
|
|
58
|
+
if (response.status === 405) { // Method Not Allowed
|
|
59
|
+
// Fallback to GET with Range header to minimize data transfer
|
|
60
|
+
const configGet = {
|
|
61
|
+
url: url,
|
|
62
|
+
method: 'GET',
|
|
63
|
+
httpsAgent: new https_1.default.Agent({
|
|
64
|
+
cert: fs_1.default.readFileSync(PeerRanker.certPath),
|
|
65
|
+
key: fs_1.default.readFileSync(PeerRanker.keyPath),
|
|
66
|
+
rejectUnauthorized: false, // Set to true in production
|
|
67
|
+
}),
|
|
68
|
+
timeout: this.timeout,
|
|
69
|
+
headers: {
|
|
70
|
+
'Range': 'bytes=0-0', // Request only the first byte
|
|
71
|
+
},
|
|
72
|
+
validateStatus: (status) => status < 500,
|
|
73
|
+
};
|
|
74
|
+
await (0, axios_1.default)(configGet);
|
|
75
|
+
}
|
|
76
|
+
const latency = Date.now() - startTime;
|
|
77
|
+
return latency;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error(`Latency measurement failed for IP ${ip}:`, error.message);
|
|
81
|
+
return Infinity; // Indicate unreachable or unresponsive peer
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Measures the bandwidth of a given IP address by downloading a file and calculating throughput.
|
|
86
|
+
* @param ip - The IP address of the peer.
|
|
87
|
+
* @returns Promise resolving to the bandwidth in bytes per second.
|
|
88
|
+
*/
|
|
89
|
+
async measureBandwidth(ip) {
|
|
90
|
+
const url = `https://${ip}${this.bandwidthTestPath}`;
|
|
91
|
+
const config = {
|
|
92
|
+
url: url,
|
|
93
|
+
method: 'GET',
|
|
94
|
+
responseType: 'stream',
|
|
95
|
+
httpsAgent: new https_1.default.Agent({
|
|
96
|
+
cert: fs_1.default.readFileSync(PeerRanker.certPath),
|
|
97
|
+
key: fs_1.default.readFileSync(PeerRanker.keyPath),
|
|
98
|
+
rejectUnauthorized: false, // Set to true in production
|
|
99
|
+
}),
|
|
100
|
+
timeout: this.timeout,
|
|
101
|
+
};
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const startTime = Date.now();
|
|
104
|
+
let bytesReceived = 0;
|
|
105
|
+
(0, axios_1.default)(config)
|
|
106
|
+
.then((response) => {
|
|
107
|
+
response.data.on('data', (chunk) => {
|
|
108
|
+
bytesReceived += chunk.length;
|
|
109
|
+
});
|
|
110
|
+
response.data.on('end', () => {
|
|
111
|
+
const timeElapsed = (Date.now() - startTime) / 1000; // seconds
|
|
112
|
+
const bandwidth = bytesReceived / timeElapsed; // bytes per second
|
|
113
|
+
resolve(bandwidth);
|
|
114
|
+
});
|
|
115
|
+
response.data.on('error', (err) => {
|
|
116
|
+
console.error(`Bandwidth measurement error for IP ${ip}:`, err.message);
|
|
117
|
+
resolve(0); // Indicate failure in measuring bandwidth
|
|
118
|
+
});
|
|
119
|
+
})
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
console.error(`Bandwidth measurement failed for IP ${ip}:`, error.message);
|
|
122
|
+
resolve(0); // Indicate failure in measuring bandwidth
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Ranks the peers based on measured latency and bandwidth.
|
|
128
|
+
* @returns Promise resolving to an array of PeerMetrics sorted by latency and bandwidth.
|
|
129
|
+
*/
|
|
130
|
+
async rankPeers() {
|
|
131
|
+
const metricsPromises = this.ipAddresses.map(async (ip) => {
|
|
132
|
+
const [latency, bandwidth] = await Promise.all([
|
|
133
|
+
this.measureLatency(ip),
|
|
134
|
+
this.measureBandwidth(ip),
|
|
135
|
+
]);
|
|
136
|
+
return { ip, latency, bandwidth };
|
|
137
|
+
});
|
|
138
|
+
const peerMetrics = await Promise.all(metricsPromises);
|
|
139
|
+
// Sort by lowest latency first, then by highest bandwidth
|
|
140
|
+
peerMetrics.sort((a, b) => {
|
|
141
|
+
if (a.latency === b.latency) {
|
|
142
|
+
return b.bandwidth - a.bandwidth; // Higher bandwidth is better
|
|
143
|
+
}
|
|
144
|
+
return a.latency - b.latency; // Lower latency is better
|
|
145
|
+
});
|
|
146
|
+
// Update the internal sorted list
|
|
147
|
+
this.sortedPeers = peerMetrics;
|
|
148
|
+
// Reset the iterator index
|
|
149
|
+
this.currentIndex = 0;
|
|
150
|
+
return peerMetrics;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Returns the next best peer based on the ranking.
|
|
154
|
+
* Cycles through the list, resetting to the beginning once the end is reached.
|
|
155
|
+
* @returns The next PeerMetrics object or null if no peers are available.
|
|
156
|
+
*/
|
|
157
|
+
GetNextBestPeer() {
|
|
158
|
+
if (this.sortedPeers.length === 0) {
|
|
159
|
+
console.warn('Peer list is empty. Please run rankPeers() first.');
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const peer = this.sortedPeers[this.currentIndex];
|
|
163
|
+
this.currentIndex = (this.currentIndex + 1) % this.sortedPeers.length;
|
|
164
|
+
return peer;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resets the internal iterator to start from the beginning of the sorted list.
|
|
168
|
+
*/
|
|
169
|
+
resetIterator() {
|
|
170
|
+
this.currentIndex = 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.PeerRanker = PeerRanker;
|