hive-p2p 1.0.108 → 1.0.112
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/config.mjs +9 -8
- package/core/crypto-codex.mjs +43 -35
- package/core/gossip.mjs +21 -10
- package/core/node.mjs +1 -3
- package/core/peer-store.mjs +1 -1
- package/core/route-builder.mjs +3 -6
- package/core/topologist.mjs +1 -1
- package/core/unicast.mjs +28 -21
- package/dist/browser/hive-p2p.min.js +3 -3
- package/package.json +3 -3
- package/packages/full/index.mjs +3 -1
- package/simulation/simul-utils.mjs +3 -2
- package/simulation/simulator.mjs +3 -7
package/core/config.mjs
CHANGED
|
@@ -88,10 +88,10 @@ export const IDENTITY = {
|
|
|
88
88
|
ARGON2_MEM: 2**16,
|
|
89
89
|
/** Boolean to indicate if we use hex ids, Default: true = hex | false = strings as Bytes (can involve in serialization failures) */
|
|
90
90
|
ARE_IDS_HEX: true,
|
|
91
|
-
/** Identifier prefix for public nodes | Default: '0' */
|
|
92
|
-
PUBLIC_PREFIX: '0',
|
|
93
|
-
/** Identifier prefix for standard nodes | Default: '1' */
|
|
94
|
-
STANDARD_PREFIX: '1',
|
|
91
|
+
/** Identifier prefix for public nodes [binaryPrefix, StringPrefix] | Default: ['0', 'P_'] */
|
|
92
|
+
PUBLIC_PREFIX: ['0', 'P_'],
|
|
93
|
+
/** Identifier prefix for standard nodes [binaryPrefix, StringPrefix] | Default: ['1', 'N_'] */
|
|
94
|
+
STANDARD_PREFIX: ['1', 'N_'],
|
|
95
95
|
/** !!EVEN NUMBER ONLY!! length of peer id | Default: 16 */
|
|
96
96
|
ID_LENGTH: 16,
|
|
97
97
|
PUBKEY_LENGTH: 32,
|
|
@@ -156,7 +156,7 @@ export const UNICAST = { // MARKERS RANGE: 0-127
|
|
|
156
156
|
/** Maximum number of routes to consider during BFS
|
|
157
157
|
* - Default: 5, light: 3, super-light: 1 */
|
|
158
158
|
MAX_ROUTES: 5,
|
|
159
|
-
/** First byte markers for unicast messages | RANGE: 0-127 */
|
|
159
|
+
/** First byte markers for unicast messages | RANGE: 0-127 @type {Record<string, string | number>} */
|
|
160
160
|
MARKERS_BYTES: {
|
|
161
161
|
message: 0,
|
|
162
162
|
'0': 'message',
|
|
@@ -181,7 +181,7 @@ export const GOSSIP = { // MARKERS RANGE: 128-255
|
|
|
181
181
|
/** Time to keep messages in cache to avoid reprocessing | Default: 20_000 (20 seconds) */
|
|
182
182
|
CACHE_DURATION: 20_000,
|
|
183
183
|
/** Maximum number of hops for gossip messages | Default: 20
|
|
184
|
-
* - Here you can set different max hops for different message types */
|
|
184
|
+
* - Here you can set different max hops for different message types @type {Record<string, number>} */
|
|
185
185
|
HOPS: {
|
|
186
186
|
default: 20, // 16 should be the maximum
|
|
187
187
|
signal_offer: 6, // works with 3 ?
|
|
@@ -192,7 +192,8 @@ export const GOSSIP = { // MARKERS RANGE: 128-255
|
|
|
192
192
|
},
|
|
193
193
|
/** Ponderation to lower the transmission rate based on neighbors count
|
|
194
194
|
* - Lowering the transmission rate based on neighbors count, but involve a lower gossip diffusion
|
|
195
|
-
* - As well you can apply different ponderation factors for different message types
|
|
195
|
+
* - As well you can apply different ponderation factors for different message types
|
|
196
|
+
* @type {Record<string, number>} */
|
|
196
197
|
TRANSMISSION_RATE: {
|
|
197
198
|
/** Minimum neighbors to apply ponderation, Default: 2
|
|
198
199
|
* - Decrease to apply ponderation sooner */
|
|
@@ -206,7 +207,7 @@ export const GOSSIP = { // MARKERS RANGE: 128-255
|
|
|
206
207
|
// peer_connected: .5, // we can reduce this, but lowering the map quality
|
|
207
208
|
// peer_disconnected: .618
|
|
208
209
|
},
|
|
209
|
-
/** First byte markers for gossip messages | RANGE: 128-255 */
|
|
210
|
+
/** First byte markers for gossip messages | RANGE: 128-255 @type {Record<string, string | number>} */
|
|
210
211
|
MARKERS_BYTES: {
|
|
211
212
|
gossip: 128,
|
|
212
213
|
'128': 'gossip',
|
package/core/crypto-codex.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { CLOCK } from '../services/clock.mjs';
|
|
2
3
|
import { SIMULATION, NODE, IDENTITY, GOSSIP, UNICAST, LOG_CSS } from './config.mjs';
|
|
3
4
|
import { GossipMessage } from './gossip.mjs';
|
|
4
|
-
import { DirectMessage
|
|
5
|
+
import { DirectMessage } from './unicast.mjs';
|
|
5
6
|
import { Converter } from '../services/converter.mjs';
|
|
6
7
|
import { ed25519, x25519, chacha20poly1305, randomBytes, Argon2Unified } from '../services/cryptos.mjs'; // now exposed in full and browser builds
|
|
7
8
|
import { concatBytes } from '@noble/ciphers/utils.js';
|
|
@@ -11,25 +12,27 @@ export class CryptoCodex {
|
|
|
11
12
|
converter = new Converter();
|
|
12
13
|
AVOID_CRYPTO = true; // AVOID CRYPTO OPERATIONS (default) => auto-enable when generate() is called, but can be set to true to disable crypto in any case (e.g. for testing with string ids)
|
|
13
14
|
verbose = NODE.DEFAULT_VERBOSE;
|
|
14
|
-
|
|
15
|
+
id;
|
|
15
16
|
/** @type {Uint8Array} */ publicKey;
|
|
16
17
|
/** @type {Uint8Array} */ privateKey;
|
|
18
|
+
get idLength() { return IDENTITY.ARE_IDS_HEX ? IDENTITY.ID_LENGTH / 2 : IDENTITY.ID_LENGTH; }
|
|
17
19
|
|
|
18
20
|
/** @param {string} [nodeId] If provided: used to generate a fake keypair > disable crypto operations */
|
|
19
21
|
constructor(nodeId, verbose = NODE.DEFAULT_VERBOSE) {
|
|
22
|
+
this.privateKey = new Uint8Array(32).fill(0);
|
|
23
|
+
this.publicKey = new Uint8Array(32).fill(0);
|
|
20
24
|
this.verbose = verbose;
|
|
21
25
|
//this.AVOID_CRYPTO = IDENTITY.ARE_IDS_HEX ? false : true; // disable crypto if string ids are used
|
|
22
26
|
if (!nodeId) return; // IF NOT PROVIDED: generate() should be called.
|
|
23
27
|
|
|
24
28
|
this.id = nodeId.padEnd(IDENTITY.ID_LENGTH, ' ').slice(0, IDENTITY.ID_LENGTH);
|
|
25
|
-
this.privateKey = new Uint8Array(32).fill(0); this.publicKey = new Uint8Array(32).fill(0);
|
|
26
29
|
const idBytes = new TextEncoder().encode(this.id); // use nodeId to create a fake public key
|
|
27
30
|
for (let i = 0; i < IDENTITY.ID_LENGTH; i++) this.publicKey[i] = idBytes[i];
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** @param {boolean} asPublicNode Default: false @param {Uint8Array | string} seed 32 bytes PrivateKey *-optional* */
|
|
31
34
|
static async createCryptoCodex(asPublicNode, seed) {
|
|
32
|
-
const cryptoCodex = new CryptoCodex(undefined,
|
|
35
|
+
const cryptoCodex = new CryptoCodex(undefined, NODE.DEFAULT_VERBOSE);
|
|
33
36
|
const seedBytes = !seed ? undefined : (typeof seed === 'string' ? cryptoCodex.converter.hexToBytes(seed) : seed);
|
|
34
37
|
await cryptoCodex.generate(asPublicNode, seedBytes);
|
|
35
38
|
return cryptoCodex;
|
|
@@ -37,23 +40,27 @@ export class CryptoCodex {
|
|
|
37
40
|
|
|
38
41
|
// IDENTITY
|
|
39
42
|
/** @param {string} id Check the first character against the PUBLIC_PREFIX */
|
|
40
|
-
static isPublicNode(id) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
static isPublicNode(id) {
|
|
44
|
+
const idStr = id[1] === '_' ? id : Converter.hexToBits(id[0]);
|
|
45
|
+
if (typeof idStr !== 'string') throw new Error("idStr isn't string!");
|
|
46
|
+
return idStr.startsWith(IDENTITY.PUBLIC_PREFIX[0]) || idStr.startsWith(IDENTITY.PUBLIC_PREFIX[1]);
|
|
47
|
+
}
|
|
48
|
+
/** @param {string} id Check the first character against the PUBLIC_PREFIX */
|
|
43
49
|
isPublicNode(id) { return CryptoCodex.isPublicNode(id); }
|
|
44
50
|
/** @param {boolean} asPublicNode @param {Uint8Array} [seed] The privateKey. DON'T USE IN SIMULATION */
|
|
45
51
|
async generate(asPublicNode, seed) { // Generate Ed25519 keypair cross-platform | set id only for simulator
|
|
46
|
-
if (this.
|
|
52
|
+
if (this.id) return;
|
|
47
53
|
|
|
48
54
|
const s = seed || await CryptoCodex.generateNewSybilIdentity(asPublicNode, this.verbose > 0);
|
|
49
55
|
await this.#generateAntiSybilIdentity(s, asPublicNode);
|
|
50
56
|
this.AVOID_CRYPTO = false; // force enable crypto operations
|
|
51
57
|
if (!this.id) throw new Error('Failed to generate identity');
|
|
52
58
|
}
|
|
53
|
-
/** Check if the pubKey meets the difficulty using Argon2 derivation @param {Uint8Array} publicKey */
|
|
59
|
+
/** Check if the pubKey meets the difficulty using Argon2 derivation @param {string | Uint8Array} publicKey */
|
|
54
60
|
async pubkeyDifficultyCheck(publicKey) {
|
|
55
61
|
if (this.AVOID_CRYPTO || !IDENTITY.DIFFICULTY) return true;
|
|
56
|
-
const
|
|
62
|
+
const pubKeyStr = typeof publicKey === 'string' ? publicKey : this.converter.bytesToHex(publicKey, IDENTITY.PUBKEY_LENGTH);
|
|
63
|
+
const { bitsString } = await this.argon2.hash(pubKeyStr, 'HiveP2P', IDENTITY.ARGON2_MEM) || {};
|
|
57
64
|
if (bitsString && bitsString.startsWith('0'.repeat(IDENTITY.DIFFICULTY))) return true;
|
|
58
65
|
}
|
|
59
66
|
/** @param {Uint8Array} publicKey */
|
|
@@ -69,7 +76,8 @@ export class CryptoCodex {
|
|
|
69
76
|
if (!asPublicNode && this.isPublicNode(id)) throw new Error('Seed does not produce a private node identity.');
|
|
70
77
|
if (!await this.pubkeyDifficultyCheck(publicKey)) throw new Error('Seed does not meet difficulty requirements.');
|
|
71
78
|
this.id = id;
|
|
72
|
-
this.privateKey = secretKey;
|
|
79
|
+
this.privateKey = secretKey;
|
|
80
|
+
this.publicKey = publicKey;
|
|
73
81
|
}
|
|
74
82
|
/** @param {boolean} asPublicNode */
|
|
75
83
|
static async generateNewSybilIdentity(asPublicNode, log = true) {
|
|
@@ -93,6 +101,7 @@ export class CryptoCodex {
|
|
|
93
101
|
const { secretKey, publicKey } = x25519.keygen(seed);
|
|
94
102
|
return { myPub: publicKey, myPriv: secretKey };
|
|
95
103
|
}
|
|
104
|
+
/** @param {Uint8Array} secret @param {Uint8Array} pub */
|
|
96
105
|
computeX25519SharedSecret(secret, pub) {
|
|
97
106
|
return x25519.getSharedSecret(secret, pub);
|
|
98
107
|
}
|
|
@@ -103,11 +112,6 @@ export class CryptoCodex {
|
|
|
103
112
|
const cipher = chacha20poly1305(sharedSecret, nonce);
|
|
104
113
|
const encrypted = cipher.encrypt(data);
|
|
105
114
|
return concatBytes(nonce, encrypted);
|
|
106
|
-
// Vanilla =>
|
|
107
|
-
/*const result = new Uint8Array(nonce.length + encrypted.length);
|
|
108
|
-
result.set(nonce, 0);
|
|
109
|
-
result.set(encrypted, nonce.length);
|
|
110
|
-
return result;*/
|
|
111
115
|
}
|
|
112
116
|
/** @param {Uint8Array} encryptedData @param {Uint8Array} sharedSecret */
|
|
113
117
|
decryptData(encryptedData, sharedSecret) {
|
|
@@ -126,11 +130,12 @@ export class CryptoCodex {
|
|
|
126
130
|
const signature = ed25519.sign(dataToSign, privateKey);
|
|
127
131
|
bufferView.set(signature, signaturePosition);
|
|
128
132
|
}
|
|
129
|
-
/** @param {string} topic @param {string | Uint8Array | Object} data @param {number} [HOPS] @param {string[]}
|
|
133
|
+
/** @param {string} topic @param {string | Uint8Array | Object} data @param {number} [HOPS] @param {string[]} [neighbors] */
|
|
130
134
|
createGossipMessage(topic, data, HOPS = 3, neighbors = [], timestamp = CLOCK.time) {
|
|
131
135
|
const MARKER = GOSSIP.MARKERS_BYTES[topic];
|
|
132
|
-
if (MARKER
|
|
133
|
-
|
|
136
|
+
if (typeof MARKER !== 'number') throw new Error(`Failed to create gossip message: wrong topic '${topic}'.`);
|
|
137
|
+
if (typeof timestamp !== 'number') throw new Error('Wrong timestamp type!');
|
|
138
|
+
|
|
134
139
|
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
135
140
|
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
136
141
|
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + dataBytes.length + IDENTITY.SIGNATURE_LENGTH + 1;
|
|
@@ -151,12 +156,13 @@ export class CryptoCodex {
|
|
|
151
156
|
clone[serializedMessage.length - 1] = Math.max(0, hops - 1);
|
|
152
157
|
return clone;
|
|
153
158
|
}
|
|
154
|
-
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] @param {Uint8Array} [encryptionKey]
|
|
159
|
+
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] @param {Uint8Array} [encryptionKey] */
|
|
155
160
|
createUnicastMessage(type, data, route, neighbors = [], encryptionKey, timestamp = CLOCK.time) {
|
|
156
161
|
const MARKER = UNICAST.MARKERS_BYTES[type];
|
|
157
|
-
if (MARKER
|
|
162
|
+
if (typeof MARKER !== 'number') throw new Error(`Failed to create gossip message: wrong type '${type}'.`);
|
|
163
|
+
if (typeof timestamp !== 'number') throw new Error('Wrong timestamp type!');
|
|
158
164
|
if (route.length < 2) throw new Error('Failed to create unicast message: route must have at least 2 nodes (next hop and target).');
|
|
159
|
-
if (route.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
165
|
+
if (route.length > UNICAST.MAX_HOPS + 1) throw new Error(`Failed to create unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
160
166
|
|
|
161
167
|
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
162
168
|
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
@@ -179,7 +185,7 @@ export class CryptoCodex {
|
|
|
179
185
|
/** @param {Uint8Array} serialized @param {string[]} newRoute */
|
|
180
186
|
createReroutedUnicastMessage(serialized, newRoute) {
|
|
181
187
|
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
|
-
if (newRoute.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create rerouted unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
188
|
+
if (newRoute.length > UNICAST.MAX_HOPS + 1) throw new Error(`Failed to create rerouted unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
183
189
|
|
|
184
190
|
const routeBytesArray = newRoute.map(id => this.converter.stringToBytes(id));
|
|
185
191
|
const totalBytes = serialized.length + 32 + (IDENTITY.ID_LENGTH * routeBytesArray.length) + IDENTITY.SIGNATURE_LENGTH;
|
|
@@ -194,7 +200,7 @@ export class CryptoCodex {
|
|
|
194
200
|
}
|
|
195
201
|
/** @param {string[]} ids */
|
|
196
202
|
#idsToBytes(ids) {
|
|
197
|
-
if (IDENTITY.ARE_IDS_HEX) return this.converter.hexToBytes(ids.join('')
|
|
203
|
+
if (IDENTITY.ARE_IDS_HEX) return this.converter.hexToBytes(ids.join(''));
|
|
198
204
|
return this.converter.stringToBytes(ids.join(''));
|
|
199
205
|
}
|
|
200
206
|
/** @param {string | Uint8Array | Object} data */
|
|
@@ -235,15 +241,17 @@ export class CryptoCodex {
|
|
|
235
241
|
const dataLength = this.converter.bytes4ToNumber(lBytes);
|
|
236
242
|
return { marker, dataCode, neighLength, timestamp, dataLength, pubkey, associatedId };
|
|
237
243
|
}
|
|
238
|
-
/** @param {Uint8Array
|
|
244
|
+
/** @param {Uint8Array} serialized */
|
|
239
245
|
readGossipMessage(serialized) {
|
|
240
246
|
if (this.verbose > 3) console.log(`%creadGossipMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
241
247
|
if (this.verbose > 4) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
242
248
|
let topic;
|
|
243
|
-
try { // 1, 1, 1, 8, 4, 32, X, 64, 1
|
|
249
|
+
try { // 1, 1, 1, 8, 4, 32, X, 64, 1
|
|
244
250
|
const { marker, dataCode, neighLength, timestamp, dataLength, pubkey, associatedId } = this.readBufferHeader(serialized);
|
|
245
251
|
topic = GOSSIP.MARKERS_BYTES[marker];
|
|
246
|
-
if (topic
|
|
252
|
+
if (typeof topic !== 'string') throw new Error(`Failed to deserialize gossip message: unknown marker byte ${serialized[0]}.`);
|
|
253
|
+
if (typeof associatedId !== 'string') throw new Error('Wrong associatedId tyoe!');
|
|
254
|
+
|
|
247
255
|
const NDBL = neighLength + dataLength;
|
|
248
256
|
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
249
257
|
const deserializedData = this.#bytesToData(dataCode, serialized.slice(47 + neighLength, 47 + NDBL));
|
|
@@ -251,12 +259,11 @@ export class CryptoCodex {
|
|
|
251
259
|
const signature = serialized.slice(signatureStart, signatureStart + IDENTITY.SIGNATURE_LENGTH);
|
|
252
260
|
const HOPS = serialized[serialized.length - 1];
|
|
253
261
|
const expectedEnd = signatureStart + IDENTITY.SIGNATURE_LENGTH + 1;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${topic || 'unknown'} gossip message:`, error.stack); }
|
|
262
|
+
return new GossipMessage(topic, timestamp, neighbors, HOPS, associatedId, pubkey, deserializedData, signature, signatureStart, expectedEnd);
|
|
263
|
+
} catch (/** @type {any} */ error) { if (this.verbose > 1) console.warn(`Error deserializing ${topic || 'unknown'} gossip message:`, error.stack); }
|
|
257
264
|
return null;
|
|
258
265
|
}
|
|
259
|
-
/** @param {Uint8Array
|
|
266
|
+
/** @param {Uint8Array} serialized @param {import('./peer-store.mjs').PeerStore} peerStore */
|
|
260
267
|
readUnicastMessage(serialized, peerStore) {
|
|
261
268
|
if (this.verbose > 3) console.log(`%creadUnicastMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
262
269
|
if (this.verbose > 4) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
@@ -264,7 +271,8 @@ export class CryptoCodex {
|
|
|
264
271
|
try { // 1, 1, 1, 8, 4, 32, X, 1, X, 64
|
|
265
272
|
const { marker, dataCode, neighLength, timestamp, dataLength, pubkey } = this.readBufferHeader(serialized, false);
|
|
266
273
|
type = UNICAST.MARKERS_BYTES[marker];
|
|
267
|
-
if (type
|
|
274
|
+
if (typeof type !== 'string') throw new Error(`Failed to deserialize unicast message: unknown marker byte ${serialized[0]}.`);
|
|
275
|
+
|
|
268
276
|
const NDBL = neighLength + dataLength;
|
|
269
277
|
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
270
278
|
const routeLength = serialized[47 + NDBL];
|
|
@@ -288,8 +296,8 @@ export class CryptoCodex {
|
|
|
288
296
|
const rerouterPubkey = serialized.slice(initialMessageEnd, initialMessageEnd + 32);
|
|
289
297
|
const newRoute = this.#bytesToIds(serialized.slice(initialMessageEnd + 32, serialized.length - IDENTITY.SIGNATURE_LENGTH));
|
|
290
298
|
const rerouterSignature = serialized.slice(serialized.length - IDENTITY.SIGNATURE_LENGTH);
|
|
291
|
-
return new
|
|
292
|
-
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${type || 'unknown'} unicast message:`, error.stack); }
|
|
299
|
+
return new DirectMessage(type, timestamp, neighbors, route, pubkey, deserializedData, signature, signatureStart, serialized.length, rerouterPubkey, newRoute, rerouterSignature);
|
|
300
|
+
} catch (/** @type {any} */ error) { if (this.verbose > 1) console.warn(`Error deserializing ${type || 'unknown'} unicast message:`, error.stack); }
|
|
293
301
|
return null;
|
|
294
302
|
}
|
|
295
303
|
/** @param {Uint8Array} serialized */
|
|
@@ -306,7 +314,7 @@ export class CryptoCodex {
|
|
|
306
314
|
}
|
|
307
315
|
return ids;
|
|
308
316
|
}
|
|
309
|
-
/** @param {1 | 2 | 3} dataCode @param {Uint8Array} dataBytes @return {string | Uint8Array | Object} */
|
|
317
|
+
/** @param {1 | 2 | 3 | number} dataCode @param {Uint8Array} dataBytes @return {string | Uint8Array | Object} */
|
|
310
318
|
#bytesToData(dataCode, dataBytes) {
|
|
311
319
|
if (dataCode === 1) return this.converter.bytesToString(dataBytes);
|
|
312
320
|
if (dataCode === 2) return dataBytes;
|
package/core/gossip.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { CLOCK } from '../services/clock.mjs';
|
|
2
3
|
import { GOSSIP } from './config.mjs';
|
|
3
4
|
import { xxHash32 } from '../libs/xxhash32.mjs';
|
|
@@ -14,7 +15,7 @@ export class GossipMessage { // TYPE DEFINITION
|
|
|
14
15
|
signatureStart; // position in the serialized message where the signature starts
|
|
15
16
|
expectedEnd; // expected length of the serialized message
|
|
16
17
|
|
|
17
|
-
/** @param {string} topic @param {number} timestamp @param {string[]} neighborsList @param {number} HOPS @param {string} senderId @param {
|
|
18
|
+
/** @param {string} topic @param {number} timestamp @param {string[]} neighborsList @param {number} HOPS @param {string} senderId @param {Uint8Array} pubkey @param {string | Uint8Array | Object} data @param {Uint8Array | undefined} signature @param {number} signatureStart @param {number} expectedEnd */
|
|
18
19
|
constructor(topic, timestamp, neighborsList, HOPS, senderId, pubkey, data, signature, signatureStart, expectedEnd) {
|
|
19
20
|
this.topic = topic; this.timestamp = timestamp; this.neighborsList = neighborsList;
|
|
20
21
|
this.HOPS = HOPS; this.senderId = senderId; this.pubkey = pubkey; this.data = data;
|
|
@@ -28,6 +29,7 @@ export class GossipMessage { // TYPE DEFINITION
|
|
|
28
29
|
* @property {string} senderId
|
|
29
30
|
* @property {string} topic
|
|
30
31
|
* @property {Uint8Array} serializedMessage
|
|
32
|
+
* @property {number} timestamp
|
|
31
33
|
* @property {number} expiration
|
|
32
34
|
*/
|
|
33
35
|
class DegenerateBloomFilter {
|
|
@@ -54,23 +56,31 @@ class DegenerateBloomFilter {
|
|
|
54
56
|
addMessage(serializedMessage) {
|
|
55
57
|
const n = CLOCK.time;
|
|
56
58
|
const { marker, neighLength, timestamp, dataLength, pubkey, associatedId } = this.cryptoCodex.readBufferHeader(serializedMessage);
|
|
57
|
-
|
|
59
|
+
const topic = GOSSIP.MARKERS_BYTES[marker];
|
|
60
|
+
if (typeof topic !== 'string') throw new Error(`Wrong topic byte: ${marker}`)
|
|
61
|
+
if (n === null || n - timestamp > GOSSIP.EXPIRATION) return;
|
|
62
|
+
if (!associatedId) return;
|
|
58
63
|
|
|
59
64
|
const hashableData = serializedMessage.subarray(0, 47 + neighLength + dataLength);
|
|
60
|
-
const h = xxHash32(hashableData);
|
|
65
|
+
const h = xxHash32(hashableData).toString();
|
|
61
66
|
this.xxHash32UsageCount++;
|
|
62
67
|
if (this.seenTimeouts[h]) return;
|
|
63
68
|
|
|
64
|
-
const topic = GOSSIP.MARKERS_BYTES[marker];
|
|
65
|
-
const senderId = associatedId;
|
|
66
69
|
const expiration = n + GOSSIP.CACHE_DURATION;
|
|
67
|
-
this.cache.push({
|
|
70
|
+
this.cache.push({
|
|
71
|
+
hash: h,
|
|
72
|
+
senderId: associatedId,
|
|
73
|
+
topic,
|
|
74
|
+
serializedMessage,
|
|
75
|
+
timestamp,
|
|
76
|
+
expiration
|
|
77
|
+
});
|
|
68
78
|
this.seenTimeouts[h] = expiration;
|
|
69
79
|
|
|
70
80
|
if (--this.cleanupIn <= 0) this.#cleanupOldestEntries(n);
|
|
71
81
|
return { hash: h, isNew: !this.seenTimeouts[h] };
|
|
72
82
|
}
|
|
73
|
-
#cleanupOldestEntries(n = CLOCK.time) {
|
|
83
|
+
#cleanupOldestEntries(n = CLOCK.time || 0) {
|
|
74
84
|
let firstValidIndex = -1;
|
|
75
85
|
for (let i = 0; i < this.cache.length; i++)
|
|
76
86
|
if (this.cache[i].expiration <= n) delete this.seenTimeouts[this.cache[i].hash];
|
|
@@ -83,7 +93,7 @@ class DegenerateBloomFilter {
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export class Gossip {
|
|
86
|
-
/** @type {Record<string, function
|
|
96
|
+
/** @type {Record<string, function[]>} */ callbacks = { message_handle: [] };
|
|
87
97
|
id; cryptoCodex; arbiter; peerStore; verbose; bloomFilter;
|
|
88
98
|
|
|
89
99
|
/** @param {string} selfId @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex @param {import('./arbiter.mjs').Arbiter} arbiter @param {import('./peer-store.mjs').PeerStore} peerStore */
|
|
@@ -96,7 +106,7 @@ export class Gossip {
|
|
|
96
106
|
this.bloomFilter = new DegenerateBloomFilter(cryptoCodex);
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
/** @param {string} callbackType @param {function
|
|
109
|
+
/** @param {string} callbackType @param {function} callback */
|
|
100
110
|
on(callbackType, callback) {
|
|
101
111
|
if (!this.callbacks[callbackType]) this.callbacks[callbackType] = [callback];
|
|
102
112
|
else this.callbacks[callbackType].push(callback);
|
|
@@ -118,11 +128,12 @@ export class Gossip {
|
|
|
118
128
|
try { transportInstance.send(serializedMessage); }
|
|
119
129
|
catch (error) { this.peerStore.connected[targetId]?.close(); }
|
|
120
130
|
}
|
|
131
|
+
/** @param {string} peerId */
|
|
121
132
|
sendGossipHistoryToPeer(peerId) {
|
|
122
133
|
const gossipHistory = this.bloomFilter.getGossipHistoryByTime('asc');
|
|
123
134
|
for (const entry of gossipHistory) this.#broadcastSerializedToPeer(peerId, entry.data);
|
|
124
135
|
}
|
|
125
|
-
/** @param {string} from @param {Uint8Array} serialized
|
|
136
|
+
/** @param {string} from @param {Uint8Array} serialized */
|
|
126
137
|
async handleGossipMessage(from, serialized) {
|
|
127
138
|
if (this.arbiter.isBanished(from)) return this.verbose >= 3 ? console.info(`%cReceived gossip message from banned peer ${from}, ignoring.`, 'color: red;') : null;
|
|
128
139
|
if (!this.arbiter.countMessageBytes(from, serialized.byteLength, 'gossip')) return; // ignore if flooding/banished
|
package/core/node.mjs
CHANGED
|
@@ -11,10 +11,8 @@ import { NodeServices } from './node-services.mjs';
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @typedef {import('./unicast.mjs').DirectMessage} DirectMessage
|
|
14
|
-
* @typedef {import('./unicast.mjs').ReroutedDirectMessage} ReroutedDirectMessage
|
|
15
14
|
* @typedef {import('./gossip.mjs').GossipMessage} GossipMessage
|
|
16
|
-
* @typedef {import('./topologist.mjs').SignalData} SignalData
|
|
17
|
-
*/
|
|
15
|
+
* @typedef {import('./topologist.mjs').SignalData} SignalData */
|
|
18
16
|
|
|
19
17
|
/** Create and start a new PublicNode instance.
|
|
20
18
|
* @param {Object} options
|
package/core/peer-store.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLOCK } from '../services/clock.mjs';
|
|
2
2
|
import { SIMULATION, NODE, DISCOVERY, LOG_CSS } from './config.mjs';
|
|
3
|
-
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.
|
|
3
|
+
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.USE_TEST_TRANSPORTS ? await import('../simulation/test-transports.mjs') : {};
|
|
4
4
|
|
|
5
5
|
export class KnownPeer { // known peer, not necessarily connected
|
|
6
6
|
neighbors; connectionsCount;
|
package/core/route-builder.mjs
CHANGED
|
@@ -55,11 +55,8 @@ export class RouteBuilder_V2 {
|
|
|
55
55
|
for (let i = 1; i < path.length; i++) {
|
|
56
56
|
const from = path[i - 1];
|
|
57
57
|
const to = path[i];
|
|
58
|
-
if (from === this.id)
|
|
59
|
-
|
|
60
|
-
} else {
|
|
61
|
-
if (!this.peerStore.known[from]?.neighbors?.[to]) return false;
|
|
62
|
-
}
|
|
58
|
+
if (from === this.id) if (!this.peerStore.connected[to]) return false;
|
|
59
|
+
else if (!this.peerStore.known[from]?.neighbors?.[to]) return false;
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
62
|
return true;
|
|
@@ -100,7 +97,7 @@ export class RouteBuilder_V2 {
|
|
|
100
97
|
const backwardQueue = [{ node: remoteId, path: [remoteId], pathSet: new Set([remoteId]), depth: 0 }];
|
|
101
98
|
const backwardVisited = new Map(); // node -> path from remoteId
|
|
102
99
|
backwardVisited.set(remoteId, [remoteId]);
|
|
103
|
-
|
|
100
|
+
|
|
104
101
|
const maxDepthPerSide = Math.ceil(maxHops / 2);
|
|
105
102
|
while ((forwardQueue.length > 0 || backwardQueue.length > 0) && nodesExplored < maxNodes) {
|
|
106
103
|
if (forwardQueue.length > 0) { // Expand forward search
|
package/core/topologist.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLOCK } from '../services/clock.mjs';
|
|
2
2
|
import { SIMULATION, TRANSPORTS, NODE, DISCOVERY, GOSSIP } from './config.mjs';
|
|
3
3
|
import { PeerConnection } from './peer-store.mjs';
|
|
4
|
-
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.
|
|
4
|
+
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.USE_TEST_TRANSPORTS ? await import('../simulation/test-transports.mjs') : {};
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} SignalData
|
package/core/unicast.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { SIMULATION, DISCOVERY, UNICAST } from "./config.mjs";
|
|
2
3
|
import { TRUST_VALUES } from "./arbiter.mjs";
|
|
3
4
|
import { RouteBuilder_V2 } from "./route-builder.mjs";
|
|
4
|
-
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.
|
|
5
|
+
//const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.USE_TEST_TRANSPORTS ? await import('../simulation/test-transports.mjs') : {};
|
|
5
6
|
const RouteBuilder = RouteBuilder_V2; // temporary switch
|
|
6
7
|
|
|
7
8
|
export class DirectMessage { // TYPE DEFINITION
|
|
@@ -14,13 +15,29 @@ export class DirectMessage { // TYPE DEFINITION
|
|
|
14
15
|
signature;
|
|
15
16
|
signatureStart; // position in the serialized message where the signature starts
|
|
16
17
|
expectedEnd; // expected length of the serialized message
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
/** Redirect only */ rerouterPubkey;
|
|
20
|
+
/** Redirect only */ newRoute; // for re-routing patch
|
|
21
|
+
/** Redirect only */ rerouterSignature;
|
|
22
|
+
|
|
18
23
|
get senderId() { return this.newRoute ? this.newRoute[0] : this.route[0]; }
|
|
24
|
+
get isRerouted() { return (this.rerouterPubkey && this.newRoute && this.rerouterSignature); }
|
|
25
|
+
getRerouterId() { if (this.newRoute) return this.newRoute[0]; }
|
|
19
26
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} type @param {number} timestamp @param {string[]} neighborsList
|
|
29
|
+
* @param {string[]} route @param {Uint8Array} pubkey @param {string | Uint8Array | Object} data
|
|
30
|
+
* @param {Uint8Array | undefined} signature @param {number} signatureStart @param {number} expectedEnd
|
|
31
|
+
* @param {Uint8Array} [rerouterPubkey] @param {string[]} [newRoute] @param {Uint8Array} [rerouterSignature] */
|
|
32
|
+
constructor(type, timestamp, neighborsList, route, pubkey, data, signature, signatureStart, expectedEnd, rerouterPubkey, newRoute, rerouterSignature) {
|
|
22
33
|
this.type = type; this.timestamp = timestamp; this.neighborsList = neighborsList;
|
|
23
|
-
this.route = route; this.pubkey = pubkey; this.data = data; this.signature = signature;
|
|
34
|
+
this.route = route; this.pubkey = pubkey; this.data = data; this.signature = signature;
|
|
35
|
+
this.signatureStart = signatureStart;
|
|
36
|
+
this.expectedEnd = expectedEnd;
|
|
37
|
+
|
|
38
|
+
this.rerouterPubkey = rerouterPubkey;
|
|
39
|
+
this.newRoute = newRoute;
|
|
40
|
+
this.rerouterSignature = rerouterSignature;
|
|
24
41
|
}
|
|
25
42
|
getSenderId() { return this.route[0]; }
|
|
26
43
|
getTargetId() { return this.route[this.route.length - 1]; }
|
|
@@ -40,18 +57,6 @@ export class DirectMessage { // TYPE DEFINITION
|
|
|
40
57
|
return { traveledRoute, selfPosition, senderId, targetId, prevId, nextId, routeLength: route.length };
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
|
-
export class ReroutedDirectMessage extends DirectMessage {
|
|
44
|
-
rerouterPubkey;
|
|
45
|
-
newRoute;
|
|
46
|
-
rerouterSignature;
|
|
47
|
-
|
|
48
|
-
/** @param {string} type @param {number} timestamp @param {string[]} route @param {string} pubkey @param {string | Uint8Array | Object} data @param {Uint8Array} rerouterPubkey @param {string | undefined} signature @param {string[]} newRoute @param {string} rerouterSignature */
|
|
49
|
-
constructor(type, timestamp, route, pubkey, data, signature, rerouterPubkey, newRoute, rerouterSignature) {
|
|
50
|
-
super(type, timestamp, route, pubkey, data, signature);
|
|
51
|
-
this.rerouterPubkey = rerouterPubkey; this.newRoute = newRoute; this.rerouterSignature = rerouterSignature; // patch
|
|
52
|
-
}
|
|
53
|
-
getRerouterId() { return this.newRoute[0]; }
|
|
54
|
-
}
|
|
55
60
|
|
|
56
61
|
export class UnicastMessager {
|
|
57
62
|
/** @type {Record<string, function(DirectMessage)[]>} */ callbacks = { message_handle: [] };
|
|
@@ -89,7 +94,7 @@ export class UnicastMessager {
|
|
|
89
94
|
const finalSpread = builtResult.success === 'blind' ? 1 : spread; // Spread only if re-routing is false
|
|
90
95
|
for (let i = 0; i < Math.min(finalSpread, builtResult.routes.length); i++) {
|
|
91
96
|
const route = builtResult.routes[i].path;
|
|
92
|
-
if (route.length > UNICAST.MAX_HOPS) {
|
|
97
|
+
if (route.length > UNICAST.MAX_HOPS + 1) {
|
|
93
98
|
if (this.verbose > 1) console.warn(`Cannot send unicast message to ${remoteId} as route exceeds maxHops (${UNICAST.MAX_HOPS}). BFS incurred.`);
|
|
94
99
|
continue; // too long route
|
|
95
100
|
}
|
|
@@ -105,7 +110,7 @@ export class UnicastMessager {
|
|
|
105
110
|
const transportInstance = this.peerStore.connected[targetId]?.transportInstance;
|
|
106
111
|
if (!transportInstance) return { success: false, reason: `Transport instance is not available for peer ${targetId}.` };
|
|
107
112
|
try { transportInstance.send(serialized); return { success: true }; }
|
|
108
|
-
catch (error) {
|
|
113
|
+
catch (/** @type {any} */ error) {
|
|
109
114
|
this.peerStore.kickPeer(targetId, 0, 'send-error');
|
|
110
115
|
if (this.verbose > 0) console.error(`Error sending message to ${targetId}:`, error.message);
|
|
111
116
|
}
|
|
@@ -141,14 +146,16 @@ export class UnicastMessager {
|
|
|
141
146
|
//if (this.id === targetId) { for (const cb of this.callbacks[message.type] || []) cb(senderId, message.data); return; } // message for self
|
|
142
147
|
if (this.id === targetId) { for (const cb of this.callbacks[message.type] || []) cb(message); return; } // message for self
|
|
143
148
|
|
|
144
|
-
// re-send the message to the next peer in the route
|
|
149
|
+
// re-send the message to the next peer in the route-
|
|
150
|
+
if (!nextId) throw new Error('Needs to tranmit message but no "nextId" provied!');
|
|
151
|
+
|
|
145
152
|
const { success, reason } = this.#sendMessageToPeer(nextId, serialized);
|
|
146
153
|
if (!success && !message.rerouterSignature) { // try to patch the route
|
|
147
154
|
const builtResult = this.pathFinder.buildRoutes(targetId, this.maxRoutes, this.maxHops, this.maxNodes, true);
|
|
148
155
|
if (!builtResult.success) return;
|
|
149
156
|
|
|
150
157
|
const newRoute = builtResult.routes[0].path;
|
|
151
|
-
if (newRoute.length > UNICAST.MAX_HOPS) {
|
|
158
|
+
if (newRoute.length > UNICAST.MAX_HOPS + 1) {
|
|
152
159
|
if (this.verbose > 1) console.warn(`Cannot re-route unicast message to ${targetId} as new route exceeds maxHops (${UNICAST.MAX_HOPS}).`);
|
|
153
160
|
return; // too long route
|
|
154
161
|
}
|