hive-p2p 1.0.7 → 1.0.9
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 +1 -1
- package/core/config.mjs +227 -0
- package/core/crypto-codex.mjs +10 -2
- package/core/gossip.mjs +1 -1
- package/core/ice-offer-manager.mjs +10 -4
- package/core/node-services.mjs +13 -10
- package/core/node.mjs +50 -19
- package/core/peer-store.mjs +1 -1
- package/core/topologist.mjs +1 -1
- package/core/unicast.mjs +1 -1
- package/libs/simplepeer-9.11.1.min.js +6 -0
- package/package.json +14 -18
- 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/resources/the-gossip-grail.html +639 -0
- package/services/clock.mjs +31 -5
- package/simulation/simul-utils.mjs +255 -0
- package/simulation/simulator.mjs +327 -0
- package/simulation/test-transports.mjs +215 -0
- package/simulation/tranports-sandbox.mjs +344 -0
- package/browser.mjs +0 -4
- package/core/parameters.mjs +0 -145
- package/index.mjs +0 -3
- package/libs/socket.io-4-8-1.min.js +0 -7
package/core/arbiter.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLOCK, NODE, GOSSIP, UNICAST, LOG_CSS } from './
|
|
1
|
+
import { CLOCK, NODE, GOSSIP, UNICAST, LOG_CSS } from './config.mjs';
|
|
2
2
|
|
|
3
3
|
// TRUST_BALANCE = seconds of ban if negative - never exceed MAX_TRUST if positive
|
|
4
4
|
// Growing each second by 1000ms until 0
|
package/core/config.mjs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const isNode = (typeof window === 'undefined');
|
|
2
|
+
if (!isNode && window.SimplePeer === undefined)
|
|
3
|
+
throw new Error('SimplePeer is not available in the browser, make sure to include it via a script tag or a bundler.');
|
|
4
|
+
import { Clock } from '../services/clock.mjs';
|
|
5
|
+
|
|
6
|
+
// HOLD: GLOBAL CONFIG FOR THE LIBRARY
|
|
7
|
+
// AVOID: CIRCULAR DEPENDENCIES AND TOO MANY FUNCTION/CONSTRUCTOR CONFIG
|
|
8
|
+
// SIMPLIFY: IMPORTS, SIMULATOR AND BROWSER SUPPORT
|
|
9
|
+
|
|
10
|
+
/** Synchronized clock that can be used outside the library */
|
|
11
|
+
export const CLOCK = Clock.instance;
|
|
12
|
+
|
|
13
|
+
export const SIMULATION = {
|
|
14
|
+
/** Specify setInterval() avoidance for faster simulation (true = avoid intervals) | Default: true */
|
|
15
|
+
AVOID_INTERVALS: false,
|
|
16
|
+
/** Use test transports (WebSocket server and SimplePeer replacement) | Default: false */
|
|
17
|
+
USE_TEST_TRANSPORTS: false,
|
|
18
|
+
/** Ice candidates delay simulation | Default: { min: 250, max: 3000 } */
|
|
19
|
+
ICE_DELAY: { min: 250, max: 3000 },
|
|
20
|
+
/** ICE offer failure rate simulation (0 to 1) | Default: .2 (20%) */
|
|
21
|
+
ICE_OFFER_FAILURE_RATE: .2,
|
|
22
|
+
/** ICE answer failure rate simulation (0 to 1) | Default: .15 (15%) */
|
|
23
|
+
ICE_ANSWER_FAILURE_RATE: .15,
|
|
24
|
+
// -------------------------------------------------|
|
|
25
|
+
/** Avoid creating follower nodes | Default: false */
|
|
26
|
+
AVOID_FOLLOWERS_NODES: false,
|
|
27
|
+
/** Auto start the simulation when creating the first node | Default: true */
|
|
28
|
+
AUTO_START: true, // auto start the simulation, false to wait the frontend | Default: true
|
|
29
|
+
/** Number of public nodes to create in the simulation
|
|
30
|
+
* - Default: 100
|
|
31
|
+
* - min: 1, medium: 3, strong: 20, hardcore: 100 */
|
|
32
|
+
PUBLIC_PEERS_COUNT: 100,
|
|
33
|
+
/** Number of standard nodes to create in the simulation
|
|
34
|
+
* - Default: 1860
|
|
35
|
+
* - stable: 12, medium: 250, strong: 2000, hardcore: 5000 */
|
|
36
|
+
PEERS_COUNT: 1860,
|
|
37
|
+
/** Number of bootstrap(public) nodes to provide as bootstrap to each peer on creation | Default: 10, null = all of them */
|
|
38
|
+
BOOTSTRAPS_PER_PEER: 10,
|
|
39
|
+
/** Delay between each peer.start() in milliseconds
|
|
40
|
+
* - Default: 60 (60sec to start 1000 peers)
|
|
41
|
+
* - 0 = faster for simulating big networks but > 0 = should be more realistic */
|
|
42
|
+
DELAY_BETWEEN_INIT: 10,
|
|
43
|
+
/** Random unicast(direct) messages to send per second | Default: 0, max: 1 (per peer) */
|
|
44
|
+
RANDOM_UNICAST_PER_SEC: 0,
|
|
45
|
+
/** Random gossip(to all) messages to send per second | Default: 0, max: 1 (per peer) */
|
|
46
|
+
RANDOM_GOSSIP_PER_SEC: 0,
|
|
47
|
+
/** Delay between each diffusion test in milliseconds | Default: 10_000 (10 seconds) */
|
|
48
|
+
DIFFUSION_TEST_DELAY: 10_000,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const NODE = {
|
|
52
|
+
/** 0: none, 1: errors, 2: +important info, 3: +debug, 4: +everything | Can be bypass by some constructors */
|
|
53
|
+
DEFAULT_VERBOSE: 1,
|
|
54
|
+
/** Timeout for upgrading a "connecting" peer to "connected" | Default: 15_000 (15 seconds) */
|
|
55
|
+
CONNECTION_UPGRADE_TIMEOUT: 15_000,
|
|
56
|
+
/** Flag to indicate if we are running in a browser environment | DON'T MODIFY THIS VALUE */
|
|
57
|
+
IS_BROWSER: isNode ? false : true,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const SERVICE = {
|
|
61
|
+
/** If the node is a public node (domain provided), it will start a WebSocket server on this port | Default: 8080 */
|
|
62
|
+
PORT: 8080,
|
|
63
|
+
/** The public node kicking basis delay | Default: 60_000 (1 minute) */
|
|
64
|
+
AUTO_KICK_DELAY: 60_000,
|
|
65
|
+
/** The public node kicking duration | Default: 30_000 (30 seconds) */
|
|
66
|
+
AUTO_KICK_DURATION: 30_000,
|
|
67
|
+
/** The public node will limit the maximum incoming connections to this value | Default: 20 */
|
|
68
|
+
MAX_WS_IN_CONNS: 20,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const IDENTITY = {
|
|
72
|
+
/** Difficulty level for anti-sybil measures, based on Argon2id Proof-of-Work
|
|
73
|
+
* - Follow a logarithmic scale (2^x)
|
|
74
|
+
* - Default: 0 (disabled)
|
|
75
|
+
* - RECOMMENDED: 7 (medium security, reasonable CPU usage)
|
|
76
|
+
* - HIGH SECURITY: 10 (high security, significant CPU usage)
|
|
77
|
+
* - Note: This setting is applied only if ARE_IDS_HEX = TRUE */
|
|
78
|
+
DIFFICULTY: 0,
|
|
79
|
+
/** Memory usage in KiB for Argon2
|
|
80
|
+
* - Follow a logarithmic scale (2^x)
|
|
81
|
+
* - Default: 2**16 = 65_536 (64 MiB)
|
|
82
|
+
* - RECOMMENDED: 2**17 = 131_072 (128 MiB)
|
|
83
|
+
* - HIGH SECURITY: 2**18 = 262_144 (256 MiB)
|
|
84
|
+
* - VERY HIGH SECURITY: 2**19 = 524_288 (512 MiB)
|
|
85
|
+
* - Note: This setting is applied only if ARE_IDS_HEX = TRUE */
|
|
86
|
+
ARGON2_MEM: 2**16,
|
|
87
|
+
/** Boolean to indicate if we use hex ids, Default: true = hex | false = strings as Bytes (can involve in serialization failures) */
|
|
88
|
+
ARE_IDS_HEX: true,
|
|
89
|
+
/** Identifier prefix for public nodes | Default: '0' */
|
|
90
|
+
PUBLIC_PREFIX: '0',
|
|
91
|
+
/** Identifier prefix for standard nodes | Default: '1' */
|
|
92
|
+
STANDARD_PREFIX: '1',
|
|
93
|
+
/** !!EVEN NUMBER ONLY!! length of peer id | Default: 16 */
|
|
94
|
+
ID_LENGTH: 16,
|
|
95
|
+
PUBKEY_LENGTH: 32,
|
|
96
|
+
PRIVATEKEY_LENGTH: 32,
|
|
97
|
+
SIGNATURE_LENGTH: 64,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const TRANSPORTS = {
|
|
101
|
+
/** Maximum SDP offers to create in advance to be ready for new connections | Default: 2 */
|
|
102
|
+
MAX_SDP_OFFERS: 2,
|
|
103
|
+
/** Time to wait for ICE gathering to complete | Default: 1_000 (1 second) */
|
|
104
|
+
ICE_COMPLETE_TIMEOUT: 1_000,
|
|
105
|
+
/** Time to wait for signal before destroying WTRC connection | Default: 8_000 (8 seconds) */
|
|
106
|
+
SIGNAL_CREATION_TIMEOUT: 8_000,
|
|
107
|
+
/** Time to consider an SDP offer as valid | Default: 40_000 (40 seconds) */
|
|
108
|
+
SDP_OFFER_EXPIRATION: 40_000,
|
|
109
|
+
|
|
110
|
+
WS_CLIENT: WebSocket,
|
|
111
|
+
WS_SERVER: isNode ? (await import('ws')).WebSocketServer : null,
|
|
112
|
+
PEER: isNode ? (await import('simple-peer')).default : SimplePeer
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const DISCOVERY = {
|
|
116
|
+
/** Delay between two peer declaring their connection to each other | Default: 10_000 (10 seconds) */
|
|
117
|
+
PEER_LINK_DELAY: 10_000,
|
|
118
|
+
/** Time to consider a peer connection as valid | Default: 60_000 (60 seconds) */
|
|
119
|
+
PEER_LINK_EXPIRATION: 60_000,
|
|
120
|
+
/** Delay between two discovery loops | Default: 2_500 (2.5 seconds) */
|
|
121
|
+
LOOP_DELAY: 2_500,
|
|
122
|
+
/** Target number of neighbors to maintain, higher values improve connectivity/resilience but increase resource usage
|
|
123
|
+
* - Default: 5
|
|
124
|
+
* - Light: 4, Medium: 5, Strong: 8, Hardcore: 12 */
|
|
125
|
+
TARGET_NEIGHBORS_COUNT: 5,
|
|
126
|
+
|
|
127
|
+
ON_CONNECT_DISPATCH: { // => on Node.#onConnect() // DEPRECATING
|
|
128
|
+
DELAY: 0, // delay before dispatching events | Default: 100 (.1 seconds)
|
|
129
|
+
BROADCAST_EVENT: false, // Boolean to indicate if we broadcast 'peer_connected'
|
|
130
|
+
OVER_NEIGHBORED: true, // Boolean to indicate if we broadcast 'over_neighbored' event when we are over neighbored | Default: true
|
|
131
|
+
SHARE_HISTORY: false, // Boolean to indicate if we broadcastToPeer some gossip history to the new peer | Default: true
|
|
132
|
+
},
|
|
133
|
+
ON_DISCONNECT_DISPATCH: { // => on Node.#onDisconnect() // DEPRECATING
|
|
134
|
+
DELAY: 0, // delay before dispatching the 'disconnected' event | Default: 500 (.5 seconds)
|
|
135
|
+
BROADCAST_EVENT: false, // Boolean to indicate if we broadcast 'peer_disconnected'
|
|
136
|
+
},
|
|
137
|
+
ON_UNICAST: { // => UnicastMessager.handleDirectMessage()
|
|
138
|
+
DIGEST_TRAVELED_ROUTE: true, // Boolean to indicate if we digest the traveled route for each unicast message | Default: true
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const UNICAST = { // MARKERS RANGE: 0-127
|
|
143
|
+
/** Maximum number of hops(relaying) for direct message | Default: 8
|
|
144
|
+
* - Default: 8, light: 6, super-light: 4, direct-only: 2 */
|
|
145
|
+
MAX_HOPS: 8,
|
|
146
|
+
/** Maximum number of nodes to consider during BFS
|
|
147
|
+
* - Default: 1728 (12³), light: 512 (8³), super-light: 144 (8²) */
|
|
148
|
+
MAX_NODES: 256,
|
|
149
|
+
/** Maximum number of routes to consider during BFS
|
|
150
|
+
* - Default: 5, light: 3, super-light: 1 */
|
|
151
|
+
MAX_ROUTES: 5,
|
|
152
|
+
/** First byte markers for unicast messages | RANGE: 0-127 */
|
|
153
|
+
MARKERS_BYTES: {
|
|
154
|
+
message: 0,
|
|
155
|
+
'0': 'message',
|
|
156
|
+
handshake: 1,
|
|
157
|
+
'1': 'handshake',
|
|
158
|
+
signal_answer: 2,
|
|
159
|
+
'2': 'signal_answer',
|
|
160
|
+
signal_offer: 3,
|
|
161
|
+
'3': 'signal_offer',
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const GOSSIP = { // MARKERS RANGE: 128-255
|
|
166
|
+
/** Time to consider a message as valid | Default: 10_000 (10 seconds) */
|
|
167
|
+
EXPIRATION: 10_000,
|
|
168
|
+
/** Time to keep messages in cache to avoid reprocessing | Default: 20_000 (20 seconds) */
|
|
169
|
+
CACHE_DURATION: 20_000,
|
|
170
|
+
/** Maximum number of hops for gossip messages | Default: 20
|
|
171
|
+
* - Here you can set different max hops for different message types */
|
|
172
|
+
HOPS: {
|
|
173
|
+
default: 20, // 16 should be the maximum
|
|
174
|
+
signal_offer: 6, // works with 3 ?
|
|
175
|
+
diffusion_test: 100, // must be high to reach all peers
|
|
176
|
+
over_neighbored: 6,
|
|
177
|
+
// peer_connected: 3,
|
|
178
|
+
// peer_disconnected: 3,
|
|
179
|
+
},
|
|
180
|
+
/** Ponderation to lower the transmission rate based on neighbors count
|
|
181
|
+
* - Lowering the transmission rate based on neighbors count, but involve a lower gossip diffusion
|
|
182
|
+
* - As well you can apply different ponderation factors for different message types */
|
|
183
|
+
TRANSMISSION_RATE: {
|
|
184
|
+
/** Minimum neighbors to apply ponderation, Default: 2
|
|
185
|
+
* - Decrease to apply ponderation sooner */
|
|
186
|
+
MIN_NEIGHBOURS_TO_APPLY_PONDERATION: 2,
|
|
187
|
+
/** Ponderation factor based on neighbors count, Default: 5
|
|
188
|
+
* - Decrease to lower transmission rate based on neighbors count */
|
|
189
|
+
NEIGHBOURS_PONDERATION: 5,
|
|
190
|
+
|
|
191
|
+
Default: 1, // 1 === 100%
|
|
192
|
+
signal_offer: .618, // .618 === 61.8%
|
|
193
|
+
// peer_connected: .5, // we can reduce this, but lowering the map quality
|
|
194
|
+
// peer_disconnected: .618
|
|
195
|
+
},
|
|
196
|
+
/** First byte markers for gossip messages | RANGE: 128-255 */
|
|
197
|
+
MARKERS_BYTES: {
|
|
198
|
+
gossip: 128,
|
|
199
|
+
'128': 'gossip',
|
|
200
|
+
signal_offer: 129,
|
|
201
|
+
'129': 'signal_offer',
|
|
202
|
+
peer_connected: 130,
|
|
203
|
+
'130': 'peer_connected',
|
|
204
|
+
peer_disconnected: 131,
|
|
205
|
+
'131': 'peer_disconnected',
|
|
206
|
+
diffusion_test: 132,
|
|
207
|
+
'132': 'diffusion_test',
|
|
208
|
+
over_neighbored: 133,
|
|
209
|
+
'133': 'over_neighbored',
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** CSS styles for console logging */
|
|
214
|
+
export const LOG_CSS = {
|
|
215
|
+
SIMULATOR: 'color: yellow; font-weight: bold;',
|
|
216
|
+
ARBITER: 'color: white;',
|
|
217
|
+
CRYPTO_CODEX: 'color: green;',
|
|
218
|
+
GOSSIP: 'color: fuchsia;',
|
|
219
|
+
UNICAST: 'color: cyan;',
|
|
220
|
+
PEER_STORE: 'color: orange;',
|
|
221
|
+
SERVICE: 'color: teal;',
|
|
222
|
+
PUNISHER: { BAN: 'color: red; font-weight: bold;', KICK: 'color: darkorange; font-weight: bold;' },
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default {
|
|
226
|
+
CLOCK, SIMULATION, NODE, TRANSPORTS, DISCOVERY, IDENTITY, UNICAST, GOSSIP, LOG_CSS
|
|
227
|
+
};
|
package/core/crypto-codex.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLOCK, SIMULATION, NODE, IDENTITY, GOSSIP, UNICAST, LOG_CSS } from './
|
|
1
|
+
import { CLOCK, SIMULATION, NODE, IDENTITY, GOSSIP, UNICAST, LOG_CSS } from './config.mjs';
|
|
2
2
|
import { GossipMessage } from './gossip.mjs';
|
|
3
3
|
import { DirectMessage, ReroutedDirectMessage } from './unicast.mjs';
|
|
4
4
|
import { Converter } from '../services/converter.mjs';
|
|
@@ -14,7 +14,8 @@ export class CryptoCodex {
|
|
|
14
14
|
/** @type {Uint8Array} */ privateKey;
|
|
15
15
|
|
|
16
16
|
/** @param {string} [nodeId] If provided: used to generate a fake keypair > disable crypto operations */
|
|
17
|
-
constructor(nodeId) {
|
|
17
|
+
constructor(nodeId, verbose = NODE.DEFAULT_VERBOSE) {
|
|
18
|
+
this.verbose = verbose;
|
|
18
19
|
if (!nodeId) return; // IF NOT PROVIDED: generate() should be called.
|
|
19
20
|
this.AVOID_CRYPTO = true;
|
|
20
21
|
this.id = nodeId.padEnd(IDENTITY.ID_LENGTH, ' ').slice(0, IDENTITY.ID_LENGTH);
|
|
@@ -23,6 +24,13 @@ export class CryptoCodex {
|
|
|
23
24
|
for (let i = 0; i < IDENTITY.ID_LENGTH; i++) this.publicKey[i] = idBytes[i];
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** @param {boolean} asPublicNode Default: false @param {Uint8Array} seed PrivateKey *-optional* */
|
|
28
|
+
static async createCryptoCodex(asPublicNode, seed) {
|
|
29
|
+
const cryptoCodex = new CryptoCodex(undefined, this.verbose);
|
|
30
|
+
await cryptoCodex.generate(asPublicNode, seed);
|
|
31
|
+
return cryptoCodex;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
// IDENTITY
|
|
27
35
|
/** @param {string} id Check the first character against the PUBLIC_PREFIX */
|
|
28
36
|
static isPublicNode(id) { return (IDENTITY.ARE_IDS_HEX ? Converter.hexToBits(id[0]) : id).startsWith(IDENTITY.PUBLIC_PREFIX); }
|
package/core/gossip.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { CLOCK, NODE, TRANSPORTS, LOG_CSS } from './
|
|
1
|
+
import { CLOCK, NODE, TRANSPORTS, LOG_CSS } from './config.mjs';
|
|
2
2
|
import { xxHash32 } from '../libs/xxhash32.mjs';
|
|
3
|
-
|
|
3
|
+
async function getWrtc() {
|
|
4
|
+
if (typeof globalThis.RTCPeerConnection !== 'undefined') return undefined;
|
|
5
|
+
return (await import('wrtc')).default;
|
|
6
|
+
}
|
|
7
|
+
const wrtc = await getWrtc();
|
|
4
8
|
|
|
5
9
|
/** - 'OfferObj' Definition
|
|
6
10
|
* @typedef {Object} OfferObj
|
|
@@ -84,7 +88,8 @@ export class OfferManager { // Manages the creation of SDP offers and handling o
|
|
|
84
88
|
this.offerInstanceByExpiration[expiration] = instance;
|
|
85
89
|
};
|
|
86
90
|
#createOffererInstance(expiration) {
|
|
87
|
-
const
|
|
91
|
+
const iceCompleteTimeout = TRANSPORTS.ICE_COMPLETE_TIMEOUT || 1_000;
|
|
92
|
+
const instance = new TRANSPORTS.PEER({ initiator: true, trickle: false, iceCompleteTimeout, wrtc, config: { iceServers: this.stunUrls } });
|
|
88
93
|
instance.on('error', error => this.#onError(error));
|
|
89
94
|
instance.on('signal', data => { // trickle: false => only one signal event with the full offer
|
|
90
95
|
const { candidate, type } = data; // with trickle, we need to adapt the approach.
|
|
@@ -158,7 +163,8 @@ export class OfferManager { // Manages the creation of SDP offers and handling o
|
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
// type === 'offer' => CREATE ANSWERER INSTANCE
|
|
161
|
-
const
|
|
166
|
+
const iceCompleteTimeout = TRANSPORTS.ICE_COMPLETE_TIMEOUT || 1_000;
|
|
167
|
+
const instance = new TRANSPORTS.PEER({ initiator: false, trickle: false, iceCompleteTimeout, wrtc, config: { iceServers: this.stunUrls } });
|
|
162
168
|
instance.on('error', (error) => this.#onError(error));
|
|
163
169
|
instance.on('signal', (data) => this.onSignalAnswer(remoteId, data, offerHash));
|
|
164
170
|
instance.on('connect', () => this.onConnect(remoteId, instance));
|
package/core/node-services.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { NODE,
|
|
1
|
+
import { SIMULATION, NODE, SERVICE, TRANSPORTS, DISCOVERY, LOG_CSS } from './config.mjs';
|
|
2
2
|
import { PeerConnection } from './peer-store.mjs';
|
|
3
3
|
import { Converter } from '../services/converter.mjs';
|
|
4
|
-
const dgram = !NODE.IS_BROWSER ? await import('dgram') : null;
|
|
4
|
+
const dgram = !NODE.IS_BROWSER ? await import('dgram') : null;
|
|
5
|
+
/*const dgram = !NODE.IS_BROWSER ?
|
|
6
|
+
await import('dgram').catch(() => null) :
|
|
7
|
+
null;*/
|
|
5
8
|
|
|
6
9
|
export class NodeServices {
|
|
7
10
|
id;
|
|
@@ -20,7 +23,7 @@ export class NodeServices {
|
|
|
20
23
|
this.cryptoCodex = cryptoCodex;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
start(domain = 'localhost', port =
|
|
26
|
+
start(domain = 'localhost', port = SERVICE.PORT) {
|
|
24
27
|
this.publicUrl = `ws://${domain}:${port}`;
|
|
25
28
|
this.#startWebSocketServer(domain, port);
|
|
26
29
|
if (!SIMULATION.USE_TEST_TRANSPORTS) this.#startSTUNServer(domain, port + 1);
|
|
@@ -30,22 +33,22 @@ export class NodeServices {
|
|
|
30
33
|
if (maxKick <= 0) return; // nothing to do
|
|
31
34
|
|
|
32
35
|
let kicked = 0;
|
|
33
|
-
const delay =
|
|
36
|
+
const delay = SERVICE.AUTO_KICK_DELAY;
|
|
34
37
|
for (const peerId in this.peerStore.connected) {
|
|
35
38
|
const conn = this.peerStore.connected[peerId];
|
|
36
39
|
const nonPublicNeighborsCount = this.peerStore.getUpdatedPeerConnectionsCount(peerId, false);
|
|
37
40
|
if (nonPublicNeighborsCount > DISCOVERY.TARGET_NEIGHBORS_COUNT) { // OVER CONNECTED
|
|
38
|
-
this.peerStore.kickPeer(peerId,
|
|
41
|
+
this.peerStore.kickPeer(peerId, SERVICE.AUTO_KICK_DURATION, 'freePublicNode');
|
|
39
42
|
if (this.peerStore.neighborsList.length <= DISCOVERY.TARGET_NEIGHBORS_COUNT) break;
|
|
40
43
|
else continue; // Don't count in maxKick
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
if (conn.getConnectionDuration() < (nonPublicNeighborsCount > 2 ? delay : delay * 2)) continue;
|
|
44
|
-
this.peerStore.kickPeer(peerId,
|
|
47
|
+
this.peerStore.kickPeer(peerId, SERVICE.AUTO_KICK_DURATION, 'freePublicNode');
|
|
45
48
|
if (++kicked >= maxKick) break;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
|
-
#startWebSocketServer(domain = 'localhost', port =
|
|
51
|
+
#startWebSocketServer(domain = 'localhost', port = SERVICE.PORT) {
|
|
49
52
|
this.wsServer = new TRANSPORTS.WS_SERVER({ port, host: domain });
|
|
50
53
|
this.wsServer.on('error', (error) => console.error(`WebSocket error on Node #${this.id}:`, error));
|
|
51
54
|
this.wsServer.on('connection', (ws) => {
|
|
@@ -70,10 +73,10 @@ export class NodeServices {
|
|
|
70
73
|
});
|
|
71
74
|
});
|
|
72
75
|
}
|
|
73
|
-
#startSTUNServer(host = 'localhost', port =
|
|
76
|
+
#startSTUNServer(host = 'localhost', port = SERVICE.PORT + 1) {
|
|
74
77
|
this.stunServer = dgram.createSocket('udp4');
|
|
75
78
|
this.stunServer.on('message', (msg, rinfo) => {
|
|
76
|
-
if (this.verbose >
|
|
79
|
+
if (this.verbose > 2) console.log(`%cSTUN message from ${rinfo.address}:${rinfo.port} - ${msg.toString('hex')}`, LOG_CSS.SERVICE);
|
|
77
80
|
if (!this.#isValidSTUNRequest(msg)) return;
|
|
78
81
|
this.stunServer.send(this.#buildSTUNResponse(msg, rinfo), rinfo.port, rinfo.address);
|
|
79
82
|
});
|
|
@@ -102,7 +105,7 @@ export class NodeServices {
|
|
|
102
105
|
response.writeUInt16BE(rinfo.port, 26); // Port
|
|
103
106
|
response.writeUInt32BE(Converter.ipToInt(rinfo.address), 28); // IP
|
|
104
107
|
|
|
105
|
-
if (this.verbose >
|
|
108
|
+
if (this.verbose > 2) console.log(`%cSTUN Response: client will discover IP ${rinfo.address}:${rinfo.port}`, 'color: green;');
|
|
106
109
|
return response;
|
|
107
110
|
}
|
|
108
111
|
/** @param {Array<{id: string, publicUrl: string}>} bootstraps */
|
package/core/node.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLOCK, SIMULATION, NODE, DISCOVERY } from './
|
|
1
|
+
import { CLOCK, SIMULATION, NODE, SERVICE, DISCOVERY } from './config.mjs';
|
|
2
2
|
import { Arbiter } from './arbiter.mjs';
|
|
3
3
|
import { OfferManager } from './ice-offer-manager.mjs';
|
|
4
4
|
import { PeerStore } from './peer-store.mjs';
|
|
@@ -8,7 +8,47 @@ import { Topologist } from './topologist.mjs';
|
|
|
8
8
|
import { CryptoCodex } from './crypto-codex.mjs';
|
|
9
9
|
import { NodeServices } from './node-services.mjs';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** Create and start a new PublicNode instance.
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {Array<{id: string, publicUrl: string}>} options.bootstraps List of bootstrap nodes used as P2P network entry
|
|
14
|
+
* @param {boolean} [options.autoStart] If true, the node will automatically start after creation (default: true)
|
|
15
|
+
* @param {CryptoCodex} [options.cryptoCodex] Identity of the node; if not provided, a new one will be generated
|
|
16
|
+
* @param {string} [options.domain] If provided, the node will operate as a public node and start necessary services (e.g., WebSocket server)
|
|
17
|
+
* @param {number} [options.port] If provided, the node will listen on this port (default: SERVICE.PORT)
|
|
18
|
+
* @param {number} [options.verbose] Verbosity level for logging (default: NODE.DEFAULT_VERBOSE) */
|
|
19
|
+
export async function createPublicNode(options) {
|
|
20
|
+
const verbose = options.verbose !== undefined ? options.verbose : NODE.DEFAULT_VERBOSE;
|
|
21
|
+
const domain = options.domain || undefined;
|
|
22
|
+
const codex = options.cryptoCodex || new CryptoCodex(undefined, verbose);
|
|
23
|
+
if (!codex.publicKey) await codex.generate(domain ? true : false);
|
|
24
|
+
|
|
25
|
+
const node = new Node(codex, options.bootstraps || [], verbose);
|
|
26
|
+
if (domain) {
|
|
27
|
+
node.services = new NodeServices(codex, node.peerStore, undefined, verbose);
|
|
28
|
+
node.services.start(domain, options.port || SERVICE.PORT);
|
|
29
|
+
node.topologist.services = node.services;
|
|
30
|
+
}
|
|
31
|
+
if (options.autoStart !== false) await node.start();
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Create and start a new Node instance.
|
|
36
|
+
* @param {Object} options
|
|
37
|
+
* @param {Array<{id: string, publicUrl: string}>} options.bootstraps List of bootstrap nodes used as P2P network entry
|
|
38
|
+
* @param {CryptoCodex} [options.cryptoCodex] Identity of the node; if not provided, a new one will be generated
|
|
39
|
+
* @param {boolean} [options.autoStart] If true, the node will automatically start after creation (default: true)
|
|
40
|
+
* @param {number} [options.verbose] Verbosity level for logging (default: NODE.DEFAULT_VERBOSE) */
|
|
41
|
+
export async function createNode(options = {}) {
|
|
42
|
+
const verbose = options.verbose !== undefined ? options.verbose : NODE.DEFAULT_VERBOSE;
|
|
43
|
+
const codex = options.cryptoCodex || new CryptoCodex(undefined, verbose);
|
|
44
|
+
if (!codex.publicKey) await codex.generate(false);
|
|
45
|
+
|
|
46
|
+
const node = new Node(codex, options.bootstraps || [], verbose);
|
|
47
|
+
if (options.autoStart !== false) await node.start();
|
|
48
|
+
return node;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class Node {
|
|
12
52
|
started = false;
|
|
13
53
|
id; cryptoCodex; verbose; arbiter;
|
|
14
54
|
/** class managing ICE offers */ offerManager;
|
|
@@ -23,15 +63,16 @@ export class NodeP2P {
|
|
|
23
63
|
* @param {Array<Record<string, string>>} bootstraps List of bootstrap nodes used as P2P network entry */
|
|
24
64
|
constructor(cryptoCodex, bootstraps = [], verbose = NODE.DEFAULT_VERBOSE) {
|
|
25
65
|
this.verbose = verbose;
|
|
66
|
+
if (this.topologist?.services) this.topologist.services.verbose = verbose;
|
|
26
67
|
this.cryptoCodex = cryptoCodex;
|
|
27
68
|
this.id = this.cryptoCodex.id;
|
|
28
|
-
const stunUrls = NodeServices.deriveSTUNServers(bootstraps);
|
|
69
|
+
const stunUrls = NodeServices.deriveSTUNServers(bootstraps || []);
|
|
29
70
|
this.offerManager = new OfferManager(this.id, stunUrls, verbose);
|
|
30
71
|
this.arbiter = new Arbiter(this.id, cryptoCodex, verbose);
|
|
31
72
|
this.peerStore = new PeerStore(this.id, this.cryptoCodex, this.offerManager, this.arbiter, verbose);
|
|
32
73
|
this.messager = new UnicastMessager(this.id, this.cryptoCodex, this.arbiter, this.peerStore, verbose);
|
|
33
74
|
this.gossip = new Gossip(this.id, this.cryptoCodex, this.arbiter, this.peerStore, verbose);
|
|
34
|
-
this.topologist = new Topologist(this.id, this.gossip, this.messager, this.peerStore, bootstraps);
|
|
75
|
+
this.topologist = new Topologist(this.id, this.gossip, this.messager, this.peerStore, bootstraps || []);
|
|
35
76
|
const { arbiter, peerStore, messager, gossip, topologist } = this;
|
|
36
77
|
|
|
37
78
|
// SETUP TRANSPORTS LISTENERS
|
|
@@ -47,7 +88,7 @@ export class NodeP2P {
|
|
|
47
88
|
// GOSSIP LISTENERS
|
|
48
89
|
gossip.on('signal_offer', (senderId, data, HOPS) => topologist.handleIncomingSignal(senderId, data, HOPS));
|
|
49
90
|
|
|
50
|
-
if (verbose > 2) console.log(`
|
|
91
|
+
if (verbose > 2) console.log(`Node initialized: ${this.id}`);
|
|
51
92
|
}
|
|
52
93
|
|
|
53
94
|
// PRIVATE METHODS
|
|
@@ -95,21 +136,11 @@ export class NodeP2P {
|
|
|
95
136
|
|
|
96
137
|
// PUBLIC API
|
|
97
138
|
get publicUrl() { return this.services?.publicUrl; }
|
|
139
|
+
get publicIdentity() { return { id: this.id, publicUrl: this.publicUrl }; }
|
|
98
140
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!codex.publicKey) await codex.generate(domain ? true : false);
|
|
103
|
-
|
|
104
|
-
const node = new NodeP2P(codex, bootstraps, verbose);
|
|
105
|
-
if (domain) {
|
|
106
|
-
node.services = new NodeServices(codex, node.peerStore, undefined, verbose);
|
|
107
|
-
node.services.start(domain, port);
|
|
108
|
-
node.topologist.services = node.services;
|
|
109
|
-
}
|
|
110
|
-
if (start) await node.start();
|
|
111
|
-
return node;
|
|
112
|
-
}
|
|
141
|
+
onMessageData(callback) { this.messager.on('message', callback); }
|
|
142
|
+
onGossipData(callback) { this.gossip.on('gossip', callback); }
|
|
143
|
+
|
|
113
144
|
async start() {
|
|
114
145
|
await CLOCK.sync(this.verbose);
|
|
115
146
|
this.started = true;
|
package/core/peer-store.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLOCK, SIMULATION, NODE, DISCOVERY, LOG_CSS } from './
|
|
1
|
+
import { CLOCK, SIMULATION, NODE, DISCOVERY, LOG_CSS } from './config.mjs';
|
|
2
2
|
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.ENABLED ? await import('../simulation/test-transports.mjs') : {};
|
|
3
3
|
|
|
4
4
|
export class KnownPeer { // known peer, not necessarily connected
|
package/core/topologist.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLOCK, SIMULATION, TRANSPORTS, NODE, DISCOVERY, GOSSIP } from './
|
|
1
|
+
import { CLOCK, SIMULATION, TRANSPORTS, NODE, DISCOVERY, GOSSIP } from './config.mjs';
|
|
2
2
|
import { PeerConnection } from './peer-store.mjs';
|
|
3
3
|
import { CryptoCodex } from './crypto-codex.mjs';
|
|
4
4
|
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.ENABLED ? await import('../simulation/test-transports.mjs') : {};
|
package/core/unicast.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SIMULATION, DISCOVERY, UNICAST } from "./
|
|
1
|
+
import { SIMULATION, DISCOVERY, UNICAST } from "./config.mjs";
|
|
2
2
|
import { TRUST_VALUES } from "./arbiter.mjs";
|
|
3
3
|
import { RouteBuilder_V2 } from "./route-builder.mjs";
|
|
4
4
|
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.ENABLED ? await import('../simulation/test-transports.mjs') : {};
|