@hive-p2p/server 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/arbiter.mjs +125 -0
- package/core/config.mjs +226 -0
- package/core/crypto-codex.mjs +257 -0
- package/core/gossip.mjs +159 -0
- package/core/ice-offer-manager.mjs +181 -0
- package/core/node-services.mjs +129 -0
- package/core/node.mjs +177 -0
- package/core/peer-store.mjs +252 -0
- package/core/route-builder.mjs +176 -0
- package/core/topologist.mjs +254 -0
- package/core/unicast.mjs +155 -0
- package/index.mjs +3 -3
- package/libs/xxhash32.mjs +65 -0
- package/package.json +5 -5
- package/rendering/NetworkRenderer.mjs +734 -0
- package/rendering/renderer-options.mjs +85 -0
- package/rendering/renderer-stores.mjs +234 -0
- package/rendering/visualizer.css +138 -0
- package/rendering/visualizer.html +60 -0
- package/rendering/visualizer.mjs +254 -0
- package/services/clock-v2.mjs +196 -0
- package/services/clock.mjs +144 -0
- package/services/converter.mjs +64 -0
- package/services/cryptos.mjs +83 -0
|
@@ -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
|
+
}
|
package/core/unicast.mjs
ADDED
|
@@ -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
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Node, createNode, createPublicNode } from "
|
|
2
|
-
import { CryptoCodex } from "
|
|
3
|
-
import CONFIG from "
|
|
1
|
+
import { Node, createNode, createPublicNode } from "./core/node.mjs";
|
|
2
|
+
import { CryptoCodex } from "./core/crypto-codex.mjs";
|
|
3
|
+
import CONFIG from "./core/config.mjs";
|
|
4
4
|
|
|
5
5
|
const HiveP2P = { Node, createNode, createPublicNode, CryptoCodex, CONFIG };
|
|
6
6
|
export { Node, createNode, createPublicNode, CryptoCodex, CONFIG };
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.21",
|
|
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
|
|
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",
|