@hive-p2p/server 1.0.21 → 1.0.22
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/core/node-services.mjs +13 -5
- package/core/node.mjs +6 -7
- package/core/topologist.mjs +49 -27
- package/package.json +1 -1
package/core/node-services.mjs
CHANGED
|
@@ -60,8 +60,15 @@ export class NodeServices {
|
|
|
60
60
|
if (remoteId) for (const cb of this.peerStore.callbacks.data) cb(remoteId, data);
|
|
61
61
|
else { // FIRST MESSAGE SHOULD BE HANDSHAKE WITH ID
|
|
62
62
|
const d = new Uint8Array(data); if (d[0] > 127) return; // not unicast, ignore
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
63
|
+
const message = this.cryptoCodex.readUnicastMessage(d);
|
|
64
|
+
if (!message) return; // invalid unicast message, ignore
|
|
65
|
+
|
|
66
|
+
const { route, type, neighborsList } = message;
|
|
67
|
+
if (type !== 'handshake' || route.length !== 2) return;
|
|
68
|
+
|
|
69
|
+
const { signatureStart, pubkey, signature } = message;
|
|
70
|
+
const signedData = d.subarray(0, signatureStart);
|
|
71
|
+
if (!this.cryptoCodex.verifySignature(pubkey, signature, signedData)) return;
|
|
65
72
|
|
|
66
73
|
remoteId = route[0];
|
|
67
74
|
this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
|
|
@@ -71,6 +78,7 @@ export class NodeServices {
|
|
|
71
78
|
for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'in');
|
|
72
79
|
}
|
|
73
80
|
});
|
|
81
|
+
ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
84
|
#startSTUNServer(host = 'localhost', port = SERVICE.PORT + 1) {
|
|
@@ -108,13 +116,13 @@ export class NodeServices {
|
|
|
108
116
|
if (this.verbose > 2) console.log(`%cSTUN Response: client will discover IP ${rinfo.address}:${rinfo.port}`, 'color: green;');
|
|
109
117
|
return response;
|
|
110
118
|
}
|
|
111
|
-
/** @param {
|
|
119
|
+
/** @param {string[]} bootstraps */
|
|
112
120
|
static deriveSTUNServers(bootstraps, includesCentralized = false) {
|
|
113
121
|
/** @type {Array<{urls: string}>} */
|
|
114
122
|
const stunUrls = [];
|
|
115
123
|
for (const b of bootstraps) {
|
|
116
|
-
const domain = b.
|
|
117
|
-
const port = parseInt(b.
|
|
124
|
+
const domain = b.split(':')[1].replace('//', '');
|
|
125
|
+
const port = parseInt(b.split(':')[2]) + 1;
|
|
118
126
|
stunUrls.push({ urls: `stun:${domain}:${port}` });
|
|
119
127
|
}
|
|
120
128
|
if (!includesCentralized) return stunUrls;
|
package/core/node.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { NodeServices } from './node-services.mjs';
|
|
|
10
10
|
|
|
11
11
|
/** Create and start a new PublicNode instance.
|
|
12
12
|
* @param {Object} options
|
|
13
|
-
* @param {
|
|
13
|
+
* @param {string[]} options.bootstraps List of bootstrap nodes used as P2P network entry
|
|
14
14
|
* @param {boolean} [options.autoStart] If true, the node will automatically start after creation (default: true)
|
|
15
15
|
* @param {CryptoCodex} [options.cryptoCodex] Identity of the node; if not provided, a new one will be generated
|
|
16
16
|
* @param {string} [options.domain] If provided, the node will operate as a public node and start necessary services (e.g., WebSocket server)
|
|
@@ -34,7 +34,7 @@ export async function createPublicNode(options) {
|
|
|
34
34
|
|
|
35
35
|
/** Create and start a new Node instance.
|
|
36
36
|
* @param {Object} options
|
|
37
|
-
* @param {
|
|
37
|
+
* @param {string[]} options.bootstraps List of bootstrap nodes used as P2P network entry
|
|
38
38
|
* @param {CryptoCodex} [options.cryptoCodex] Identity of the node; if not provided, a new one will be generated
|
|
39
39
|
* @param {boolean} [options.autoStart] If true, the node will automatically start after creation (default: true)
|
|
40
40
|
* @param {number} [options.verbose] Verbosity level for logging (default: NODE.DEFAULT_VERBOSE) */
|
|
@@ -60,7 +60,7 @@ export class Node {
|
|
|
60
60
|
|
|
61
61
|
/** Initialize a new P2P node instance, use .start() to init topologist
|
|
62
62
|
* @param {CryptoCodex} cryptoCodex - Identity of the node.
|
|
63
|
-
* @param {
|
|
63
|
+
* @param {string[]} bootstraps List of bootstrap nodes used as P2P network entry */
|
|
64
64
|
constructor(cryptoCodex, bootstraps = [], verbose = NODE.DEFAULT_VERBOSE) {
|
|
65
65
|
this.verbose = verbose;
|
|
66
66
|
if (this.topologist?.services) this.topologist.services.verbose = verbose;
|
|
@@ -72,7 +72,7 @@ export class Node {
|
|
|
72
72
|
this.peerStore = new PeerStore(this.id, this.cryptoCodex, this.offerManager, this.arbiter, verbose);
|
|
73
73
|
this.messager = new UnicastMessager(this.id, this.cryptoCodex, this.arbiter, this.peerStore, verbose);
|
|
74
74
|
this.gossip = new Gossip(this.id, this.cryptoCodex, this.arbiter, this.peerStore, verbose);
|
|
75
|
-
this.topologist = new Topologist(this.id, this.gossip, this.messager, this.peerStore, bootstraps || []);
|
|
75
|
+
this.topologist = new Topologist(this.id, this.cryptoCodex, this.gossip, this.messager, this.peerStore, bootstraps || []);
|
|
76
76
|
const { arbiter, peerStore, messager, gossip, topologist } = this;
|
|
77
77
|
|
|
78
78
|
// SETUP TRANSPORTS LISTENERS
|
|
@@ -97,8 +97,8 @@ export class Node {
|
|
|
97
97
|
const remoteIsPublic = this.cryptoCodex.isPublicNode(peerId);
|
|
98
98
|
if (this.publicUrl) return; // public node do not need to do anything special on connect
|
|
99
99
|
if (this.verbose > ((this.publicUrl || remoteIsPublic) ? 3 : 2)) console.log(`(${this.id}) ${direction === 'in' ? 'Incoming' : 'Outgoing'} connection established with peer ${peerId}`);
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
100
|
+
const bothAreNotPublic = !remoteIsPublic && !this.cryptoCodex.isPublicNode(this.id);
|
|
101
|
+
if (bothAreNotPublic || direction === 'in') this.sendMessage(peerId, this.id, 'handshake'); // send it in both case, no doubt...
|
|
102
102
|
|
|
103
103
|
const isHoverNeighbored = this.peerStore.neighborsList.length >= DISCOVERY.TARGET_NEIGHBORS_COUNT + this.halfTarget;
|
|
104
104
|
const dispatchEvents = () => {
|
|
@@ -136,7 +136,6 @@ export class Node {
|
|
|
136
136
|
|
|
137
137
|
// PUBLIC API
|
|
138
138
|
get publicUrl() { return this.services?.publicUrl; }
|
|
139
|
-
get publicIdentity() { return { id: this.id, publicUrl: this.publicUrl }; }
|
|
140
139
|
|
|
141
140
|
onMessageData(callback) { this.messager.on('message', callback); }
|
|
142
141
|
onGossipData(callback) { this.gossip.on('gossip', callback); }
|
package/core/topologist.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CLOCK, SIMULATION, TRANSPORTS, NODE, DISCOVERY, GOSSIP } from './config.mjs';
|
|
2
2
|
import { PeerConnection } from './peer-store.mjs';
|
|
3
|
-
import { CryptoCodex } from './crypto-codex.mjs';
|
|
4
3
|
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.ENABLED ? await import('../simulation/test-transports.mjs') : {};
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -47,10 +46,11 @@ class OfferQueue {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export class Topologist {
|
|
50
|
-
id; gossip; messager; peerStore; bootstraps;
|
|
49
|
+
id; cryptoCodex; gossip; messager; peerStore; bootstraps;
|
|
51
50
|
halfTarget = Math.ceil(DISCOVERY.TARGET_NEIGHBORS_COUNT / 2);
|
|
52
51
|
twiceTarget = DISCOVERY.TARGET_NEIGHBORS_COUNT * 2;
|
|
53
|
-
/** @type {
|
|
52
|
+
/** @type {Map<string, boolean>} */ bootstrapsConnectionState = new Map();
|
|
53
|
+
|
|
54
54
|
get isPublicNode() { return this.services?.publicUrl ? true : false; }
|
|
55
55
|
/** @type {import('./node-services.mjs').NodeServices | undefined} */ services;
|
|
56
56
|
|
|
@@ -59,12 +59,12 @@ export class Topologist {
|
|
|
59
59
|
offersQueue = new OfferQueue();
|
|
60
60
|
maxBonus = NODE.CONNECTION_UPGRADE_TIMEOUT * .2; // 20% of 15sec: 3sec max
|
|
61
61
|
|
|
62
|
-
/** @param {string} selfId @param {import('./gossip.mjs').Gossip} gossip @param {import('./unicast.mjs').UnicastMessager} messager @param {import('./peer-store.mjs').PeerStore} peerStore @param {
|
|
63
|
-
constructor(selfId, gossip, messager, peerStore, bootstraps) {
|
|
64
|
-
this.id = selfId; this.gossip = gossip; this.messager = messager; this.peerStore = peerStore;
|
|
65
|
-
for (const
|
|
62
|
+
/** @param {string} selfId @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex @param {import('./gossip.mjs').Gossip} gossip @param {import('./unicast.mjs').UnicastMessager} messager @param {import('./peer-store.mjs').PeerStore} peerStore @param {string[]} bootstraps */
|
|
63
|
+
constructor(selfId, cryptoCodex, gossip, messager, peerStore, bootstraps) {
|
|
64
|
+
this.id = selfId; this.cryptoCodex = cryptoCodex; this.gossip = gossip; this.messager = messager; this.peerStore = peerStore;
|
|
65
|
+
for (const url of bootstraps) this.bootstrapsConnectionState.set(url, false);
|
|
66
66
|
this.bootstraps = [...bootstraps].sort(() => Math.random() - 0.5); // shuffle
|
|
67
|
-
this.nextBootstrapIndex = Math.random() * this.
|
|
67
|
+
this.nextBootstrapIndex = Math.random() * this.bootstraps.length | 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// PUBLIC METHODS
|
|
@@ -109,21 +109,19 @@ export class Topologist {
|
|
|
109
109
|
this.offersQueue.pushSortTrim(offerItem);
|
|
110
110
|
}
|
|
111
111
|
tryConnectNextBootstrap(neighborsCount = 0, nonPublicNeighborsCount = 0) {
|
|
112
|
-
if (this.
|
|
112
|
+
if (this.bootstraps.length === 0) return;
|
|
113
113
|
const publicConnectedCount = neighborsCount - nonPublicNeighborsCount;
|
|
114
|
-
let
|
|
115
|
-
for (const id in this.peerStore.connecting)
|
|
116
|
-
if (this.bootstrapsIds.has(id)) publicConnectingCount++;
|
|
117
|
-
else connectingCount++;
|
|
114
|
+
let connectingCount = 0;
|
|
115
|
+
for (const id in this.peerStore.connecting) connectingCount++;
|
|
118
116
|
|
|
119
117
|
// MINIMIZE BOOTSTRAP CONNECTIONS DEPENDING ON HOW MANY NEIGHBORS WE HAVE
|
|
120
|
-
if (publicConnectedCount
|
|
118
|
+
if (publicConnectedCount >= this.halfTarget) return; // already connected to enough bootstraps
|
|
121
119
|
if (neighborsCount >= DISCOVERY.TARGET_NEIGHBORS_COUNT) return; // no more bootstrap needed
|
|
122
120
|
if (connectingCount + nonPublicNeighborsCount > this.twiceTarget) return; // no more bootstrap needed
|
|
123
121
|
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
this.#connectToPublicNode(
|
|
122
|
+
const publicUrl = this.bootstraps[this.nextBootstrapIndex++ % this.bootstraps.length];
|
|
123
|
+
if (this.bootstrapsConnectionState.get(publicUrl)) return; // already connecting/connected
|
|
124
|
+
this.#connectToPublicNode(publicUrl);
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
// INTERNAL METHODS
|
|
@@ -140,25 +138,49 @@ export class Topologist {
|
|
|
140
138
|
#getOverlap(peerId1 = 'toto') {
|
|
141
139
|
const p1n = this.peerStore.known[peerId1]?.neighbors || {};
|
|
142
140
|
const result = { overlap: 0, nonPublicCount: 0, p1nCount: this.peerStore.getUpdatedPeerConnectionsCount(peerId1) };
|
|
143
|
-
for (const id in p1n) if (!
|
|
141
|
+
for (const id in p1n) if (!this.cryptoCodex.isPublicNode(id)) result.nonPublicCount++;
|
|
144
142
|
for (const id of this.peerStore.standardNeighborsList) if (p1n[id]) result.overlap++;
|
|
145
143
|
return result;
|
|
146
144
|
}
|
|
147
145
|
/** Get overlap information for multiple peers @param {string[]} peerIds */
|
|
148
146
|
#getOverlaps(peerIds = []) { return peerIds.map(id => ({ id, ...this.#getOverlap(id) })); }
|
|
149
|
-
#connectToPublicNode(
|
|
150
|
-
|
|
147
|
+
#connectToPublicNode(publicUrl = 'localhost:8080') {
|
|
148
|
+
let remoteId = null;
|
|
151
149
|
const ws = new TRANSPORTS.WS_CLIENT(publicUrl); ws.binaryType = 'arraybuffer';
|
|
152
|
-
if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
|
|
153
|
-
this.peerStore.connecting[remoteId].out = new PeerConnection(remoteId, ws, 'out', true);
|
|
154
150
|
ws.onerror = (error) => console.error(`WebSocket error:`, error.stack);
|
|
155
151
|
ws.onopen = () => {
|
|
156
|
-
|
|
157
|
-
ws.
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
this.bootstrapsConnectionState.set(publicUrl, true);
|
|
153
|
+
ws.onclose = () => {
|
|
154
|
+
this.bootstrapsConnectionState.set(publicUrl, false);
|
|
155
|
+
for (const cb of this.peerStore.callbacks.disconnect) cb(remoteId, 'out');
|
|
156
|
+
}
|
|
157
|
+
ws.onmessage = (data) => {
|
|
158
|
+
if (remoteId) for (const cb of this.peerStore.callbacks.data) cb(remoteId, data.data);
|
|
159
|
+
else { // FIRST MESSAGE SHOULD BE HANDSHAKE WITH ID
|
|
160
|
+
const d = new Uint8Array(data.data); if (d[0] > 127) return; // not unicast, ignore
|
|
161
|
+
const message = this.cryptoCodex.readUnicastMessage(d);
|
|
162
|
+
if (!message) return; // invalid unicast message, ignore
|
|
163
|
+
|
|
164
|
+
const { route, type, neighborsList } = message;
|
|
165
|
+
if (type !== 'handshake' || route.length !== 2) return;
|
|
166
|
+
|
|
167
|
+
const { signatureStart, pubkey, signature } = message;
|
|
168
|
+
const signedData = d.subarray(0, signatureStart);
|
|
169
|
+
if (!this.cryptoCodex.verifySignature(pubkey, signature, signedData)) return;
|
|
170
|
+
|
|
171
|
+
remoteId = route[0];
|
|
172
|
+
this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
|
|
173
|
+
this.peerStore.connecting[remoteId]?.in?.close(); // close incoming connection if any
|
|
174
|
+
if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
|
|
175
|
+
this.peerStore.connecting[remoteId].out = new PeerConnection(remoteId, ws, 'out', true);
|
|
176
|
+
for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
|
|
180
|
+
};
|
|
160
181
|
}
|
|
161
182
|
#tryToSpreadSDP(nonPublicNeighborsCount = 0, isHalfReached = false) { // LOOP TO SELECT ONE UNSEND READY OFFER AND BROADCAST IT
|
|
183
|
+
if (!this.peerStore.neighborsList.length) return; // no neighbors, no need to spread offers
|
|
162
184
|
// LIMIT OFFER SPREADING IF WE ARE CONNECTING TO MANY PEERS, LOWER GOSSIP TRAFFIC
|
|
163
185
|
const connectingCount = Object.keys(this.peerStore.connecting).length;
|
|
164
186
|
const ingPlusEd = connectingCount + nonPublicNeighborsCount;
|
|
@@ -198,7 +220,7 @@ export class Topologist {
|
|
|
198
220
|
for (const id in this.peerStore.known) {
|
|
199
221
|
if (Math.random() > r) continue; // randomize a bit the search
|
|
200
222
|
if (--maxSearch <= 0) break;
|
|
201
|
-
if (id === this.id ||
|
|
223
|
+
if (id === this.id || this.cryptoCodex.isPublicNode(id) || this.peerStore.isKicked(id)) continue;
|
|
202
224
|
else if (this.peerStore.connected[id] || this.peerStore.connecting[id]) continue;
|
|
203
225
|
|
|
204
226
|
const { overlap, nonPublicCount } = this.#getOverlap(id);
|