hive-p2p 1.0.6 → 1.0.8
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/crypto-codex.mjs +9 -1
- package/core/ice-offer-manager.mjs +4 -2
- package/core/node-services.mjs +2 -2
- package/core/node.mjs +47 -16
- package/core/parameters.mjs +4 -3
- package/index.mjs +7 -3
- package/package.json +1 -1
package/core/crypto-codex.mjs
CHANGED
|
@@ -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); }
|
|
@@ -84,7 +84,8 @@ export class OfferManager { // Manages the creation of SDP offers and handling o
|
|
|
84
84
|
this.offerInstanceByExpiration[expiration] = instance;
|
|
85
85
|
};
|
|
86
86
|
#createOffererInstance(expiration) {
|
|
87
|
-
const
|
|
87
|
+
const iceCompleteTimeout = TRANSPORTS.ICE_COMPLETE_TIMEOUT || 1_000;
|
|
88
|
+
const instance = new TRANSPORTS.PEER({ initiator: true, trickle: false, iceCompleteTimeout, wrtc, config: { iceServers: this.stunUrls } });
|
|
88
89
|
instance.on('error', error => this.#onError(error));
|
|
89
90
|
instance.on('signal', data => { // trickle: false => only one signal event with the full offer
|
|
90
91
|
const { candidate, type } = data; // with trickle, we need to adapt the approach.
|
|
@@ -158,7 +159,8 @@ export class OfferManager { // Manages the creation of SDP offers and handling o
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
// type === 'offer' => CREATE ANSWERER INSTANCE
|
|
161
|
-
const
|
|
162
|
+
const iceCompleteTimeout = TRANSPORTS.ICE_COMPLETE_TIMEOUT || 1_000;
|
|
163
|
+
const instance = new TRANSPORTS.PEER({ initiator: false, trickle: false, iceCompleteTimeout, wrtc, config: { iceServers: this.stunUrls } });
|
|
162
164
|
instance.on('error', (error) => this.#onError(error));
|
|
163
165
|
instance.on('signal', (data) => this.onSignalAnswer(remoteId, data, offerHash));
|
|
164
166
|
instance.on('connect', () => this.onConnect(remoteId, instance));
|
package/core/node-services.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export class NodeServices {
|
|
|
73
73
|
#startSTUNServer(host = 'localhost', port = NODE.SERVICE.PORT + 1) {
|
|
74
74
|
this.stunServer = dgram.createSocket('udp4');
|
|
75
75
|
this.stunServer.on('message', (msg, rinfo) => {
|
|
76
|
-
if (this.verbose >
|
|
76
|
+
if (this.verbose > 2) console.log(`%cSTUN message from ${rinfo.address}:${rinfo.port} - ${msg.toString('hex')}`, 'color: blue;');
|
|
77
77
|
if (!this.#isValidSTUNRequest(msg)) return;
|
|
78
78
|
this.stunServer.send(this.#buildSTUNResponse(msg, rinfo), rinfo.port, rinfo.address);
|
|
79
79
|
});
|
|
@@ -102,7 +102,7 @@ export class NodeServices {
|
|
|
102
102
|
response.writeUInt16BE(rinfo.port, 26); // Port
|
|
103
103
|
response.writeUInt32BE(Converter.ipToInt(rinfo.address), 28); // IP
|
|
104
104
|
|
|
105
|
-
if (this.verbose >
|
|
105
|
+
if (this.verbose > 2) console.log(`%cSTUN Response: client will discover IP ${rinfo.address}:${rinfo.port}`, 'color: green;');
|
|
106
106
|
return response;
|
|
107
107
|
}
|
|
108
108
|
/** @param {Array<{id: string, publicUrl: string}>} bootstraps */
|
package/core/node.mjs
CHANGED
|
@@ -8,6 +8,46 @@ import { Topologist } from './topologist.mjs';
|
|
|
8
8
|
import { CryptoCodex } from './crypto-codex.mjs';
|
|
9
9
|
import { NodeServices } from './node-services.mjs';
|
|
10
10
|
|
|
11
|
+
/** Create and start a new PublicNodeP2P 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: NODE.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 NodeP2P(codex, options.bootstraps || [], verbose);
|
|
26
|
+
if (domain) {
|
|
27
|
+
node.services = new NodeServices(codex, node.peerStore, undefined, verbose);
|
|
28
|
+
node.services.start(domain, options.port || NODE.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 NodeP2P 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 NodeP2P(codex, options.bootstraps || [], verbose);
|
|
47
|
+
if (options.autoStart !== false) await node.start();
|
|
48
|
+
return node;
|
|
49
|
+
}
|
|
50
|
+
|
|
11
51
|
export class NodeP2P {
|
|
12
52
|
started = false;
|
|
13
53
|
id; cryptoCodex; verbose; arbiter;
|
|
@@ -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
|
|
@@ -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/parameters.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export const CLOCK = Clock.instance;
|
|
|
9
9
|
export const SIMULATION = {
|
|
10
10
|
// FACILITIES TO SIMULATE NETWORK CONDITIONS AND SCENARIOS
|
|
11
11
|
AVOID_INTERVALS: false, // avoid intervals for faster simulation | default: true
|
|
12
|
-
USE_TEST_TRANSPORTS:
|
|
12
|
+
USE_TEST_TRANSPORTS: false, // enable simulation features
|
|
13
13
|
ICE_DELAY: { min: 250, max: 3000 }, // ICE candidates in ms | default: { min: 250, max: 3000 }
|
|
14
14
|
ICE_OFFER_FAILURE_RATE: .2, // default: .2, 20% offer failure
|
|
15
15
|
ICE_ANSWER_FAILURE_RATE: .15, // default: .15, 15% answer failure
|
|
@@ -40,7 +40,7 @@ export const NODE = {
|
|
|
40
40
|
export const IDENTITY = {
|
|
41
41
|
DIFFICULTY: 0, // number of leading CATEGORY_PREFIX in bits for anti-sybil | default: 0(disabled) | RECOMMENDED: 7 | ON APPLY IF ARE_IDS_HEX = TRUE
|
|
42
42
|
ARGON2_MEM: 2**17, // Memory usage in KiB for Argon2 | default: 2**16 = 65_536 (64 MiB) | RECOMMENDED: 2**17 = 131_072 (128 MiB) | ON APPLY IF ARE_IDS_HEX = TRUE
|
|
43
|
-
ARE_IDS_HEX:
|
|
43
|
+
ARE_IDS_HEX: true, // Boolean to indicate if we use hex ids, default: true = hex | false = strings as Bytes (can involve in serialization failures)
|
|
44
44
|
PUBLIC_PREFIX: '0', // Identifier prefix for public nodes | default: '0'
|
|
45
45
|
STANDARD_PREFIX: '1', // Identifier prefix for standard nodes | default: '1'
|
|
46
46
|
ID_LENGTH: 16, // !!EVEN NUMBER ONLY!! length of peer id | default: 16
|
|
@@ -50,7 +50,8 @@ export const IDENTITY = {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export const TRANSPORTS = {
|
|
53
|
-
MAX_SDP_OFFERS:
|
|
53
|
+
MAX_SDP_OFFERS: 2, // max SDP offers to create in advance | default: 3
|
|
54
|
+
ICE_COMPLETE_TIMEOUT: 1_000, // time to wait for ICE gathering to complete | default: 1_000 (1 second)
|
|
54
55
|
SIGNAL_CREATION_TIMEOUT: 8_000, // time to wait for signal before destroying WTRC connection | default: 8_000 (8 seconds) | note: SimplePeer have a internal timeout of 5 secondes, we should be above that
|
|
55
56
|
SDP_OFFER_EXPIRATION: 40_000, // duration to consider an SDP offer as valid | default: 40_000 (40 seconds)
|
|
56
57
|
WS_CLIENT: WebSocket, // Simulation: patched with TestWsConnection (this one can be used as a server too)
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { NodeP2P, createNode, createPublicNode } from "./core/node.mjs";
|
|
2
|
+
import { CryptoCodex } from "./core/crypto-codex.mjs";
|
|
3
|
+
import PARAMETERS from "./core/parameters.mjs";
|
|
4
|
+
|
|
5
|
+
const HiveP2P = { NodeP2P, createNode, createPublicNode, CryptoCodex, PARAMETERS };
|
|
6
|
+
export { NodeP2P, createNode, createPublicNode, CryptoCodex, PARAMETERS };
|
|
7
|
+
export default HiveP2P;
|