@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.
- package/core/crypto-codex.mjs +60 -11
- package/core/gossip.mjs +2 -2
- package/core/node-services.mjs +4 -2
- package/core/node.mjs +2 -2
- package/core/topologist.mjs +4 -3
- package/core/unicast.mjs +3 -3
- package/package.json +1 -1
- package/services/cryptos.mjs +5 -7
package/core/crypto-codex.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/core/node-services.mjs
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/core/topologist.mjs
CHANGED
|
@@ -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
|
-
|
|
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
package/services/cryptos.mjs
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
//-----------------------------------------------------------------------------
|