@hive-p2p/server 1.0.67 → 1.0.69
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/arbiter.mjs +3 -3
- package/core/crypto-codex.mjs +11 -56
- package/core/gossip.mjs +2 -2
- package/core/node-services.mjs +4 -5
- package/core/node.mjs +2 -2
- package/core/topologist.mjs +5 -5
- package/core/unicast.mjs +3 -6
- package/package.json +1 -1
- package/services/cryptos.mjs +4 -12
package/core/arbiter.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export class Arbiter {
|
|
|
99
99
|
/** Call from HiveP2P module only! @param {string} from @param {any} message @param {Uint8Array} serialized @param {number} [powCheckFactor] default: 0.01 (1%) */
|
|
100
100
|
async digestMessage(from, message, serialized, powCheckFactor = .01) {
|
|
101
101
|
const { senderId, pubkey, topic, expectedEnd } = message; // avoid powControl() on banished peers
|
|
102
|
-
if (!
|
|
102
|
+
if (!this.#signatureControl(from, message, serialized)) return;
|
|
103
103
|
if (!this.#lengthControl(from, topic ? 'gossip' : 'unicast', serialized, expectedEnd)) return;
|
|
104
104
|
|
|
105
105
|
const routeOrHopsOk = topic ? this.#hopsControl(from, message) : this.#routeLengthControl(from, message);
|
|
@@ -111,11 +111,11 @@ export class Arbiter {
|
|
|
111
111
|
return true;
|
|
112
112
|
}
|
|
113
113
|
/** @param {string} from @param {import('./gossip.mjs').GossipMessage} message @param {Uint8Array} serialized */
|
|
114
|
-
|
|
114
|
+
#signatureControl(from, message, serialized) {
|
|
115
115
|
try {
|
|
116
116
|
const { pubkey, signature, signatureStart } = message;
|
|
117
117
|
const signedData = serialized.subarray(0, signatureStart);
|
|
118
|
-
const signatureValid =
|
|
118
|
+
const signatureValid = this.cryptoCodex.verifySignature(pubkey, signedData, signature);
|
|
119
119
|
if (!signatureValid) throw new Error('Gossip signature invalid');
|
|
120
120
|
this.adjustTrust(from, TRUST_VALUES.VALID_SIGNATURE, 'Gossip signature valid');
|
|
121
121
|
return true;
|
package/core/crypto-codex.mjs
CHANGED
|
@@ -5,54 +5,10 @@ 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
|
-
|
|
50
8
|
export class CryptoCodex {
|
|
51
9
|
argon2 = new Argon2Unified();
|
|
52
10
|
converter = new Converter();
|
|
53
11
|
AVOID_CRYPTO = false;
|
|
54
|
-
/** @type {Ed25519BatchVerifier} only if "AVOID_CRYPTO" is disabled*/
|
|
55
|
-
verifier;
|
|
56
12
|
verbose = NODE.DEFAULT_VERBOSE;
|
|
57
13
|
/** @type {string} */ id;
|
|
58
14
|
/** @type {Uint8Array} */ publicKey;
|
|
@@ -88,7 +44,6 @@ export class CryptoCodex {
|
|
|
88
44
|
if (this.nodeId) return;
|
|
89
45
|
await this.#generateAntiSybilIdentity(seed, asPublicNode);
|
|
90
46
|
this.AVOID_CRYPTO = false; // enable crypto operations
|
|
91
|
-
this.verifier = new Ed25519BatchVerifier();
|
|
92
47
|
if (!this.id) throw new Error('Failed to generate identity');
|
|
93
48
|
}
|
|
94
49
|
/** Check if the pubKey meets the difficulty using Argon2 derivation @param {Uint8Array} publicKey */
|
|
@@ -105,7 +60,7 @@ export class CryptoCodex {
|
|
|
105
60
|
async #generateAntiSybilIdentity(seed, asPublicNode) {
|
|
106
61
|
const maxIterations = (2 ** IDENTITY.DIFFICULTY) * 100; // avoid infinite loop
|
|
107
62
|
for (let i = 0; i < maxIterations; i++) { // avoid infinite loop
|
|
108
|
-
const { secretKey, publicKey } =
|
|
63
|
+
const { secretKey, publicKey } = ed25519.keygen(seed);
|
|
109
64
|
const id = this.#idFromPublicKey(publicKey);
|
|
110
65
|
if (asPublicNode && !this.isPublicNode(id)) continue; // Check prefix
|
|
111
66
|
if (!asPublicNode && this.isPublicNode(id)) continue; // Check prefix
|
|
@@ -121,14 +76,14 @@ export class CryptoCodex {
|
|
|
121
76
|
|
|
122
77
|
// MESSSAGE CREATION (SERIALIZATION AND SIGNATURE INCLUDED)
|
|
123
78
|
/** @param {Uint8Array} bufferView @param {Uint8Array} privateKey @param {number} [signaturePosition] */
|
|
124
|
-
|
|
79
|
+
signBufferViewAndAppendSignature(bufferView, privateKey, signaturePosition = bufferView.length - IDENTITY.SIGNATURE_LENGTH) {
|
|
125
80
|
if (this.AVOID_CRYPTO) return;
|
|
126
81
|
const dataToSign = bufferView.subarray(0, signaturePosition);
|
|
127
|
-
const signature =
|
|
82
|
+
const signature = ed25519.sign(dataToSign, privateKey);
|
|
128
83
|
bufferView.set(signature, signaturePosition);
|
|
129
84
|
}
|
|
130
85
|
/** @param {string} topic @param {string | Uint8Array | Object} data @param {number} [HOPS] @param {string[]} route @param {string[]} [neighbors] */
|
|
131
|
-
|
|
86
|
+
createGossipMessage(topic, data, HOPS = 3, neighbors = [], timestamp = CLOCK.time) {
|
|
132
87
|
const MARKER = GOSSIP.MARKERS_BYTES[topic];
|
|
133
88
|
if (MARKER === undefined) throw new Error(`Failed to create gossip message: unknown topic '${topic}'.`);
|
|
134
89
|
|
|
@@ -142,7 +97,7 @@ export class CryptoCodex {
|
|
|
142
97
|
bufferView.set(neighborsBytes, 47); // X bytes for neighbors
|
|
143
98
|
bufferView.set(dataBytes, 47 + neighborsBytes.length); // X bytes for data
|
|
144
99
|
bufferView.set([Math.min(255, HOPS)], totalBytes - 1); // 1 byte for HOPS (Unsigned)
|
|
145
|
-
|
|
100
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH - 1);
|
|
146
101
|
return bufferView;
|
|
147
102
|
}
|
|
148
103
|
/** @param {Uint8Array} serializedMessage */
|
|
@@ -153,7 +108,7 @@ export class CryptoCodex {
|
|
|
153
108
|
return clone;
|
|
154
109
|
}
|
|
155
110
|
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] */
|
|
156
|
-
|
|
111
|
+
createUnicastMessage(type, data, route, neighbors = [], timestamp = CLOCK.time) {
|
|
157
112
|
const MARKER = UNICAST.MARKERS_BYTES[type];
|
|
158
113
|
if (MARKER === undefined) throw new Error(`Failed to create unicast message: unknown type '${type}'.`);
|
|
159
114
|
if (route.length < 2) throw new Error('Failed to create unicast message: route must have at least 2 nodes (next hop and target).');
|
|
@@ -173,11 +128,11 @@ export class CryptoCodex {
|
|
|
173
128
|
bufferView.set([route.length], 47 + NDBL); // 1 byte for route length
|
|
174
129
|
bufferView.set(routeBytes, 47 + 1 + NDBL); // X bytes for route
|
|
175
130
|
|
|
176
|
-
|
|
131
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
|
|
177
132
|
return bufferView;
|
|
178
133
|
}
|
|
179
134
|
/** @param {Uint8Array} serialized @param {string[]} newRoute */
|
|
180
|
-
|
|
135
|
+
createReroutedUnicastMessage(serialized, newRoute) {
|
|
181
136
|
if (newRoute.length < 2) throw new Error('Failed to create rerouted unicast message: route must have at least 2 nodes (next hop and target).');
|
|
182
137
|
if (newRoute.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create rerouted unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
183
138
|
|
|
@@ -189,7 +144,7 @@ export class CryptoCodex {
|
|
|
189
144
|
bufferView.set(this.publicKey, serialized.length); // 32 bytes for new public key
|
|
190
145
|
for (let i = 0; i < routeBytesArray.length; i++) bufferView.set(routeBytesArray[i], serialized.length + 32 + (i * IDENTITY.ID_LENGTH)); // new route
|
|
191
146
|
|
|
192
|
-
|
|
147
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
|
|
193
148
|
return bufferView;
|
|
194
149
|
}
|
|
195
150
|
/** @param {string[]} ids */
|
|
@@ -217,9 +172,9 @@ export class CryptoCodex {
|
|
|
217
172
|
|
|
218
173
|
// MESSSAGE READING (DESERIALIZATION AND SIGNATURE VERIFICATION INCLUDED)
|
|
219
174
|
/** @param {Uint8Array} publicKey @param {Uint8Array} dataToVerify @param {Uint8Array} signature */
|
|
220
|
-
|
|
175
|
+
verifySignature(publicKey, dataToVerify, signature) {
|
|
221
176
|
if (this.AVOID_CRYPTO) return true;
|
|
222
|
-
return
|
|
177
|
+
return ed25519.verify(signature, dataToVerify, publicKey);
|
|
223
178
|
}
|
|
224
179
|
/** @param {Uint8Array} bufferView */
|
|
225
180
|
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
|
-
|
|
106
|
+
broadcastToAll(data, topic = 'gossip', HOPS) {
|
|
107
107
|
const hops = HOPS || GOSSIP.HOPS[topic] || GOSSIP.HOPS.default;
|
|
108
|
-
const serializedMessage =
|
|
108
|
+
const serializedMessage = 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,12 +51,12 @@ 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',
|
|
54
|
+
this.wsServer.on('connection', (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
|
|
|
58
58
|
let remoteId;
|
|
59
|
-
ws.on('message',
|
|
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
62
|
const d = new Uint8Array(data); if (d[0] > 127) return; // not unicast, ignore
|
|
@@ -68,7 +68,7 @@ export class NodeServices {
|
|
|
68
68
|
|
|
69
69
|
const { signatureStart, pubkey, signature } = message;
|
|
70
70
|
const signedData = d.subarray(0, signatureStart);
|
|
71
|
-
if (!
|
|
71
|
+
if (!this.cryptoCodex.verifySignature(pubkey, signedData, signature)) return;
|
|
72
72
|
|
|
73
73
|
remoteId = route[0];
|
|
74
74
|
this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
|
|
@@ -79,8 +79,7 @@ export class NodeServices {
|
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
ws.send(handshakeMsg);
|
|
82
|
+
ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
|
|
84
83
|
});
|
|
85
84
|
}
|
|
86
85
|
#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
|
-
|
|
165
|
+
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
|
-
|
|
168
|
+
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,13 +169,13 @@ 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 = () => {
|
|
173
173
|
this.bootstrapsConnectionState.set(publicUrl, true);
|
|
174
174
|
ws.onclose = () => {
|
|
175
175
|
this.bootstrapsConnectionState.set(publicUrl, false);
|
|
176
176
|
for (const cb of this.peerStore.callbacks.disconnect) cb(remoteId, 'out');
|
|
177
177
|
}
|
|
178
|
-
ws.onmessage =
|
|
178
|
+
ws.onmessage = (data) => {
|
|
179
179
|
if (remoteId) for (const cb of this.peerStore.callbacks.data) cb(remoteId, data.data);
|
|
180
180
|
else { // FIRST MESSAGE SHOULD BE HANDSHAKE WITH ID
|
|
181
181
|
const d = new Uint8Array(data.data); if (d[0] > 127) return; // not unicast, ignore
|
|
@@ -187,7 +187,7 @@ export class Topologist {
|
|
|
187
187
|
|
|
188
188
|
const { signatureStart, pubkey, signature } = message;
|
|
189
189
|
const signedData = d.subarray(0, signatureStart);
|
|
190
|
-
if (!
|
|
190
|
+
if (!this.cryptoCodex.verifySignature(pubkey, signedData, signature)) return;
|
|
191
191
|
|
|
192
192
|
remoteId = route[0];
|
|
193
193
|
this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
|
|
@@ -197,8 +197,8 @@ export class Topologist {
|
|
|
197
197
|
for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
|
|
198
198
|
}
|
|
199
199
|
};
|
|
200
|
-
|
|
201
|
-
ws.send(
|
|
200
|
+
|
|
201
|
+
ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
#tryToSpreadSDP(nonPublicNeighborsCount = 0, isHalfReached = false) { // LOOP TO SELECT ONE UNSEND READY OFFER AND BROADCAST IT
|
package/core/unicast.mjs
CHANGED
|
@@ -76,14 +76,13 @@ 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
|
-
|
|
79
|
+
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);
|
|
83
83
|
if (!builtResult.success) return false;
|
|
84
84
|
|
|
85
85
|
// Caution: re-routing usage who can involve insane results
|
|
86
|
-
const createdMessagePromises = [];
|
|
87
86
|
const finalSpread = builtResult.success === 'blind' ? 1 : spread; // Spread only if re-routing is false
|
|
88
87
|
for (let i = 0; i < Math.min(finalSpread, builtResult.routes.length); i++) {
|
|
89
88
|
const route = builtResult.routes[i].path;
|
|
@@ -91,11 +90,9 @@ export class UnicastMessager {
|
|
|
91
90
|
if (this.verbose > 1) console.warn(`Cannot send unicast message to ${remoteId} as route exceeds maxHops (${UNICAST.MAX_HOPS}). BFS incurred.`);
|
|
92
91
|
continue; // too long route
|
|
93
92
|
}
|
|
94
|
-
createdMessagePromises.push(this.cryptoCodex.createUnicastMessage(type, data, route, this.peerStore.neighborsList));
|
|
95
|
-
}
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
this.#sendMessageToPeer(route[1], this.cryptoCodex.createUnicastMessage(type, data, route, this.peerStore.neighborsList));
|
|
95
|
+
}
|
|
99
96
|
return true;
|
|
100
97
|
}
|
|
101
98
|
/** @param {string} targetId @param {Uint8Array} serialized */
|
package/package.json
CHANGED
package/services/cryptos.mjs
CHANGED
|
@@ -2,19 +2,11 @@ import { Converter } from './converter.mjs';
|
|
|
2
2
|
const IS_BROWSER = typeof window !== 'undefined';
|
|
3
3
|
|
|
4
4
|
// ED25519 EXPOSURE NODEJS/BROWSER COMPATIBLE ---------------------------------
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
]);*/
|
|
5
|
+
const { ed25519, x25519 } = await import('@noble/curves/ed25519.js');
|
|
6
|
+
const { chacha20poly1305 } = await import('@noble/ciphers/chacha.js');
|
|
7
|
+
const { randomBytes } = await import('@noble/ciphers/utils.js');
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
const ed_ = await import('@noble/ed25519');
|
|
12
|
-
|
|
13
|
-
/** @type {import('@noble/ed25519')} */
|
|
14
|
-
const ed25519 = ed_.default || ed_;
|
|
15
|
-
//ed25519.hashes.sha512 = sha512;
|
|
16
|
-
//export { ed25519, sha512 };
|
|
17
|
-
export { ed25519 };
|
|
9
|
+
export { ed25519, x25519, chacha20poly1305, randomBytes };
|
|
18
10
|
|
|
19
11
|
//-----------------------------------------------------------------------------
|
|
20
12
|
|