@hive-p2p/server 1.0.21 → 1.0.23

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.
@@ -59,18 +59,30 @@ export class NodeServices {
59
59
  ws.on('message', (data) => { // When peer proves his id, we can handle data normally
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
- const d = new Uint8Array(data); if (d[0] > 127) return; // not unicast, ignore
63
- const { route, type, neighborsList } = this.cryptoCodex.readUnicastMessage(d) || {};
64
- if (type !== 'handshake' || route.length !== 2 || route[1] !== this.id) return;
65
-
66
- remoteId = route[0];
67
- this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
68
- this.peerStore.connecting[remoteId]?.out?.close(); // close outgoing connection if any
69
- if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
70
- this.peerStore.connecting[remoteId].in = new PeerConnection(remoteId, ws, 'in', true);
71
- for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'in');
62
+ try {
63
+ const d = new Uint8Array(data); if (d[0] > 127) return; // not unicast, ignore
64
+ const message = this.cryptoCodex.readUnicastMessage(d);
65
+ if (!message) return; // invalid unicast message, ignore
66
+
67
+ const { route, type, neighborsList } = message;
68
+ if (type !== 'handshake' || route.length !== 2) return;
69
+
70
+ const { signatureStart, pubkey, signature } = message;
71
+ const signedData = d.subarray(0, signatureStart);
72
+ if (!this.cryptoCodex.verifySignature(pubkey, signature, signedData)) return;
73
+
74
+ remoteId = route[0];
75
+ this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
76
+ this.peerStore.connecting[remoteId]?.out?.close(); // close outgoing connection if any
77
+ if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
78
+ this.peerStore.connecting[remoteId].in = new PeerConnection(remoteId, ws, 'in', true);
79
+ for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'in');
80
+ } catch (error) {
81
+ console.error(`Error handling WebSocket message on Node #${this.id}:`, error);
82
+ }
72
83
  }
73
84
  });
85
+ ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
74
86
  });
75
87
  }
76
88
  #startSTUNServer(host = 'localhost', port = SERVICE.PORT + 1) {
@@ -108,13 +120,13 @@ export class NodeServices {
108
120
  if (this.verbose > 2) console.log(`%cSTUN Response: client will discover IP ${rinfo.address}:${rinfo.port}`, 'color: green;');
109
121
  return response;
110
122
  }
111
- /** @param {Array<{id: string, publicUrl: string}>} bootstraps */
123
+ /** @param {string[]} bootstraps */
112
124
  static deriveSTUNServers(bootstraps, includesCentralized = false) {
113
125
  /** @type {Array<{urls: string}>} */
114
126
  const stunUrls = [];
115
127
  for (const b of bootstraps) {
116
- const domain = b.publicUrl.split(':')[1].replace('//', '');
117
- const port = parseInt(b.publicUrl.split(':')[2]) + 1;
128
+ const domain = b.split(':')[1].replace('//', '');
129
+ const port = parseInt(b.split(':')[2]) + 1;
118
130
  stunUrls.push({ urls: `stun:${domain}:${port}` });
119
131
  }
120
132
  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 {Array<{id: string, publicUrl: string}>} options.bootstraps List of bootstrap nodes used as P2P network entry
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 {Array<{id: string, publicUrl: string}>} options.bootstraps List of bootstrap nodes used as P2P network entry
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 {Array<Record<string, string>>} bootstraps List of bootstrap nodes used as P2P network entry */
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 isHandshakeInitiator = remoteIsPublic || direction === 'in';
101
- if (isHandshakeInitiator) this.sendMessage(peerId, this.id, 'handshake'); // send it in both case, no doubt...
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); }
@@ -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 {Set<string>} */ bootstrapsIds = new Set();
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 {Array<{id: string, publicUrl: string}>} bootstraps */
63
- constructor(selfId, gossip, messager, peerStore, bootstraps) {
64
- this.id = selfId; this.gossip = gossip; this.messager = messager; this.peerStore = peerStore;
65
- for (const bootstrap of bootstraps) this.bootstrapsIds.add(bootstrap.id);
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.bootstrapsIds.size | 0;
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.bootstrapsIds.size === 0) return;
112
+ if (this.bootstraps.length === 0) return;
113
113
  const publicConnectedCount = neighborsCount - nonPublicNeighborsCount;
