@hive-p2p/server 1.0.18 → 1.0.20

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.
@@ -0,0 +1,254 @@
1
+ import { CLOCK, SIMULATION, TRANSPORTS, NODE, DISCOVERY, GOSSIP } from './config.mjs';
2
+ import { PeerConnection } from './peer-store.mjs';
3
+ import { CryptoCodex } from './crypto-codex.mjs';
4
+ const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.ENABLED ? await import('../simulation/test-transports.mjs') : {};
5
+
6
+ /**
7
+ * @typedef {Object} SignalData
8
+ * @property {Array<string>} neighbors
9
+ * @property {Object} signal
10
+ * @property {'offer' | 'answer'} signal.type
11
+ * @property {string} signal.sdp
12
+ * @property {string} [offerHash]
13
+ *
14
+ * @typedef {Object} OfferQueueItem
15
+ * @property {string} senderId
16
+ * @property {SignalData} data
17
+ * @property {number} overlap
18
+ * @property {number} neighborsCount
19
+ * @property {number} timestamp
20
+ * */
21
+
22
+ class OfferQueue {
23
+ maxOffers = 30;
24
+ /** @type {Array<OfferQueueItem>} */ offers = [];
25
+ /** @type {'overlap' | 'neighborsCount'} */ orderingBy = 'neighborsCount';
26
+ get size() { return this.offers.length; }
27
+
28
+ updateOrderingBy(isHalfTargetReached = false) { this.orderingBy = isHalfTargetReached ? 'overlap' : 'neighborsCount'; }
29
+ removeOlderThan(age = 1000) { this.offers = this.offers.filter(item => item.timestamp + age >= CLOCK.time); }
30
+ get bestOfferInfo() {
31
+ const { senderId, overlap, neighborsCount, data, timestamp } = this.offers.shift() || {};
32
+ return { senderId, data, timestamp, value: this.orderingBy === 'overlap' ? overlap : neighborsCount };
33
+ }
34
+
35
+ /** @param {OfferQueueItem} offer @param {boolean} isHalfTargetReached @param {{min: number, max: number}} [ignoringFactors] */
36
+ pushSortTrim(offer, ignoringFactors = {min: .2, max: .8}) { // => 20%-80% to ignore the offer depending on the queue length
37
+ const { min, max } = ignoringFactors; // AVOID SIMULATOR FLOODING, AND AVOID ALL PEERS TO PROCESS SAME OFFERS
38
+ if (Math.random() < Math.min(min, this.offers.size / this.maxOffers * max)) return;
39
+
40
+ this.offers.push(offer);
41
+ if (this.offers.length === 1) return;
42
+
43
+ // SORT THE QUEUE: by overlap ASCENDING, or by neighborsCount DESCENDING
44
+ this.offers.sort((a, b) => this.orderingBy === 'overlap' ? a.overlap - b.overlap : b.neighborsCount - a.neighborsCount);
45
+ if (this.size > this.maxOffers) this.offers.pop();
46
+ }
47
+ }
48
+
49
+ export class Topologist {
50
+ id; gossip; messager; peerStore; bootstraps;
51
+ halfTarget = Math.ceil(DISCOVERY.TARGET_NEIGHBORS_COUNT / 2);
52
+ twiceTarget = DISCOVERY.TARGET_NEIGHBORS_COUNT * 2;
53
+ /** @type {Set<string>} */ bootstrapsIds = new Set();
54
+ get isPublicNode() { return this.services?.publicUrl ? true : false; }
55
+ /** @type {import('./node-services.mjs').NodeServices | undefined} */ services;
56
+
57
+ phase = 0;
58
+ nextBootstrapIndex = 0;
59
+ offersQueue = new OfferQueue();
60
+ maxBonus = NODE.CONNECTION_UPGRADE_TIMEOUT * .2; // 20% of 15sec: 3sec max
61
+
62
+ /** @param {string} selfId @param {import('./gossip.mjs').Gossip} gossip @param {import('./unicast.mjs').UnicastMessager} messager @param {import('./peer-store.mjs').PeerStore} peerStore @param {Array<{id: string, publicUrl: string}>} bootstraps */
63
+ constructor(selfId, gossip, messager, peerStore, bootstraps) {
64
+ this.id = selfId; this.gossip = gossip; this.messager = messager; this.peerStore = peerStore;
65
+ for (const bootstrap of bootstraps) this.bootstrapsIds.add(bootstrap.id);
66
+ this.bootstraps = [...bootstraps].sort(() => Math.random() - 0.5); // shuffle
67
+ this.nextBootstrapIndex = Math.random() * this.bootstrapsIds.size | 0;
68
+ }
69
+
70
+ // PUBLIC METHODS
71
+ tick() {
72
+ const { neighborsCount, nonPublicNeighborsCount, isEnough, isTooMany, isHalfReached } = this.#localTopologyInfo;
73
+ const offersToCreate = nonPublicNeighborsCount >= DISCOVERY.TARGET_NEIGHBORS_COUNT / 3 ? 1 : TRANSPORTS.MAX_SDP_OFFERS;
74
+ this.peerStore.offerManager.offersToCreate = isEnough ? 0 : offersToCreate;
75
+ if (this.isPublicNode) { this.services.freePublicNodeByKickingPeers(); return; } // public nodes don't need more connections
76
+ if (isTooMany) return Math.random() > .05 ? this.#improveTopologyByKickingPeers() : null;
77
+
78
+ if (!isEnough) this.#digestBestOffers(); // => needs more peers
79
+ else if (Math.random() > .05) this.#digestBestOffers(); // => sometimes, try topology improvement...
80
+
81
+ this.phase = this.phase ? 0 : 1;
82
+ if (this.phase === 0) this.tryConnectNextBootstrap(neighborsCount, nonPublicNeighborsCount);
83
+ if (this.phase === 1) this.#tryToSpreadSDP(nonPublicNeighborsCount, isHalfReached);
84
+ }
85
+ /** @param {string} peerId @param {SignalData} data @param {number} [HOPS] */
86
+ handleIncomingSignal(senderId, data, HOPS) {
87
+ if (this.isPublicNode || !senderId || this.peerStore.isKicked(senderId)) return;
88
+ if (data.signal?.type !== 'offer' && data.signal?.type !== 'answer') return;
89
+
90
+ const { signal, offerHash } = data || {}; // remoteInfo
91
+ const { connected, nonPublicNeighborsCount, isTooMany, isHalfReached } = this.#localTopologyInfo;
92
+ if (isTooMany || connected[senderId]) return;
93
+
94
+ if (signal.type === 'answer') { // ANSWER SHORT CIRCUIT => Rich should connect poor, and poor should connect rich.
95
+ if (this.peerStore.addConnectingPeer(senderId, signal, offerHash) !== true) return;
96
+ const delta = Math.abs(nonPublicNeighborsCount - this.#getOverlap(senderId).nonPublicCount);
97
+ const bonusPerDeltaPoint = this.maxBonus / DISCOVERY.TARGET_NEIGHBORS_COUNT; // from 0 to maxBonus
98
+ const bonus = Math.round(Math.min(this.maxBonus, delta * bonusPerDeltaPoint));
99
+ return this.peerStore.assignSignal(senderId, signal, offerHash, CLOCK.time + bonus);
100
+ }
101
+
102
+ // OFFER
103
+ if (nonPublicNeighborsCount > this.twiceTarget) return; // we are over connected, ignore the offer
104
+ const { overlap, nonPublicCount } = this.#getOverlap(senderId);
105
+ if (nonPublicCount > this.twiceTarget) return; // the sender is over connected, ignore the offer
106
+
107
+ const offerItem = { senderId, data, overlap, neighborsCount: nonPublicCount, timestamp: CLOCK.time };
108
+ this.offersQueue.updateOrderingBy(isHalfReached);
109
+ this.offersQueue.pushSortTrim(offerItem);
110
+ }
111
+ tryConnectNextBootstrap(neighborsCount = 0, nonPublicNeighborsCount = 0) {
112
+ if (this.bootstrapsIds.size === 0) return;
113
+ const publicConnectedCount = neighborsCount - nonPublicNeighborsCount;
114
+ let [connectingCount, publicConnectingCount] = [0, 0];
115
+ for (const id in this.peerStore.connecting)
116
+ if (this.bootstrapsIds.has(id)) publicConnectingCount++;
117
+ else connectingCount++;
118
+
119
+ // MINIMIZE BOOTSTRAP CONNECTIONS DEPENDING ON HOW MANY NEIGHBORS WE HAVE
120
+ if (publicConnectedCount + publicConnectingCount >= this.halfTarget) return; // already connected to enough bootstraps
121
+ if (neighborsCount >= DISCOVERY.TARGET_NEIGHBORS_COUNT) return; // no more bootstrap needed
122
+ if (connectingCount + nonPublicNeighborsCount > this.twiceTarget) return; // no more bootstrap needed
123
+
124
+ const { id, publicUrl } = this.bootstraps[this.nextBootstrapIndex++ % this.bootstrapsIds.size];
125
+ if (id && publicUrl && (this.peerStore.connected[id] || this.peerStore.connecting[id])) return;
126
+ this.#connectToPublicNode(id, publicUrl);
127
+ }
128
+
129
+ // INTERNAL METHODS
130
+ get #localTopologyInfo() {
131
+ return {
132
+ connected: this.peerStore.connected,
133
+ neighborsCount: this.peerStore.neighborsList.length,
134
+ nonPublicNeighborsCount: this.peerStore.standardNeighborsList.length,
135
+ isEnough: this.peerStore.standardNeighborsList.length >= DISCOVERY.TARGET_NEIGHBORS_COUNT,
136
+ isTooMany: this.peerStore.standardNeighborsList.length > DISCOVERY.TARGET_NEIGHBORS_COUNT,
137
+ isHalfReached: this.peerStore.standardNeighborsList.length >= this.halfTarget,
138
+ }
139
+ }
140
+ #getOverlap(peerId1 = 'toto') {
141
+ const p1n = this.peerStore.known[peerId1]?.neighbors || {};
142
+ const result = { overlap: 0, nonPublicCount: 0, p1nCount: this.peerStore.getUpdatedPeerConnectionsCount(peerId1) };
143
+ for (const id in p1n) if (!CryptoCodex.isPublicNode(id)) result.nonPublicCount++;
144
+ for (const id of this.peerStore.standardNeighborsList) if (p1n[id]) result.overlap++;
145
+ return result;
146
+ }
147
+ /** Get overlap information for multiple peers @param {string[]} peerIds */
148
+ #getOverlaps(peerIds = []) { return peerIds.map(id => ({ id, ...this.#getOverlap(id) })); }
149
+ #connectToPublicNode(remoteId = 'toto', publicUrl = 'localhost:8080') {
150
+ if (!CryptoCodex.isPublicNode(remoteId)) return this.verbose < 1 ? null : console.warn(`Topologist: trying to connect to a non-public node (${remoteId})`);
151
+ const ws = new TRANSPORTS.WS_CLIENT(publicUrl); ws.binaryType = 'arraybuffer';
152
+ if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
153
+ this.peerStore.connecting[remoteId].out = new PeerConnection(remoteId, ws, 'out', true);
154
+ ws.onerror = (error) => console.error(`WebSocket error:`, error.stack);
155
+ ws.onopen = () => {
156
+ ws.onclose = () => { for (const cb of this.peerStore.callbacks.disconnect) cb(remoteId, 'out'); }
157
+ ws.onmessage = (data) => { for (const cb of this.peerStore.callbacks.data) cb(remoteId, data.data); };
158
+ for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
159
+ };
160
+ }
161
+ #tryToSpreadSDP(nonPublicNeighborsCount = 0, isHalfReached = false) { // LOOP TO SELECT ONE UNSEND READY OFFER AND BROADCAST IT
162
+ // LIMIT OFFER SPREADING IF WE ARE CONNECTING TO MANY PEERS, LOWER GOSSIP TRAFFIC
163
+ const connectingCount = Object.keys(this.peerStore.connecting).length;
164
+ const ingPlusEd = connectingCount + nonPublicNeighborsCount;
165
+
166
+ // SELECT BEST READY OFFER BASED ON TIMESTAMP
167
+ let [ offerHash, readyOffer, since ] = [ null, null, null ];
168
+ for (const hash in this.peerStore.offerManager.offers) {
169
+ const offer = this.peerStore.offerManager.offers[hash];
170
+ const { isUsed, sentCounter, signal, timestamp } = offer;
171
+ if (isUsed || sentCounter > 0) continue; // already used or already sent at least once
172
+ const createdSince = CLOCK.time - timestamp;
173
+ if (createdSince > TRANSPORTS.SDP_OFFER_EXPIRATION / 2) continue; // old, don't spread
174
+ if (since && createdSince > since) continue; // already have a better (more recent) offer
175
+ readyOffer = offer; offerHash = hash; since = createdSince;
176
+ break;
177
+ }
178
+ if (!offerHash || !readyOffer) return; // no ready offer to spread
179
+
180
+ // IF WE ARE CONNECTED TO LESS 2 (WRTC) AND NOT TO MUCH CONNECTING, WE CAN BROADCAST IT TO ALL
181
+ if (!isHalfReached && ingPlusEd <= this.twiceTarget) {
182
+ this.gossip.broadcastToAll({ signal: readyOffer.signal, offerHash }, 'signal_offer');
183
+ readyOffer.sentCounter++; // avoid sending it again
184
+ return; // limit to one per loop
185
+ }
186
+
187
+ let bestValue = null;
188
+ for (const overlapInfo of this.#getOverlaps(this.peerStore.standardNeighborsList)) {
189
+ const value = overlapInfo[isHalfReached ? 'overlap' : 'nonPublicCount'];
190
+ if (bestValue === null) bestValue = value;
191
+ if (isHalfReached && value < bestValue) bestValue = value;
192
+ if (!isHalfReached && value <= bestValue) bestValue = value;
193
+ }
194
+
195
+ let maxIds = 100; let maxSearch = 1000; const knownCount = this.peerStore.knownCount;
196
+ const r = Math.max(Math.min(maxSearch / knownCount, knownCount / maxSearch), .127);
197
+ const selectedIds = []; // ELSE, SEND USING UNICAST TO THE BEST 10 CANDIDATES
198
+ for (const id in this.peerStore.known) {
199
+ if (Math.random() > r) continue; // randomize a bit the search
200
+ if (--maxSearch <= 0) break;
201
+ if (id === this.id || CryptoCodex.isPublicNode(id) || this.peerStore.isKicked(id)) continue;
202
+ else if (this.peerStore.connected[id] || this.peerStore.connecting[id]) continue;
203
+
204
+ const { overlap, nonPublicCount } = this.#getOverlap(id);
205
+ if (nonPublicCount > DISCOVERY.TARGET_NEIGHBORS_COUNT) continue; // the peer is over connected, ignore it
206
+ if (bestValue === null) bestValue = isHalfReached ? overlap : nonPublicCount;
207
+ if (isHalfReached && overlap > bestValue) continue; // only target lowest overlap
208
+ if (!isHalfReached && nonPublicCount < bestValue) continue; // only target highest neighbors count
209
+ selectedIds.push(id);
210
+ if (--maxIds <= 0) break;
211
+ }
212
+ if (!selectedIds.length) return;
213
+
214
+ const sentTo = new Map();
215
+ for (let i = 0; i < Math.min(selectedIds.length, 100); i++) {
216
+ const peerId = selectedIds[Math.floor(Math.random() * selectedIds.length)];
217
+ if (sentTo.has(peerId)) continue;
218
+ if (sentTo.size === 0) readyOffer.sentCounter++;
219
+ sentTo.set(peerId, true);
220
+ this.messager.sendUnicast(peerId, { signal: readyOffer.signal, offerHash }, 'signal_offer', 1);
221
+ if (sentTo.size >= 12) break; // limit to 12 unicast max
222
+ }
223
+ }
224
+ /** Process signal_offer queue by filtering fresh offers and answering these with best:
225
+ * - Lowest overlap if we already have neighbors
226
+ * - Highest neighbors count if we don't */
227
+ #digestBestOffers() {
228
+ let bestValue = null;
229
+ let connectingCount = Object.keys(this.peerStore.connecting).length;
230
+ this.offersQueue.updateOrderingBy(this.#localTopologyInfo.isHalfReached);
231
+ this.offersQueue.removeOlderThan(TRANSPORTS.SDP_OFFER_EXPIRATION / 2); // remove close to expiration offers
232
+ for (let i = 0; i < this.offersQueue.size; i++) {
233
+ //if (connectingCount > this.twiceTarget * 2) break; // stop if we are over connecting
234
+ const { senderId, data, timestamp, value } = this.offersQueue.bestOfferInfo;
235
+ if (!senderId || !data || !timestamp) break;
236
+ if (this.peerStore.connected[senderId] || this.peerStore.isKicked(senderId)) continue;
237
+ if (this.peerStore.connecting[senderId]?.['in']) continue;
238
+ bestValue = bestValue === null ? value : bestValue;
239
+ if (bestValue !== value) break; // stop if the value is not the best anymore
240
+
241
+ if (this.peerStore.addConnectingPeer(senderId, data.signal, data.offerHash) !== true) continue;
242
+ this.peerStore.assignSignal(senderId, data.signal, data.offerHash, timestamp);
243
+ connectingCount++;
244
+ }
245
+ }
246
+
247
+ /** Kick the peer with the biggest overlap (any round of 2.5sec is isTooMany)
248
+ * - If all peers have the same overlap, kick the one with the most non-public neighbors */
249
+ #improveTopologyByKickingPeers() {
250
+ const overlaps = this.#getOverlaps(this.peerStore.standardNeighborsList);
251
+ const sortedPeers = overlaps.sort((a, b) => b.overlap - a.overlap || b.nonPublicCount - a.nonPublicCount);
252
+ this.peerStore.kickPeer(sortedPeers[0].id, 60_000, 'improveTopology');
253
+ }
254
+ }
@@ -0,0 +1,155 @@
1
+ import { SIMULATION, DISCOVERY, UNICAST } from "./config.mjs";
2
+ import { TRUST_VALUES } from "./arbiter.mjs";
3
+ 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 RouteBuilder = RouteBuilder_V2; // temporary switch
6
+
7
+ export class DirectMessage { // TYPE DEFINITION
8
+ type = 'message';
9
+ timestamp;
10
+ neighborsList;
11
+ route;
12
+ pubkey;
13
+ data;
14
+ signature;
15
+ signatureStart; // position in the serialized message where the signature starts
16
+ expectedEnd; // expected length of the serialized message
17
+
18
+ /** @param {string} type @param {number} timestamp @param {string[]} neighborsList @param {string[]} route @param {string} pubkey @param {string | Uint8Array | Object} data @param {string | undefined} signature @param {number} signatureStart @param {number} expectedEnd */
19
+ constructor(type, timestamp, neighborsList, route, pubkey, data, signature, signatureStart, expectedEnd) {
20
+ this.type = type; this.timestamp = timestamp; this.neighborsList = neighborsList;
21
+ this.route = route; this.pubkey = pubkey; this.data = data; this.signature = signature; this.signatureStart = signatureStart; this.expectedEnd = expectedEnd;
22
+ }
23
+ getSenderId() { return this.route[0]; }
24
+ getTargetId() { return this.route[this.route.length - 1]; }
25
+ extractRouteInfo(selfId = 'toto') {
26
+ const route = this.newRoute || this.route;
27
+ const traveledRoute = [];
28
+ let selfPosition = -1;
29
+ for (let i = 0; i < route.length; i++) {
30
+ traveledRoute.push(route[i]);
31
+ if (route[i] === selfId) { selfPosition = i; break; }
32
+ }
33
+ const senderId = route[0];
34
+ const targetId = route[route.length - 1];
35
+ const prevId = selfPosition > 0 ? route[selfPosition - 1] : null;
36
+ const nextId = (selfPosition !== -1) ? route[selfPosition + 1] : null;
37
+ return { traveledRoute, selfPosition, senderId, targetId, prevId, nextId, routeLength: route.length };
38
+ }
39
+ }
40
+ export class ReroutedDirectMessage extends DirectMessage {
41
+ rerouterPubkey;
42
+ newRoute;
43
+ rerouterSignature;
44
+
45
+ /** @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 */
46
+ constructor(type, timestamp, route, pubkey, data, signature, rerouterPubkey, newRoute, rerouterSignature) {
47
+ super(type, timestamp, route, pubkey, data, signature);
48
+ this.rerouterPubkey = rerouterPubkey; this.newRoute = newRoute; this.rerouterSignature = rerouterSignature; // patch
49
+ }
50
+ getRerouterId() { return this.newRoute[0]; }
51
+ }
52
+
53
+ export class UnicastMessager {
54
+ /** @type {Record<string, Function[]>} */ callbacks = { message_handle: [] };
55
+ id; cryptoCodex; arbiter; peerStore; verbose; pathFinder;
56
+
57
+ maxHops = UNICAST.MAX_HOPS;
58
+ maxRoutes = UNICAST.MAX_ROUTES;
59
+ maxNodes = UNICAST.MAX_NODES;
60
+
61
+ /** @param {string} selfId @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex @param {import('./arbiter.mjs').Arbiter} arbiter @param {import('./peer-store.mjs').PeerStore} peerStore */
62
+ constructor(selfId, cryptoCodex, arbiter, peerStore, verbose = 0) {
63
+ this.id = selfId;
64
+ this.cryptoCodex = cryptoCodex;
65
+ this.arbiter = arbiter;
66
+ this.peerStore = peerStore;
67
+ this.verbose = verbose;
68
+ this.pathFinder = new RouteBuilder(this.id, this.peerStore);
69
+ }
70
+
71
+ /** @param {string} callbackType @param {Function} callback */
72
+ on(callbackType, callback) {
73
+ if (!this.callbacks[callbackType]) this.callbacks[callbackType] = [callback];
74
+ else this.callbacks[callbackType].push(callback);
75
+ }
76
+ /** Send unicast message to a target
77
+ * @param {string} remoteId @param {string | Uint8Array | Object} data @param {string} type
78
+ * @param {number} [spread] Max neighbors used to relay the message, default: 1 */
79
+ sendUnicast(remoteId, data, type = 'message', spread = 1) {
80
+ if (remoteId === this.id) return false;
81
+
82
+ const builtResult = this.pathFinder.buildRoutes(remoteId, this.maxRoutes, this.maxHops, this.maxNodes, true);
83
+ if (!builtResult.success) return false;
84
+
85
+ // Caution: re-routing usage who can involve insane results
86
+ const finalSpread = builtResult.success === 'blind' ? 1 : spread; // Spread only if re-routing is false
87
+ for (let i = 0; i < Math.min(finalSpread, builtResult.routes.length); i++) {
88
+ const route = builtResult.routes[i].path;
89
+ if (route.length > UNICAST.MAX_HOPS) {
90
+ if (this.verbose > 1) console.warn(`Cannot send unicast message to ${remoteId} as route exceeds maxHops (${UNICAST.MAX_HOPS}). BFS incurred.`);
91
+ continue; // too long route
92
+ }
93
+ const message = this.cryptoCodex.createUnicastMessage(type, data, route, this.peerStore.neighborsList);
94
+ this.#sendMessageToPeer(route[1], message); // send to next peer
95
+ }
96
+ return true;
97
+ }
98
+ /** @param {string} targetId @param {Uint8Array} serialized */
99
+ #sendMessageToPeer(targetId, serialized) {
100
+ if (this.id === targetId) return { success: false, reason: `Cannot send message to self.` };
101
+ const transportInstance = this.peerStore.connected[targetId]?.transportInstance;
102
+ if (!transportInstance) return { success: false, reason: `Transport instance is not available for peer ${targetId}.` };
103
+ try { transportInstance.send(serialized); return { success: true }; }
104
+ catch (error) {
105
+ this.peerStore.kickPeer(targetId, 0, 'send-error');
106
+ if (this.verbose > 0) console.error(`Error sending message to ${targetId}:`, error.message);
107
+ }
108
+ return { success: false, reason: `Error sending message to ${targetId}.` };
109
+ }
110
+ /** @param {string} from @param {Uint8Array} serialized */
111
+ async handleDirectMessage(from, serialized) {
112
+ if (this.arbiter.isBanished(from)) return this.verbose >= 3 ? console.info(`%cReceived direct message from banned peer ${from}, ignoring.`, 'color: red;') : null;
113
+ if (!this.arbiter.countMessageBytes(from, serialized.byteLength, 'unicast')) return; // ignore if flooding/banished
114
+
115
+ const message = this.cryptoCodex.readUnicastMessage(serialized);
116
+ if (!message) return this.arbiter.countPeerAction(from, 'WRONG_SERIALIZATION');
117
+ await this.arbiter.digestMessage(from, message, serialized);
118
+ if (this.arbiter.isBanished(from)) return; // ignore messages from banished peers
119
+ if (this.arbiter.isBanished(message.senderId)) return; // ignore messages from banished peers
120
+
121
+ const { traveledRoute, selfPosition, senderId, targetId, prevId, nextId } = message.extractRouteInfo(this.id);
122
+ if (from === senderId && from === this.id) throw new Error('DirectMessage senderId and from are both self id !!');
123
+
124
+ for (const cb of this.callbacks.message_handle || []) cb(); // Simulator counter
125
+ if (selfPosition === -1) return this.arbiter.adjustTrust(from, TRUST_VALUES.UNICAST_INVALID_ROUTE, 'Self not in route');
126
+ if (prevId && from !== prevId) return this.arbiter.adjustTrust(from, TRUST_VALUES.UNICAST_INVALID_ROUTE, 'Previous hop id does not match actual from id');
127
+ if (senderId === this.id) return this.arbiter.adjustTrust(from, TRUST_VALUES.UNICAST_INVALID_ROUTE, 'SenderId is self id');
128
+
129
+ if (this.verbose > 3)
130
+ if (senderId === from) console.log(`(${this.id}) Direct ${message.type} from ${senderId}: ${message.data}`);
131
+ else console.log(`(${this.id}) Direct ${message.type} from ${senderId} (lastRelay: ${from}): ${message.data}`);
132
+
133
+ this.peerStore.digestPeerNeighbors(senderId, message.neighborsList);
134
+ if (from !== senderId) this.arbiter.adjustTrust(from, TRUST_VALUES.UNICAST_RELAYED, 'Relayed unicast message');
135
+ if (DISCOVERY.ON_UNICAST.DIGEST_TRAVELED_ROUTE) this.peerStore.digestValidRoute(traveledRoute);
136
+ if (this.id === targetId) { for (const cb of this.callbacks[message.type] || []) cb(senderId, message.data); return; } // message for self
137
+
138
+ // re-send the message to the next peer in the route
139
+ const { success, reason } = this.#sendMessageToPeer(nextId, serialized);
140
+ if (!success && !message.rerouterSignature) { // try to patch the route
141
+ const builtResult = this.pathFinder.buildRoutes(targetId, this.maxRoutes, this.maxHops, this.maxNodes, true);
142
+ if (!builtResult.success) return;
143
+
144
+ const newRoute = builtResult.routes[0].path;
145
+ if (newRoute.length > UNICAST.MAX_HOPS) {
146
+ if (this.verbose > 1) console.warn(`Cannot re-route unicast message to ${targetId} as new route exceeds maxHops (${UNICAST.MAX_HOPS}).`);
147
+ return; // too long route
148
+ }
149
+
150
+ const patchedMessage = this.cryptoCodex.createReroutedUnicastMessage(serialized, newRoute);
151
+ const nextPeerId = newRoute[selfPosition + 1];
152
+ this.#sendMessageToPeer(nextPeerId, patchedMessage);
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,65 @@
1
+ const PRIME32_1 = 2654435761;
2
+ const PRIME32_2 = 2246822519;
3
+ const PRIME32_3 = 3266489917;
4
+ const PRIME32_4 = 668265263;
5
+ const PRIME32_5 = 374761393;
6
+ let encoder;
7
+ /** @param input - byte array or string @param seed - optional seed (32-bit unsigned); */
8
+ export function xxHash32(input, seed = 0) {
9
+ const buffer = typeof input === 'string' ? (encoder ??= new TextEncoder()).encode(input) : input;
10
+ const b = buffer;
11
+ let acc = (seed + PRIME32_5) & 0xffffffff;
12
+ let offset = 0;
13
+ if (b.length >= 16) {
14
+ const accN = [
15
+ (seed + PRIME32_1 + PRIME32_2) & 0xffffffff,
16
+ (seed + PRIME32_2) & 0xffffffff,
17
+ (seed + 0) & 0xffffffff,
18
+ (seed - PRIME32_1) & 0xffffffff,
19
+ ];
20
+ const b = buffer;
21
+ const limit = b.length - 16;
22
+ let lane = 0;
23
+ for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) {
24
+ const i = offset;
25
+ const laneN0 = b[i + 0] + (b[i + 1] << 8);
26
+ const laneN1 = b[i + 2] + (b[i + 3] << 8);
27
+ const laneNP = laneN0 * PRIME32_2 + ((laneN1 * PRIME32_2) << 16);
28
+ let acc = (accN[lane] + laneNP) & 0xffffffff;
29
+ acc = (acc << 13) | (acc >>> 19);
30
+ const acc0 = acc & 0xffff;
31
+ const acc1 = acc >>> 16;
32
+ accN[lane] = (acc0 * PRIME32_1 + ((acc1 * PRIME32_1) << 16)) & 0xffffffff;
33
+ lane = (lane + 1) & 0x3;
34
+ }
35
+ acc =
36
+ (((accN[0] << 1) | (accN[0] >>> 31)) +
37
+ ((accN[1] << 7) | (accN[1] >>> 25)) +
38
+ ((accN[2] << 12) | (accN[2] >>> 20)) +
39
+ ((accN[3] << 18) | (accN[3] >>> 14))) &
40
+ 0xffffffff;
41
+ }
42
+ acc = (acc + buffer.length) & 0xffffffff;
43
+ const limit = buffer.length - 4;
44
+ for (; offset <= limit; offset += 4) {
45
+ const i = offset;
46
+ const laneN0 = b[i + 0] + (b[i + 1] << 8);
47
+ const laneN1 = b[i + 2] + (b[i + 3] << 8);
48
+ const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16);
49
+ acc = (acc + laneP) & 0xffffffff;
50
+ acc = (acc << 17) | (acc >>> 15);
51
+ acc = ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) & 0xffffffff;
52
+ }
53
+ for (; offset < b.length; ++offset) {
54
+ const lane = b[offset];
55
+ acc = acc + lane * PRIME32_5;
56
+ acc = (acc << 11) | (acc >>> 21);
57
+ acc = ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) & 0xffffffff;
58
+ }
59
+ acc = acc ^ (acc >>> 15);
60
+ acc = (((acc & 0xffff) * PRIME32_2) & 0xffffffff) + (((acc >>> 16) * PRIME32_2) << 16);
61
+ acc = acc ^ (acc >>> 13);
62
+ acc = (((acc & 0xffff) * PRIME32_3) & 0xffffffff) + (((acc >>> 16) * PRIME32_3) << 16);
63
+ acc = acc ^ (acc >>> 16);
64
+ return acc < 0 ? acc + 4294967296 : acc;
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hive-p2p/server",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -9,10 +9,10 @@
9
9
  "main": "index.mjs",
10
10
  "files": [
11
11
  "index.mjs",
12
- "core\\",
13
- "libs\\xxhash32.mjs",
14
- "rendering\\",
15
- "services\\"
12
+ "core/",
13
+ "libs/xxhash32.mjs",
14
+ "rendering/",
15
+ "services/"
16
16
  ],
17
17
  "dependencies": {
18
18
  "@noble/ed25519": "3.0.0",