@hive-p2p/server 1.0.64 → 1.0.66

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.
@@ -5,10 +5,54 @@ import { DirectMessage, ReroutedDirectMessage } from './unicast.mjs';
5
5
  import { Converter } from '../services/converter.mjs';
6
6
  import { ed25519, Argon2Unified } from '../services/cryptos.mjs'; // now exposed in full and browser builds
7
7
 
8
+ class Ed25519BatchVerifier {
9
+ #verifyQueue = [];
10
+ #verifyResolvers = new Map();
11
+ #batchTimer = null;
12
+ #nextId = 0;
13
+ #BATCH_SIZE = 10;
14
+ #BATCH_TIMEOUT = 5;
15
+
16
+ verifySignature(publicKey, dataToVerify, signature) {
17
+ return new Promise((resolve) => {
18
+ const id = this.#nextId++;
19
+ this.#verifyQueue.push({ id, publicKey, dataToVerify, signature });
20
+ this.#verifyResolvers.set(id, resolve);
21
+
22
+ // Flush immediately if batch full
23
+ if (this.#verifyQueue.length >= this.#BATCH_SIZE) {
24
+ this.#flushBatch();
25
+ return;
26
+ }
27
+
28
+ // Otherwise schedule flush
29
+ if (!this.#batchTimer)
30
+ this.#batchTimer = setTimeout(() => this.#flushBatch(), this.#BATCH_TIMEOUT);
31
+ });
32
+ }
33
+
34
+ async #flushBatch() {
35
+ if (this.#batchTimer) clearTimeout(this.#batchTimer), this.#batchTimer = null;
36
+ if (this.#verifyQueue.length === 0) return;
37
+
38
+ const batch = this.#verifyQueue.splice(0);
39
+ const results = await Promise.all(
40
+ batch.map(item => ed25519.verifyAsync(item.signature, item.dataToVerify, item.publicKey))
41
+ );
42
+
43
+ for (let i = 0; i < batch.length; i++) {
44
+ this.#verifyResolvers.get(batch[i].id)(results[i]);
45
+ this.#verifyResolvers.delete(batch[i].id);
46
+ }
47
+ }
48
+ }
49
+
8
50
  export class CryptoCodex {
9
51
  argon2 = new Argon2Unified();
10
52
  converter = new Converter();
11
53
  AVOID_CRYPTO = false;
54
+ /** @type {Ed25519BatchVerifier} only if "AVOID_CRYPTO" is disabled*/
55
+ verifier;
12
56
  verbose = NODE.DEFAULT_VERBOSE;
13
57
  /** @type {string} */ id;
14
58
  /** @type {Uint8Array} */ publicKey;
@@ -17,8 +61,9 @@ export class CryptoCodex {
17
61
  /** @param {string} [nodeId] If provided: used to generate a fake keypair > disable crypto operations */
18
62
  constructor(nodeId, verbose = NODE.DEFAULT_VERBOSE) {
19
63
  this.verbose = verbose;
64
+ //this.AVOID_CRYPTO = IDENTITY.ARE_IDS_HEX ? false : true; // disable crypto if string ids are used
20
65
  if (!nodeId) return; // IF NOT PROVIDED: generate() should be called.
21
- this.AVOID_CRYPTO = true;
66
+
22
67
  this.id = nodeId.padEnd(IDENTITY.ID_LENGTH, ' ').slice(0, IDENTITY.ID_LENGTH);
23
68
  this.privateKey = new Uint8Array(32).fill(0); this.publicKey = new Uint8Array(32).fill(0);
24
69
  const idBytes = new TextEncoder().encode(this.id); // use nodeId to create a fake public key
@@ -42,6 +87,8 @@ export class CryptoCodex {
42
87
  async generate(asPublicNode, seed) { // Generate Ed25519 keypair cross-platform | set id only for simulator
43
88
  if (this.nodeId) return;
44
89
  await this.#generateAntiSybilIdentity(seed, asPublicNode);
90
+ this.AVOID_CRYPTO = false; // enable crypto operations
91
+ this.verifier = new Ed25519BatchVerifier();
45
92
  if (!this.id) throw new Error('Failed to generate identity');
46
93
  }
47
94
  /** Check if the pubKey meets the difficulty using Argon2 derivation @param {Uint8Array} publicKey */
@@ -58,7 +105,7 @@ export class CryptoCodex {
58
105
  async #generateAntiSybilIdentity(seed, asPublicNode) {
59
106
  const maxIterations = (2 ** IDENTITY.DIFFICULTY) * 100; // avoid infinite loop
60
107
  for (let i = 0; i < maxIterations; i++) { // avoid infinite loop
61
- const { secretKey, publicKey } = ed25519.keygen(seed);
108
+ const { secretKey, publicKey } = await ed25519.keygenAsync(seed);
62
109
  const id = this.#idFromPublicKey(publicKey);
63
110
  if (asPublicNode && !this.isPublicNode(id)) continue; // Check prefix
64
111
  if (!asPublicNode && this.isPublicNode(id)) continue; // Check prefix
@@ -73,13 +120,15 @@ export class CryptoCodex {
73
120
  }
74
121
 
75
122
  // MESSSAGE CREATION (SERIALIZATION AND SIGNATURE INCLUDED)
76
- signBufferViewAndAppendSignature(bufferView, privateKey, signaturePosition = bufferView.length - IDENTITY.SIGNATURE_LENGTH) {
123
+ /** @param {Uint8Array} bufferView @param {Uint8Array} privateKey @param {number} [signaturePosition] */
124
+ async signBufferViewAndAppendSignature(bufferView, privateKey, signaturePosition = bufferView.length - IDENTITY.SIGNATURE_LENGTH) {
77
125
  if (this.AVOID_CRYPTO) return;
78
126
  const dataToSign = bufferView.subarray(0, signaturePosition);
79
- bufferView.set(ed25519.sign(dataToSign, privateKey), signaturePosition);
127
+ const signature = await ed25519.signAsync(dataToSign, privateKey);
128
+ bufferView.set(signature, signaturePosition);
80
129
  }
81
130
  /** @param {string} topic @param {string | Uint8Array | Object} data @param {number} [HOPS] @param {string[]} route @param {string[]} [neighbors] */
82
- createGossipMessage(topic, data, HOPS = 3, neighbors = [], timestamp = CLOCK.time) {
131
+ async createGossipMessage(topic, data, HOPS = 3, neighbors = [], timestamp = CLOCK.time) {
83
132
  const MARKER = GOSSIP.MARKERS_BYTES[topic];
84
133
  if (MARKER === undefined) throw new Error(`Failed to create gossip message: unknown topic '${topic}'.`);
85
134
 
@@ -93,7 +142,7 @@ export class CryptoCodex {
93
142
  bufferView.set(neighborsBytes, 47); // X bytes for neighbors
94
143
  bufferView.set(dataBytes, 47 + neighborsBytes.length); // X bytes for data
95
144
  bufferView.set([Math.min(255, HOPS)], totalBytes - 1); // 1 byte for HOPS (Unsigned)
96
- this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH - 1);
145
+ await this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH - 1);
97
146
  return bufferView;
98
147
  }
99
148
  /** @param {Uint8Array} serializedMessage */
@@ -104,7 +153,7 @@ export class CryptoCodex {
104
153
  return clone;
105
154
  }
106
155
  /** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] */
107
- createUnicastMessage(type, data, route, neighbors = [], timestamp = CLOCK.time) {
156
+ async createUnicastMessage(type, data, route, neighbors = [], timestamp = CLOCK.time) {
108
157
  const MARKER = UNICAST.MARKERS_BYTES[type];
109
158
  if (MARKER === undefined) throw new Error(`Failed to create unicast message: unknown type '${type}'.`);
110
159
  if (route.length < 2) throw new Error('Failed to create unicast message: route must have at least 2 nodes (next hop and target).');
@@ -124,11 +173,11 @@ export class CryptoCodex {
124
173
  bufferView.set([route.length], 47 + NDBL); // 1 byte for route length
125
174
  bufferView.set(routeBytes, 47 + 1 + NDBL); // X bytes for route
126
175
 
127
- this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
176
+ await this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
128
177
  return bufferView;
129
178
  }
130
179
  /** @param {Uint8Array} serialized @param {string[]} newRoute */
131
- createReroutedUnicastMessage(serialized, newRoute) {
180
+ async createReroutedUnicastMessage(serialized, newRoute) {
132
181
  if (newRoute.length < 2) throw new Error('Failed to create rerouted unicast message: route must have at least 2 nodes (next hop and target).');
133
182
  if (newRoute.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create rerouted unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
134
183
 
@@ -140,7 +189,7 @@ export class CryptoCodex {
140
189
  bufferView.set(this.publicKey, serialized.length); // 32 bytes for new public key
141
190
  for (let i = 0; i < routeBytesArray.length; i++) bufferView.set(routeBytesArray[i], serialized.length + 32 + (i * IDENTITY.ID_LENGTH)); // new route
142
191
 
143
- this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
192
+ await this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
144
193
  return bufferView;
145
194
  }
146
195
  /** @param {string[]} ids */
@@ -170,7 +219,7 @@ export class CryptoCodex {
170
219
  /** @param {Uint8Array} publicKey @param {Uint8Array} dataToVerify @param {Uint8Array} signature */
171
220
  async verifySignature(publicKey, dataToVerify, signature) {
172
221
  if (this.AVOID_CRYPTO) return true;
173
- return ed25519.verifyAsync(signature, dataToVerify, publicKey);
222
+ return this.verifier.verifySignature(publicKey, dataToVerify, signature);
174
223
  }
175
224
  /** @param {Uint8Array} bufferView */
176
225
  readBufferHeader(bufferView, readAssociatedId = true) {
package/core/gossip.mjs CHANGED
@@ -103,9 +103,9 @@ export class Gossip {
103
103
  }
104
104
  /** Gossip a message to all connected peers > will be forwarded to all peers
105
105
  * @param {string | Uint8Array | Object} data @param {string} topic default: 'gossip' @param {number} [HOPS] */
106
- broadcastToAll(data, topic = 'gossip', HOPS) {
106
+ async broadcastToAll(data, topic = 'gossip', HOPS) {
107
107
  const hops = HOPS || GOSSIP.HOPS[topic] || GOSSIP.HOPS.default;
108
- const serializedMessage = this.cryptoCodex.createGossipMessage(topic, data, hops, this.peerStore.neighborsList);
108
+ const serializedMessage = await this.cryptoCodex.createGossipMessage(topic, data, hops, this.peerStore.neighborsList);
109
109
  if (!this.bloomFilter.addMessage(serializedMessage)) return; // avoid sending duplicate messages
110
110
  if (this.verbose > 3) console.log(`(${this.id}) Gossip ${topic}, to ${JSON.stringify(this.peerStore.neighborsList)}: ${data}`);
111
111
  for (const peerId of this.peerStore.neighborsList) this.#broadcastToPeer(peerId, serializedMessage);
@@ -51,7 +51,7 @@ export class NodeServices {
51
51
  #startWebSocketServer(domain = 'localhost', port = SERVICE.PORT) {
52
52
  this.wsServer = new TRANSPORTS.WS_SERVER({ port, host: domain });
53
53
  this.wsServer.on('error', (error) => console.error(`WebSocket error on Node #${this.id}:`, error));
54
- this.wsServer.on('connection', (ws) => {
54
+ this.wsServer.on('connection', async (ws) => {
55
55
  ws.on('close', () => { if (remoteId) for (const cb of this.peerStore.callbacks.disconnect) cb(remoteId, 'in'); });
56
56
  ws.on('error', (error) => console.error(`WebSocket error on Node #${this.id} with peer ${remoteId}:`, error.stack));
57
57
 
@@ -78,7 +78,9 @@ export class NodeServices {
78
78
  for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'in');
79
79
  }
80
80
  });
81
- ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
81
+
82
+ const handshakeMsg = await this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList);
83
+ ws.send(handshakeMsg);
82
84
  });
83
85
  }
84
86
  #startSTUNServer(host = 'localhost', port = SERVICE.PORT + 1) {
package/core/node.mjs CHANGED
@@ -162,10 +162,10 @@ export class Node {
162
162
  /** Broadcast a message to all connected peers or to a specified peer
163
163
  * @param {string | Uint8Array | Object} data @param {string} topic default: 'gossip' @param {string} [targetId] default: broadcast to all
164
164
  * @param {number} [timestamp] default: CLOCK.time @param {number} [HOPS] default: GOSSIP.HOPS[topic] || GOSSIP.HOPS.default */
165
- broadcast(data, topic, HOPS) { this.gossip.broadcastToAll(data, topic, HOPS); }
165
+ async broadcast(data, topic, HOPS) { return this.gossip.broadcastToAll(data, topic, HOPS); }
166
166
 
167
167
  /** @param {string} remoteId @param {string | Uint8Array | Object} data @param {string} type */
168
- sendMessage(remoteId, data, type, spread = 1) { this.messager.sendUnicast(remoteId, data, type, spread); }
168
+ async sendMessage(remoteId, data, type, spread = 1) { return this.messager.sendUnicast(remoteId, data, type, spread); }
169
169
 
170
170
  /** Send a connection request to a peer */
171
171
  async tryConnectToPeer(targetId = 'toto', retry = 10) {
@@ -169,7 +169,7 @@ export class Topologist {
169
169
  let remoteId = null;
170
170
  const ws = new TRANSPORTS.WS_CLIENT(this.#getFullWsUrl(publicUrl)); ws.binaryType = 'arraybuffer';
171
171
  ws.onerror = (error) => console.error(`WebSocket error:`, error.stack);
172
- ws.onopen = () => {
172
+ ws.onopen = async () => {
173
173
  this.bootstrapsConnectionState.set(publicUrl, true);
174
174
  ws.onclose = () => {
175
175
  this.bootstrapsConnectionState.set(publicUrl, false);
@@ -197,8 +197,9 @@ export class Topologist {
197
197
  for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
198
198
  }
199
199
  };
200
- ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
201
- };
200
+ const handshakeMsg = await this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList);
201
+ ws.send(handshakeMsg);
202
+ };
202
203
  }
203
204
  #tryToSpreadSDP(nonPublicNeighborsCount = 0, isHalfReached = false) { // LOOP TO SELECT ONE UNSEND READY OFFER AND BROADCAST IT
204
205
  if (!this.automation.spreadOffers) return;
package/core/unicast.mjs CHANGED
@@ -76,7 +76,7 @@ export class UnicastMessager {
76
76
  /** Send unicast message to a target
77
77
  * @param {string} remoteId @param {string | Uint8Array | Object} data @param {string} type
78
78
  * @param {number} [spread] Max neighbors used to relay the message, default: 1 */
79
- sendUnicast(remoteId, data, type = 'message', spread = 1) {
79
+ async sendUnicast(remoteId, data, type = 'message', spread = 1) {
80
80
  if (remoteId === this.id) return false;
81
81
 
82
82
  const builtResult = this.pathFinder.buildRoutes(remoteId, this.maxRoutes, this.maxHops, this.maxNodes, true);
@@ -90,7 +90,7 @@ export class UnicastMessager {
90
90
  if (this.verbose > 1) console.warn(`Cannot send unicast message to ${remoteId} as route exceeds maxHops (${UNICAST.MAX_HOPS}). BFS incurred.`);
91
91
  continue; // too long route
92
92
  }
93
- const message = this.cryptoCodex.createUnicastMessage(type, data, route, this.peerStore.neighborsList);
93
+ const message = await this.cryptoCodex.createUnicastMessage(type, data, route, this.peerStore.neighborsList);
94
94
  this.#sendMessageToPeer(route[1], message); // send to next peer
95
95
  }
96
96
  return true;
@@ -148,7 +148,7 @@ export class UnicastMessager {
148
148
  return; // too long route
149
149
  }
150
150
 
151
- const patchedMessage = this.cryptoCodex.createReroutedUnicastMessage(serialized, newRoute);
151
+ const patchedMessage = await this.cryptoCodex.createReroutedUnicastMessage(serialized, newRoute);
152
152
  const nextPeerId = newRoute[selfPosition + 1];
153
153
  this.#sendMessageToPeer(nextPeerId, patchedMessage);
154
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hive-p2p/server",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -6,16 +6,14 @@ const IS_BROWSER = typeof window !== 'undefined';
6
6
  import(IS_BROWSER ? '../libs/ed25519-custom.min.js' : '@noble/ed25519'),
7
7
  import(IS_BROWSER ? '../libs/ed25519-custom.min.js' : '@noble/hashes/sha2.js')
8
8
  ]);*/
9
- /** @type {import('@noble/ed25519')} */
10
- //const ed25519 = ed_.default || ed_;
11
- //ed25519.hashes.sha512 = sha512;
12
- //export { ed25519, sha512 };
13
9
 
14
- const [ed_] = await Promise.all([
15
- import(IS_BROWSER ? '../libs/ed25519-custom.min.js' : '@noble/ed25519'),
16
- ]);
10
+ // Now we only use nodejs version without sha512
11
+ const ed_ = await import('@noble/ed25519');
12
+
17
13
  /** @type {import('@noble/ed25519')} */
18
14
  const ed25519 = ed_.default || ed_;
15
+ //ed25519.hashes.sha512 = sha512;
16
+ //export { ed25519, sha512 };
19
17
  export { ed25519 };
20
18
 
21
19
  //-----------------------------------------------------------------------------