@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.
@@ -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 { route, type, neighborsList } = this.cryptoCodex.readUnicastMessage(d) || {};
64
- if (type !== 'handshake' || route.length !== 2 || route[1] !== this.id) return;
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 {Array<{id: string, publicUrl: string}>} bootstraps */
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.publicUrl.split(':')[1].replace('//', '');
117
- const port = parseInt(b.publicUrl.split(':')[2]) + 1;
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 {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,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 (!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
+ 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 || CryptoCodex.isPublicNode(id) || this.peerStore.isKicked(id)) continue;
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);
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.22",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },