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 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',
@@ -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, ReroutedDirectMessage } from './unicast.mjs';
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
- /** @type {string} */ id;
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, this.verbose);
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) { return (IDENTITY.ARE_IDS_HEX ? Converter.hexToBits(id[0]) : id).startsWith(IDENTITY.PUBLIC_PREFIX); }
41
- /** @param {string} id */
42
- get idLength() { return IDENTITY.ARE_IDS_HEX ? IDENTITY.ID_LENGTH / 2 : IDENTITY.ID_LENGTH; }
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.nodeId) return;
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 { bitsString } = await this.argon2.hash(publicKey, 'HiveP2P', IDENTITY.ARGON2_MEM) || {};
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; this.publicKey = publicKey;
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[]} route @param {string[]} [neighbors] */
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 === undefined) throw new Error(`Failed to create gossip message: unknown topic '${topic}'.`);
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] @param {number} [timestamp] */
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 === undefined) throw new Error(`Failed to create unicast message: unknown type '${type}'.`);
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(''), IDENTITY.ID_LENGTH * ids.length);
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 | ArrayBuffer} serialized @return {GossipMessage | null } */
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 === undefined) throw new Error(`Failed to deserialize gossip message: unknown marker byte ${d[0]}.`);
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
- const senderId = associatedId;
255
- return new GossipMessage(topic, timestamp, neighbors, HOPS, senderId, pubkey, deserializedData, signature, signatureStart, expectedEnd);
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 | ArrayBuffer} serialized @param {import('./peer-store.mjs').PeerStore} peerStore @return {DirectMessage | ReroutedDirectMessage | null} */
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 === undefined) throw new Error(`Failed to deserialize unicast message: unknown marker byte ${d[0]}.`);
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 ReroutedDirectMessage(type, timestamp, neighbors, route, pubkey, deserializedData, signature, rerouterPubkey, newRoute, rerouterSignature, serialized.length);
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 {string} pubkey @param {string | Uint8Array | Object} data @param {string | undefined} signature @param {number} signatureStart @param {number} expectedEnd */
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
- if (n - timestamp > GOSSIP.EXPIRATION) return;
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({ hash: h, senderId, topic, serializedMessage, timestamp, expiration });
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(GossipMessage)[]>} */ callbacks = { message_handle: [] };
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(GossipMessage)} callback */
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 @returns {void} */
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
@@ -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.ENABLED ? await import('../simulation/test-transports.mjs') : {};
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;
@@ -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
- if (!this.peerStore.connected[to]) return false;
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
@@ -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.ENABLED ? await import('../simulation/test-transports.mjs') : {};
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.ENABLED ? await import('../simulation/test-transports.mjs') : {};
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
- /** @type {string[] | undefined} */ newRoute; // for re-routing patch
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
- /** @param {string} type @param {number} timestamp @param {string[]} neighborsList @param {string[]} route @param {Uint8Array} pubkey @param {string | Uint8Array | Object} data @param {Uint8Array | undefined} signature @param {number} signatureStart @param {number} expectedEnd */
21
- constructor(type, timestamp, neighborsList, route, pubkey, data, signature, signatureStart, expectedEnd) {
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; this.signatureStart = signatureStart; this.expectedEnd = expectedEnd;
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
  }