114
- let [connectingCount, publicConnectingCount] = [0, 0];
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 + publicConnectingCount >= this.halfTarget) return; // already connected to enough bootstraps
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 { id, publicUrl } = this.bootstraps[this.nextBootstrapIndex++ % this.bootstrapsIds.size];
125
- if (id && publicUrl && (this.peerStore.connected[id] || this.peerStore.connecting[id])) return;
126
- this.#connectToPublicNode(id, publicUrl);
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,53 @@ 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 (!CryptoCodex.isPublicNode(id)) result.nonPublicCount++;
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(remoteId = 'toto', publicUrl = 'localhost:8080') {
150
- if (!CryptoCodex.isPublicNode(remoteId)) return this.verbose < 1 ? null : console.warn(`Topologist: trying to connect to a non-public node (${remoteId})`);
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
- ws.onclose = () => { for (const cb of this.peerStore.callbacks.disconnect) cb(remoteId, 'out'); }
157
- ws.onmessage = (data) => { for (const cb of this.peerStore.callbacks.data) cb(remoteId, data.data); };
158
- for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
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
+ try {
161
+ const d = new Uint8Array(data.data); if (d[0] > 127) return; // not unicast, ignore
162
+ const message = this.cryptoCodex.readUnicastMessage(d);
163
+ if (!message) return; // invalid unicast message, ignore
164
+
165
+ const { route, type, neighborsList } = message;
166
+ if (type !== 'handshake' || route.length !== 2) return;
167
+
168
+ const { signatureStart, pubkey, signature } = message;
169
+ const signedData = d.subarray(0, signatureStart);
170
+ if (!this.cryptoCodex.verifySignature(pubkey, signature, signedData)) return;
171
+
172
+ remoteId = route[0];
173
+ this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
174
+ this.peerStore.connecting[remoteId]?.in?.close(); // close incoming connection if any
175
+ if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
176
+ this.peerStore.connecting[remoteId].out = new PeerConnection(remoteId, ws, 'out', true);
177
+ for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
178
+ } catch (error) {
179
+ console.error(`Error handling WebSocket message on Node #${this.id}:`, error);
180
+ }
181
+ }
182
+ };
183
+ ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
184
+ };
160
185
  }
161
186
  #tryToSpreadSDP(nonPublicNeighborsCount = 0, isHalfReached = false) { // LOOP TO SELECT ONE UNSEND READY OFFER AND BROADCAST IT
187
+ if (!this.peerStore.neighborsList.length) return; // no neighbors, no need to spread offers
162
188
  // LIMIT OFFER SPREADING IF WE ARE CONNECTING TO MANY PEERS, LOWER GOSSIP TRAFFIC
163
189
  const connectingCount = Object.keys(this.peerStore.connecting).length;
164
190
  const ingPlusEd = connectingCount + nonPublicNeighborsCount;
@@ -198,7 +224,7 @@ export class Topologist {
198
224
  for (const id in this.peerStore.known) {
199
225
  if (Math.random() > r) continue; // randomize a bit the search
200
226
  if (--maxSearch <= 0) break;
201
- if (id === this.id || CryptoCodex.isPublicNode(id) || this.peerStore.isKicked(id)) continue;
227
+ if (id === this.id || this.cryptoCodex.isPublicNode(id) || this.peerStore.isKicked(id)) continue;
202
228
  else if (this.peerStore.connected[id] || this.peerStore.connecting[id]) continue;
203
229
 
204
230
  const { overlap, nonPublicCount } = this.#getOverlap(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hive-p2p/server",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },