@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.
- 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/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
package/core/arbiter.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { CLOCK, NODE, GOSSIP, UNICAST, LOG_CSS } from './config.mjs';
|
|
2
|
+
|
|
3
|
+
// TRUST_BALANCE = seconds of ban if negative - never exceed MAX_TRUST if positive
|
|
4
|
+
// Growing each second by 1000ms until 0
|
|
5
|
+
// Lowered each second by 100ms until 0 (avoid attacker growing balances on multiple disconnected peers)
|
|
6
|
+
|
|
7
|
+
const BYTES_COUNT_PERIOD = 10_000; // 10 seconds
|
|
8
|
+
const MAX_BYTES_PER_PERIOD = 1_000_000; // 1MB per period
|
|
9
|
+
|
|
10
|
+
const MAX_TRUST = 3_600_000; // +3600 seconds = 1 hour of good behavior
|
|
11
|
+
export const TRUST_VALUES = {
|
|
12
|
+
// POSITIVE IDENTITY
|
|
13
|
+
VALID_SIGNATURE: +10_000, // +10 seconds
|
|
14
|
+
VALID_POW: +300_000, // +5 minutes
|
|
15
|
+
// POSITIVE MESSAGES
|
|
16
|
+
UNICAST_RELAYED: +5_000, // +5 seconds
|
|
17
|
+
|
|
18
|
+
// NEGATIVE IDENTITY
|
|
19
|
+
//WRONG_ID_PREFIX: -300_000, // -5 minutes
|
|
20
|
+
WRONG_SIGNATURE: -600_000, // -10 minutes
|
|
21
|
+
WRONG_POW: -100_000_000, // -100_000 seconds = 27 hours - should never happen with valid nodes
|
|
22
|
+
|
|
23
|
+
// NEGATIVE MESSAGES
|
|
24
|
+
WRONG_SERIALIZATION: -60_000, // -1 minute
|
|
25
|
+
GOSSIP_FLOOD: -60_000, // -1 minute per message
|
|
26
|
+
UNICAST_FLOOD: -30_000, // -30 seconds per message
|
|
27
|
+
HOPS_EXCEEDED: -300_000, // -5 minutes
|
|
28
|
+
UNICAST_INVALID_ROUTE: -60_000, // -1 minute
|
|
29
|
+
FAILED_HANDSHAKE: -600_000, // -10 minutes ??? => TODO
|
|
30
|
+
WRONG_LENGTH: -600_000, // -10 minutes
|
|
31
|
+
};
|
|
32
|
+
export class Arbiter {
|
|
33
|
+
id; cryptoCodex; verbose;
|
|
34
|
+
|
|
35
|
+
/** - Key: peerId, Value: trustBalance
|
|
36
|
+
* - trustBalance = milliseconds of ban if negative
|
|
37
|
+
* @type {Record<string, number>} */
|
|
38
|
+
trustBalances = {};
|
|
39
|
+
bytesCounters = { gossip: 0, unicast: 0 };
|
|
40
|
+
bytesCounterResetIn = 0;
|
|
41
|
+
|
|
42
|
+
/** @param {string} selfId @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex @param {number} verbose */
|
|
43
|
+
constructor(selfId, cryptoCodex, verbose = 0) {
|
|
44
|
+
this.id = selfId; this.cryptoCodex = cryptoCodex; this.verbose = verbose;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
tick() {
|
|
48
|
+
for (const peerId in this.trustBalances) {
|
|
49
|
+
let balance = this.trustBalances[peerId];
|
|
50
|
+
if (balance === 0) continue; // increase to 0 or decrease slowly to 0
|
|
51
|
+
else balance = balance < 0 ? Math.min(0, balance + 1_000) : Math.max(0, balance - 100);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// RESET GOSSIP BYTES COUNTER
|
|
55
|
+
if (this.bytesCounterResetIn - 1_000 > 0) return;
|
|
56
|
+
this.bytesCounterResetIn = BYTES_COUNT_PERIOD;
|
|
57
|
+
this.bytesCounters = { gossip: 0, unicast: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Call from HiveP2P module only!
|
|
61
|
+
* @param {string} peerId
|
|
62
|
+
* @param {'WRONG_SERIALIZATION'} action */
|
|
63
|
+
countPeerAction(peerId, action) {
|
|
64
|
+
if (TRUST_VALUES[action]) return this.adjustTrust(peerId, TRUST_VALUES[action]);
|
|
65
|
+
}
|
|
66
|
+
/** @param {string} peerId @param {number} delta @param {string} [reason] */
|
|
67
|
+
adjustTrust(peerId, delta, reason = 'na') { // Internal and API use - return true if peer isn't banished
|
|
68
|
+
if (peerId === this.id) return; // self
|
|
69
|
+
if (delta) this.trustBalances[peerId] = Math.min(MAX_TRUST, (this.trustBalances[peerId] || 0) + delta);
|
|
70
|
+
if (delta && this.verbose > 2) console.log(`%c(Arbiter: ${this.id}) ${peerId} +${delta}ms (${reason}). Updated: ${this.trustBalances[peerId]}ms.`, LOG_CSS.ARBITER);
|
|
71
|
+
if (this.isBanished(peerId) && this.verbose > 1) console.log(`%c(Arbiter: ${this.id}) Peer ${peerId} is now banished.`, LOG_CSS.ARBITER);
|
|
72
|
+
}
|
|
73
|
+
isBanished(peerId = 'toto') { return (this.trustBalances[peerId] || 0) < 0; }
|
|
74
|
+
|
|
75
|
+
// MESSAGE VERIFICATION
|
|
76
|
+
/** @param {string} peerId @param {number} byteLength @param {'gossip' | 'unicast'} type */
|
|
77
|
+
countMessageBytes(peerId, byteLength, type) {
|
|
78
|
+
if (!this.bytesCounters[type]) this.bytesCounters[type] = 0;
|
|
79
|
+
this.bytesCounters[type] += byteLength;
|
|
80
|
+
if (this.bytesCounters[type] < MAX_BYTES_PER_PERIOD) return true;
|
|
81
|
+
return this.adjustTrust(peerId, type === 'gossip' ? TRUST_VALUES.GOSSIP_FLOOD : TRUST_VALUES.UNICAST_FLOOD, 'Message flood detected');
|
|
82
|
+
}
|
|
83
|
+
/** Call from HiveP2P module only! @param {string} from @param {any} message @param {Uint8Array} serialized @param {number} [powCheckFactor] default: 0.01 (1%) */
|
|
84
|
+
async digestMessage(from, message, serialized, powCheckFactor = .01) {
|
|
85
|
+
const { senderId, pubkey, topic, expectedEnd } = message; // avoid powControl() on banished peers
|
|
86
|
+
this.#signatureControl(from, message, serialized);
|
|
87
|
+
if (!this.#lengthControl(topic ? 'gossip' : 'unicast', serialized, expectedEnd)) return;
|
|
88
|
+
|
|
89
|
+
if (topic) this.#hopsControl(from, message);
|
|
90
|
+
else this.#routeLengthControl(from, message);
|
|
91
|
+
|
|
92
|
+
if (this.isBanished(from) || this.isBanished(senderId)) return;
|
|
93
|
+
if (this.trustBalances[senderId] > TRUST_VALUES.VALID_POW) return;
|
|
94
|
+
if (Math.random() < powCheckFactor) await this.#powControl(senderId, pubkey);
|
|
95
|
+
}
|
|
96
|
+
/** @param {'gossip' | 'unicast'} type */
|
|
97
|
+
#lengthControl(type, serialized, expectedEnd) {
|
|
98
|
+
if (!expectedEnd || serialized.length === expectedEnd) return true;
|
|
99
|
+
this.adjustTrust(from, TRUST_VALUES.WRONG_LENGTH, `${type} message length mismatch`);
|
|
100
|
+
}
|
|
101
|
+
/** @param {string} from @param {import('./gossip.mjs').GossipMessage} message @param {Uint8Array} serialized */
|
|
102
|
+
#signatureControl(from, message, serialized) {
|
|
103
|
+
const { pubkey, signature, signatureStart } = message;
|
|
104
|
+
const signedData = serialized.subarray(0, signatureStart);
|
|
105
|
+
const signatureValid = this.cryptoCodex.verifySignature(pubkey, signature, signedData);
|
|
106
|
+
if (signatureValid) this.adjustTrust(from, TRUST_VALUES.VALID_SIGNATURE, 'Gossip signature valid');
|
|
107
|
+
else this.adjustTrust(from, TRUST_VALUES.WRONG_SIGNATURE, 'Gossip signature invalid');
|
|
108
|
+
}
|
|
109
|
+
/** GOSSIP only @param {string} from @param {import('./gossip.mjs').GossipMessage} message */
|
|
110
|
+
#hopsControl(from, message) {
|
|
111
|
+
if (message.HOPS <= (GOSSIP.HOPS[message.topic] || GOSSIP.HOPS.default)) return;
|
|
112
|
+
this.adjustTrust(from, TRUST_VALUES.HOPS_EXCEEDED, 'Gossip HOPS exceeded');
|
|
113
|
+
}
|
|
114
|
+
/** UNICAST only @param {string} from @param {import('./unicast.mjs').DirectMessage} message */
|
|
115
|
+
#routeLengthControl(from, message) {
|
|
116
|
+
if (message.route.length <= UNICAST.MAX_HOPS) return;
|
|
117
|
+
this.adjustTrust(from, TRUST_VALUES.HOPS_EXCEEDED, 'Unicast HOPS exceeded');
|
|
118
|
+
}
|
|
119
|
+
/** ONLY APPLY AFTER #signatureControl() - @param {string} senderId @param {Uint8Array} pubkey */
|
|
120
|
+
async #powControl(senderId, pubkey) {
|
|
121
|
+
const isValid = await this.cryptoCodex.pubkeyDifficultyCheck(pubkey);
|
|
122
|
+
if (isValid) this.adjustTrust(senderId, TRUST_VALUES.VALID_POW, 'Gossip PoW valid');
|
|
123
|
+
else this.adjustTrust(senderId, TRUST_VALUES.WRONG_POW, 'Gossip PoW invalid');
|
|
124
|
+
}
|
|
125
|
+
}
|
package/core/config.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const isNode = (typeof window === 'undefined');
|
|
2
|
+
if (!isNode) (await import('../libs/simplepeer-9.11.1.min.js')).default;
|
|
3
|
+
import { Clock } from '../services/clock.mjs';
|
|
4
|
+
|
|
5
|
+
// HOLD: GLOBAL CONFIG FOR THE LIBRARY
|
|
6
|
+
// AVOID: CIRCULAR DEPENDENCIES AND TOO MANY FUNCTION/CONSTRUCTOR CONFIG
|
|
7
|
+
// SIMPLIFY: IMPORTS, SIMULATOR AND BROWSER SUPPORT
|
|
8
|
+
|
|
9
|
+
/** Synchronized clock that can be used outside the library */
|
|
10
|
+
export const CLOCK = Clock.instance;
|
|
11
|
+
|
|
12
|
+
export const SIMULATION = {
|
|
13
|
+
/** Specify setInterval() avoidance for faster simulation (true = avoid intervals) | Default: true */
|
|
14
|
+
AVOID_INTERVALS: false,
|
|
15
|
+
/** Use test transports (WebSocket server and SimplePeer replacement) | Default: false */
|
|
16
|
+
USE_TEST_TRANSPORTS: false,
|
|
17
|
+
/** Ice candidates delay simulation | Default: { min: 250, max: 3000 } */
|
|
18
|
+
ICE_DELAY: { min: 250, max: 3000 },
|
|
19
|
+
/** ICE offer failure rate simulation (0 to 1) | Default: .2 (20%) */
|
|
20
|
+
ICE_OFFER_FAILURE_RATE: .2,
|
|
21
|
+
/** ICE answer failure rate simulation (0 to 1) | Default: .15 (15%) */
|
|
22
|
+
ICE_ANSWER_FAILURE_RATE: .15,
|
|
23
|
+
// -------------------------------------------------|
|
|
24
|
+
/** Avoid creating follower nodes | Default: false */
|
|
25
|
+
AVOID_FOLLOWERS_NODES: false,
|
|
26
|
+
/** Auto start the simulation when creating the first node | Default: true */
|
|
27
|
+
AUTO_START: true, // auto start the simulation, false to wait the frontend | Default: true
|
|
28
|
+
/** Number of public nodes to create in the simulation
|
|
29
|
+
* - Default: 100
|
|
30
|
+
* - min: 1, medium: 3, strong: 20, hardcore: 100 */
|
|
31
|
+
PUBLIC_PEERS_COUNT: 100,
|
|
32
|
+
/** Number of standard nodes to create in the simulation
|
|
33
|
+
* - Default: 1860
|
|
34
|
+
* - stable: 12, medium: 250, strong: 2000, hardcore: 5000 */
|
|
35
|
+
PEERS_COUNT: 1860,
|
|
36
|
+
/** Number of bootstrap(public) nodes to provide as bootstrap to each peer on creation | Default: 10, null = all of them */
|
|
37
|
+
BOOTSTRAPS_PER_PEER: 10,
|
|
38
|
+
/** Delay between each peer.start() in milliseconds
|
|
39
|
+
* - Default: 60 (60sec to start 1000 peers)
|
|
40
|
+
* - 0 = faster for simulating big networks but > 0 = should be more realistic */
|
|
41
|
+
DELAY_BETWEEN_INIT: 10,
|
|
42
|
+
/** Random unicast(direct) messages to send per second | Default: 0, max: 1 (per peer) */
|
|
43
|
+
RANDOM_UNICAST_PER_SEC: 0,
|
|
44
|
+
/** Random gossip(to all) messages to send per second | Default: 0, max: 1 (per peer) */
|
|
45
|
+
RANDOM_GOSSIP_PER_SEC: 0,
|
|
46
|
+
/** Delay between each diffusion test in milliseconds | Default: 10_000 (10 seconds) */
|
|
47
|
+
DIFFUSION_TEST_DELAY: 10_000,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const NODE = {
|
|
51
|
+
/** 0: none, 1: errors, 2: +important info, 3: +debug, 4: +everything | Can be bypass by some constructors */
|
|
52
|
+
DEFAULT_VERBOSE: 1,
|
|
53
|
+
/** Timeout for upgrading a "connecting" peer to "connected" | Default: 15_000 (15 seconds) */
|
|
54
|
+
CONNECTION_UPGRADE_TIMEOUT: 15_000,
|
|
55
|
+
/** Flag to indicate if we are running in a browser environment | DON'T MODIFY THIS VALUE */
|
|
56
|
+
IS_BROWSER: isNode ? false : true,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const SERVICE = {
|
|
60
|
+
/** If the node is a public node (domain provided), it will start a WebSocket server on this port | Default: 8080 */
|
|
61
|
+
PORT: 8080,
|
|
62
|
+
/** The public node kicking basis delay | Default: 60_000 (1 minute) */
|
|
63
|
+
AUTO_KICK_DELAY: 60_000,
|
|
64
|
+
/** The public node kicking duration | Default: 30_000 (30 seconds) */
|
|
65
|
+
AUTO_KICK_DURATION: 30_000,
|
|
66
|
+
/** The public node will limit the maximum incoming connections to this value | Default: 20 */
|
|
67
|
+
MAX_WS_IN_CONNS: 20,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const IDENTITY = {
|
|
71
|
+
/** Difficulty level for anti-sybil measures, based on Argon2id Proof-of-Work
|
|
72
|
+
* - Follow a logarithmic scale (2^x)
|
|
73
|
+
* - Default: 0 (disabled)
|
|
74
|
+
* - RECOMMENDED: 7 (medium security, reasonable CPU usage)
|
|
75
|
+
* - HIGH SECURITY: 10 (high security, significant CPU usage)
|
|
76
|
+
* - Note: This setting is applied only if ARE_IDS_HEX = TRUE */
|
|
77
|
+
DIFFICULTY: 0,
|
|
78
|
+
/** Memory usage in KiB for Argon2
|
|
79
|
+
* - Follow a logarithmic scale (2^x)
|
|
80
|
+
* - Default: 2**16 = 65_536 (64 MiB)
|
|
81
|
+
* - RECOMMENDED: 2**17 = 131_072 (128 MiB)
|
|
82
|
+
* - HIGH SECURITY: 2**18 = 262_144 (256 MiB)
|
|
83
|
+
* - VERY HIGH SECURITY: 2**19 = 524_288 (512 MiB)
|
|
84
|
+
* - Note: This setting is applied only if ARE_IDS_HEX = TRUE */
|
|
85
|
+
ARGON2_MEM: 2**16,
|
|
86
|
+
/** Boolean to indicate if we use hex ids, Default: true = hex | false = strings as Bytes (can involve in serialization failures) */
|
|
87
|
+
ARE_IDS_HEX: true,
|
|
88
|
+
/** Identifier prefix for public nodes | Default: '0' */
|
|
89
|
+
PUBLIC_PREFIX: '0',
|
|
90
|
+
/** Identifier prefix for standard nodes | Default: '1' */
|
|
91
|
+
STANDARD_PREFIX: '1',
|
|
92
|
+
/** !!EVEN NUMBER ONLY!! length of peer id | Default: 16 */
|
|
93
|
+
ID_LENGTH: 16,
|
|
94
|
+
PUBKEY_LENGTH: 32,
|
|
95
|
+
PRIVATEKEY_LENGTH: 32,
|
|
96
|
+
SIGNATURE_LENGTH: 64,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const TRANSPORTS = {
|
|
100
|
+
/** Maximum SDP offers to create in advance to be ready for new connections | Default: 2 */
|
|
101
|
+
MAX_SDP_OFFERS: 2,
|
|
102
|
+
/** Time to wait for ICE gathering to complete | Default: 1_000 (1 second) */
|
|
103
|
+
ICE_COMPLETE_TIMEOUT: 1_000,
|
|
104
|
+
/** Time to wait for signal before destroying WTRC connection | Default: 8_000 (8 seconds) */
|
|
105
|
+
SIGNAL_CREATION_TIMEOUT: 8_000,
|
|
106
|
+
/** Time to consider an SDP offer as valid | Default: 40_000 (40 seconds) */
|
|
107
|
+
SDP_OFFER_EXPIRATION: 40_000,
|
|
108
|
+
|
|
109
|
+
WS_CLIENT: WebSocket,
|
|
110
|
+
WS_SERVER: isNode ? (await import('ws')).WebSocketServer : null,
|
|
111
|
+
PEER: isNode ? (await import('simple-peer')).default : window.SimplePeer
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const DISCOVERY = {
|
|
115
|
+
/** Delay between two peer declaring their connection to each other | Default: 10_000 (10 seconds) */
|
|
116
|
+
PEER_LINK_DELAY: 10_000,
|
|
117
|
+
/** Time to consider a peer connection as valid | Default: 60_000 (60 seconds) */
|
|
118
|
+
PEER_LINK_EXPIRATION: 60_000,
|
|
119
|
+
/** Delay between two discovery loops | Default: 2_500 (2.5 seconds) */
|
|
120
|
+
LOOP_DELAY: 2_500,
|
|
121
|
+
/** Target number of neighbors to maintain, higher values improve connectivity/resilience but increase resource usage
|
|
122
|
+
* - Default: 5
|
|
123
|
+
* - Light: 4, Medium: 5, Strong: 8, Hardcore: 12 */
|
|
124
|
+
TARGET_NEIGHBORS_COUNT: 5,
|
|
125
|
+
|
|
126
|
+
ON_CONNECT_DISPATCH: { // => on Node.#onConnect() // DEPRECATING
|
|
127
|
+
DELAY: 0, // delay before dispatching events | Default: 100 (.1 seconds)
|
|
128
|
+
BROADCAST_EVENT: false, // Boolean to indicate if we broadcast 'peer_connected'
|
|
129
|
+
OVER_NEIGHBORED: true, // Boolean to indicate if we broadcast 'over_neighbored' event when we are over neighbored | Default: true
|
|
130
|
+
SHARE_HISTORY: false, // Boolean to indicate if we broadcastToPeer some gossip history to the new peer | Default: true
|
|
131
|
+
},
|
|
132
|
+
ON_DISCONNECT_DISPATCH: { // => on Node.#onDisconnect() // DEPRECATING
|
|
133
|
+
DELAY: 0, // delay before dispatching the 'disconnected' event | Default: 500 (.5 seconds)
|
|
134
|
+
BROADCAST_EVENT: false, // Boolean to indicate if we broadcast 'peer_disconnected'
|
|
135
|
+
},
|
|
136
|
+
ON_UNICAST: { // => UnicastMessager.handleDirectMessage()
|
|
137
|
+
DIGEST_TRAVELED_ROUTE: true, // Boolean to indicate if we digest the traveled route for each unicast message | Default: true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const UNICAST = { // MARKERS RANGE: 0-127
|
|
142
|
+
/** Maximum number of hops(relaying) for direct message | Default: 8
|
|
143
|
+
* - Default: 8, light: 6, super-light: 4, direct-only: 2 */
|
|
144
|
+
MAX_HOPS: 8,
|
|
145
|
+
/** Maximum number of nodes to consider during BFS
|
|
146
|
+
* - Default: 1728 (12³), light: 512 (8³), super-light: 144 (8²) */
|
|
147
|
+
MAX_NODES: 256,
|
|
148
|
+
/** Maximum number of routes to consider during BFS
|
|
149
|
+
* - Default: 5, light: 3, super-light: 1 */
|
|
150
|
+
MAX_ROUTES: 5,
|
|
151
|
+
/** First byte markers for unicast messages | RANGE: 0-127 */
|
|
152
|
+
MARKERS_BYTES: {
|
|
153
|
+
message: 0,
|
|
154
|
+
'0': 'message',
|
|
155
|
+
handshake: 1,
|
|
156
|
+
'1': 'handshake',
|
|
157
|
+
signal_answer: 2,
|
|
158
|
+
'2': 'signal_answer',
|
|
159
|
+
signal_offer: 3,
|
|
160
|
+
'3': 'signal_offer',
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const GOSSIP = { // MARKERS RANGE: 128-255
|
|
165
|
+
/** Time to consider a message as valid | Default: 10_000 (10 seconds) */
|
|
166
|
+
EXPIRATION: 10_000,
|
|
167
|
+
/** Time to keep messages in cache to avoid reprocessing | Default: 20_000 (20 seconds) */
|
|
168
|
+
CACHE_DURATION: 20_000,
|
|
169
|
+
/** Maximum number of hops for gossip messages | Default: 20
|
|
170
|
+
* - Here you can set different max hops for different message types */
|
|
171
|
+
HOPS: {
|
|
172
|
+
default: 20, // 16 should be the maximum
|
|
173
|
+
signal_offer: 6, // works with 3 ?
|
|
174
|
+
diffusion_test: 100, // must be high to reach all peers
|
|
175
|
+
over_neighbored: 6,
|
|
176
|
+
// peer_connected: 3,
|
|
177
|
+
// peer_disconnected: 3,
|
|
178
|
+
},
|
|
179
|
+
/** Ponderation to lower the transmission rate based on neighbors count
|
|
180
|
+
* - Lowering the transmission rate based on neighbors count, but involve a lower gossip diffusion
|
|
181
|
+
* - As well you can apply different ponderation factors for different message types */
|
|
182
|
+
TRANSMISSION_RATE: {
|
|
183
|
+
/** Minimum neighbors to apply ponderation, Default: 2
|
|
184
|
+
* - Decrease to apply ponderation sooner */
|
|
185
|
+
MIN_NEIGHBOURS_TO_APPLY_PONDERATION: 2,
|
|
186
|
+
/** Ponderation factor based on neighbors count, Default: 5
|
|
187
|
+
* - Decrease to lower transmission rate based on neighbors count */
|
|
188
|
+
NEIGHBOURS_PONDERATION: 5,
|
|
189
|
+
|
|
190
|
+
Default: 1, // 1 === 100%
|
|
191
|
+
signal_offer: .618, // .618 === 61.8%
|
|
192
|
+
// peer_connected: .5, // we can reduce this, but lowering the map quality
|
|
193
|
+
// peer_disconnected: .618
|
|
194
|
+
},
|
|
195
|
+
/** First byte markers for gossip messages | RANGE: 128-255 */
|
|
196
|
+
MARKERS_BYTES: {
|
|
197
|
+
gossip: 128,
|
|
198
|
+
'128': 'gossip',
|
|
199
|
+
signal_offer: 129,
|
|
200
|
+
'129': 'signal_offer',
|
|
201
|
+
peer_connected: 130,
|
|
202
|
+
'130': 'peer_connected',
|
|
203
|
+
peer_disconnected: 131,
|
|
204
|
+
'131': 'peer_disconnected',
|
|
205
|
+
diffusion_test: 132,
|
|
206
|
+
'132': 'diffusion_test',
|
|
207
|
+
over_neighbored: 133,
|
|
208
|
+
'133': 'over_neighbored',
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** CSS styles for console logging */
|
|
213
|
+
export const LOG_CSS = {
|
|
214
|
+
SIMULATOR: 'color: yellow; font-weight: bold;',
|
|
215
|
+
ARBITER: 'color: white;',
|
|
216
|
+
CRYPTO_CODEX: 'color: green;',
|
|
217
|
+
GOSSIP: 'color: fuchsia;',
|
|
218
|
+
UNICAST: 'color: cyan;',
|
|
219
|
+
PEER_STORE: 'color: orange;',
|
|
220
|
+
SERVICE: 'color: teal;',
|
|
221
|
+
PUNISHER: { BAN: 'color: red; font-weight: bold;', KICK: 'color: darkorange; font-weight: bold;' },
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default {
|
|
225
|
+
CLOCK, SIMULATION, NODE, TRANSPORTS, DISCOVERY, IDENTITY, UNICAST, GOSSIP, LOG_CSS
|
|
226
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { CLOCK, SIMULATION, NODE, IDENTITY, GOSSIP, UNICAST, LOG_CSS } from './config.mjs';
|
|
2
|
+
import { GossipMessage } from './gossip.mjs';
|
|
3
|
+
import { DirectMessage, ReroutedDirectMessage } from './unicast.mjs';
|
|
4
|
+
import { Converter } from '../services/converter.mjs';
|
|
5
|
+
import { ed25519, Argon2Unified } from '../services/cryptos.mjs';
|
|
6
|
+
|
|
7
|
+
export class CryptoCodex {
|
|
8
|
+
argon2 = new Argon2Unified();
|
|
9
|
+
converter = new Converter();
|
|
10
|
+
AVOID_CRYPTO = false;
|
|
11
|
+
verbose = NODE.DEFAULT_VERBOSE;
|
|
12
|
+
/** @type {string} */ id;
|
|
13
|
+
/** @type {Uint8Array} */ publicKey;
|
|
14
|
+
/** @type {Uint8Array} */ privateKey;
|
|
15
|
+
|
|
16
|
+
/** @param {string} [nodeId] If provided: used to generate a fake keypair > disable crypto operations */
|
|
17
|
+
constructor(nodeId, verbose = NODE.DEFAULT_VERBOSE) {
|
|
18
|
+
this.verbose = verbose;
|
|
19
|
+
if (!nodeId) return; // IF NOT PROVIDED: generate() should be called.
|
|
20
|
+
this.AVOID_CRYPTO = true;
|
|
21
|
+
this.id = nodeId.padEnd(IDENTITY.ID_LENGTH, ' ').slice(0, IDENTITY.ID_LENGTH);
|
|
22
|
+
this.privateKey = new Uint8Array(32).fill(0); this.publicKey = new Uint8Array(32).fill(0);
|
|
23
|
+
const idBytes = new TextEncoder().encode(this.id); // use nodeId to create a fake public key
|
|
24
|
+
for (let i = 0; i < IDENTITY.ID_LENGTH; i++) this.publicKey[i] = idBytes[i];
|
|
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
|
+
|
|
34
|
+
// IDENTITY
|
|
35
|
+
/** @param {string} id Check the first character against the PUBLIC_PREFIX */
|
|
36
|
+
static isPublicNode(id) { return (IDENTITY.ARE_IDS_HEX ? Converter.hexToBits(id[0]) : id).startsWith(IDENTITY.PUBLIC_PREFIX); }
|
|
37
|
+
/** @param {string} id */
|
|
38
|
+
get idLength() { return IDENTITY.ARE_IDS_HEX ? IDENTITY.ID_LENGTH / 2 : IDENTITY.ID_LENGTH; }
|
|
39
|
+
isPublicNode(id) { return CryptoCodex.isPublicNode(id); }
|
|
40
|
+
/** @param {boolean} asPublicNode @param {Uint8Array} [seed] The privateKey. DON'T USE IN SIMULATION */
|
|
41
|
+
async generate(asPublicNode, seed) { // Generate Ed25519 keypair cross-platform | set id only for simulator
|
|
42
|
+
if (this.nodeId) return;
|
|
43
|
+
await this.#generateAntiSybilIdentity(seed, asPublicNode);
|
|
44
|
+
if (!this.id) throw new Error('Failed to generate identity');
|
|
45
|
+
}
|
|
46
|
+
/** Check if the pubKey meets the difficulty using Argon2 derivation @param {Uint8Array} publicKey */
|
|
47
|
+
async pubkeyDifficultyCheck(publicKey) {
|
|
48
|
+
if (this.AVOID_CRYPTO || !IDENTITY.DIFFICULTY) return true;
|
|
49
|
+
const { bitsString } = await this.argon2.hash(publicKey, 'HiveP2P', IDENTITY.ARGON2_MEM) || {};
|
|
50
|
+
if (bitsString && bitsString.startsWith('0'.repeat(IDENTITY.DIFFICULTY))) return true;
|
|
51
|
+
}
|
|
52
|
+
#idFromPublicKey(publicKey) {
|
|
53
|
+
if (IDENTITY.ARE_IDS_HEX) return this.converter.bytesToHex(publicKey.slice(0, this.idLength), IDENTITY.ID_LENGTH);
|
|
54
|
+
return this.converter.bytesToString(publicKey.slice(0, IDENTITY.ID_LENGTH));
|
|
55
|
+
}
|
|
56
|
+
/** @param {Uint8Array} seed The privateKey. @param {boolean} asPublicNode */
|
|
57
|
+
async #generateAntiSybilIdentity(seed, asPublicNode) {
|
|
58
|
+
const maxIterations = (2 ** IDENTITY.DIFFICULTY) * 100; // avoid infinite loop
|
|
59
|
+
for (let i = 0; i < maxIterations; i++) { // avoid infinite loop
|
|
60
|
+
const { secretKey, publicKey } = ed25519.keygen(seed);
|
|
61
|
+
const id = this.#idFromPublicKey(publicKey);
|
|
62
|
+
if (asPublicNode && !this.isPublicNode(id)) continue; // Check prefix
|
|
63
|
+
if (!asPublicNode && this.isPublicNode(id)) continue; // Check prefix
|
|
64
|
+
if (!await this.pubkeyDifficultyCheck(publicKey)) continue; // Check difficulty
|
|
65
|
+
|
|
66
|
+
this.id = id;
|
|
67
|
+
this.privateKey = secretKey; this.publicKey = publicKey;
|
|
68
|
+
if (this.verbose > 2) console.log(`%cNode generated id: ${this.id} (isPublic: ${asPublicNode}, difficulty: ${IDENTITY.DIFFICULTY}) after ${((i + 1) / 2).toFixed(1)} iterations`, LOG_CSS.CRYPTO_CODEX);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (this.verbose > 0) console.log(`%cFAILED to generate id after ${maxIterations} iterations. Try lowering the difficulty.`, LOG_CSS.CRYPTO_CODEX);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MESSSAGE CREATION (SERIALIZATION AND SIGNATURE INCLUDED)
|
|
75
|
+
signBufferViewAndAppendSignature(bufferView, privateKey, signaturePosition = bufferView.length - IDENTITY.SIGNATURE_LENGTH) {
|
|
76
|
+
if (this.AVOID_CRYPTO) return;
|
|
77
|
+
const dataToSign = bufferView.subarray(0, signaturePosition);
|
|
78
|
+
bufferView.set(ed25519.sign(dataToSign, privateKey), signaturePosition);
|
|
79
|
+
}
|
|
80
|
+
/** @param {string} topic @param {string | Uint8Array | Object} data @param {number} [HOPS] @param {string[]} route @param {string[]} [neighbors] */
|
|
81
|
+
createGossipMessage(topic, data, HOPS = 3, neighbors = [], timestamp = CLOCK.time) {
|
|
82
|
+
const MARKER = GOSSIP.MARKERS_BYTES[topic];
|
|
83
|
+
if (MARKER === undefined) throw new Error(`Failed to create gossip message: unknown topic '${topic}'.`);
|
|
84
|
+
|
|
85
|
+
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
86
|
+
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
87
|
+
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + dataBytes.length + IDENTITY.SIGNATURE_LENGTH + 1;
|
|
88
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
89
|
+
const bufferView = new Uint8Array(buffer);
|
|
90
|
+
this.#setBufferHeader(bufferView, MARKER, dataCode, neighbors.length, timestamp, dataBytes, this.publicKey);
|
|
91
|
+
|
|
92
|
+
bufferView.set(neighborsBytes, 47); // X bytes for neighbors
|
|
93
|
+
bufferView.set(dataBytes, 47 + neighborsBytes.length); // X bytes for data
|
|
94
|
+
bufferView.set([Math.min(255, HOPS)], totalBytes - 1); // 1 byte for HOPS (Unsigned)
|
|
95
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH - 1);
|
|
96
|
+
return bufferView;
|
|
97
|
+
}
|
|
98
|
+
/** @param {Uint8Array} serializedMessage */
|
|
99
|
+
decrementGossipHops(serializedMessage) { // Here we just need to decrement the HOPS value => last byte of the message
|
|
100
|
+
const clone = new Uint8Array(serializedMessage); // avoid modifying the original message
|
|
101
|
+
const hops = serializedMessage[serializedMessage.length - 1];
|
|
102
|
+
clone[serializedMessage.length - 1] = Math.max(0, hops - 1);
|
|
103
|
+
return clone;
|
|
104
|
+
}
|
|
105
|
+
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] */
|
|
106
|
+
createUnicastMessage(type, data, route, neighbors = [], timestamp = CLOCK.time) {
|
|
107
|
+
const MARKER = UNICAST.MARKERS_BYTES[type];
|
|
108
|
+
if (MARKER === undefined) throw new Error(`Failed to create unicast message: unknown type '${type}'.`);
|
|
109
|
+
if (route.length < 2) throw new Error('Failed to create unicast message: route must have at least 2 nodes (next hop and target).');
|
|
110
|
+
if (route.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
111
|
+
|
|
112
|
+
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
113
|
+
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
114
|
+
const routeBytes = this.#idsToBytes(route);
|
|
115
|
+
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + 1 + routeBytes.length + dataBytes.length + IDENTITY.SIGNATURE_LENGTH;
|
|
116
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
117
|
+
const bufferView = new Uint8Array(buffer);
|
|
118
|
+
this.#setBufferHeader(bufferView, MARKER, dataCode, neighbors.length, timestamp, dataBytes, this.publicKey);
|
|
119
|
+
|
|
120
|
+
const NDBL = neighborsBytes.length + dataBytes.length;
|
|
121
|
+
bufferView.set(neighborsBytes, 47); // X bytes for neighbors
|
|
122
|
+
bufferView.set(dataBytes, 47 + neighborsBytes.length); // X bytes for data
|
|
123
|
+
bufferView.set([route.length], 47 + NDBL); // 1 byte for route length
|
|
124
|
+
bufferView.set(routeBytes, 47 + 1 + NDBL); // X bytes for route
|
|
125
|
+
|
|
126
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
|
|
127
|
+
return bufferView;
|
|
128
|
+
}
|
|
129
|
+
/** @param {Uint8Array} serialized @param {string[]} newRoute */
|
|
130
|
+
createReroutedUnicastMessage(serialized, newRoute) {
|
|
131
|
+
if (newRoute.length < 2) throw new Error('Failed to create rerouted unicast message: route must have at least 2 nodes (next hop and target).');
|
|
132
|
+
if (newRoute.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create rerouted unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
133
|
+
|
|
134
|
+
const routeBytesArray = newRoute.map(id => this.converter.stringToBytes(id));
|
|
135
|
+
const totalBytes = serialized.length + 32 + (IDENTITY.ID_LENGTH * routeBytesArray.length) + IDENTITY.SIGNATURE_LENGTH;
|
|
136
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
137
|
+
const bufferView = new Uint8Array(buffer);
|
|
138
|
+
bufferView.set(serialized, 0); // original serialized message
|
|
139
|
+
bufferView.set(this.publicKey, serialized.length); // 32 bytes for new public key
|
|
140
|
+
for (let i = 0; i < routeBytesArray.length; i++) bufferView.set(routeBytesArray[i], serialized.length + 32 + (i * IDENTITY.ID_LENGTH)); // new route
|
|
141
|
+
|
|
142
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
|
|
143
|
+
return bufferView;
|
|
144
|
+
}
|
|
145
|
+
/** @param {string[]} ids */
|
|
146
|
+
#idsToBytes(ids) {
|
|
147
|
+
if (IDENTITY.ARE_IDS_HEX) return this.converter.hexToBytes(ids.join(''), IDENTITY.ID_LENGTH * ids.length);
|
|
148
|
+
return this.converter.stringToBytes(ids.join(''));
|
|
149
|
+
}
|
|
150
|
+
/** @param {string | Uint8Array | Object} data */
|
|
151
|
+
#dataToBytes(data) { // typeCodes: 1=string, 2=Uint8Array, 3=JSON
|
|
152
|
+
if (typeof data === 'string') return { dataCode: 1, dataBytes: this.converter.stringToBytes(data) };
|
|
153
|
+
if (data instanceof Uint8Array) return { dataCode: 2, dataBytes: data };
|
|
154
|
+
return { dataCode: 3, dataBytes: this.converter.stringToBytes(JSON.stringify(data)) };
|
|
155
|
+
}
|
|
156
|
+
/** @param {Uint8Array} bufferView @param {number} marker @param {number} dataCode @param {number} neighborsCount @param {number} timestamp @param {Uint8Array} dataBytes @param {Uint8Array} publicKey */
|
|
157
|
+
#setBufferHeader(bufferView, marker, dataCode, neighborsCount, timestamp, dataBytes, publicKey) {
|
|
158
|
+
const timestampBytes = this.converter.numberTo8Bytes(timestamp);
|
|
159
|
+
const dataLengthBytes = this.converter.numberTo4Bytes(dataBytes.length);
|
|
160
|
+
bufferView.set([marker], 0); // 1 byte for marker
|
|
161
|
+
bufferView.set([dataCode], 1); // 1 byte for data type code
|
|
162
|
+
bufferView.set([neighborsCount], 2); // 1 byte for neighbors length
|
|
163
|
+
bufferView.set(timestampBytes, 3); // 8 bytes for timestamp
|
|
164
|
+
bufferView.set(dataLengthBytes, 11); // 4 bytes for data length
|
|
165
|
+
bufferView.set(publicKey, 15); // 32 bytes for pubkey
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// MESSSAGE READING (DESERIALIZATION AND SIGNATURE VERIFICATION INCLUDED)
|
|
169
|
+
/** @param {Uint8Array} publicKey @param {Uint8Array} dataToVerify @param {Uint8Array} signature */
|
|
170
|
+
verifySignature(publicKey, dataToVerify, signature) {
|
|
171
|
+
if (this.AVOID_CRYPTO) return true;
|
|
172
|
+
return ed25519.verify(dataToVerify, signature, publicKey);
|
|
173
|
+
}
|
|
174
|
+
/** @param {Uint8Array} bufferView */
|
|
175
|
+
readBufferHeader(bufferView, readAssociatedId = true) {
|
|
176
|
+
const marker = bufferView[0]; // 1 byte for marker
|
|
177
|
+
const dataCode = bufferView[1]; // 1 byte for data type code
|
|
178
|
+
const neighborsCount = bufferView[2]; // 1 byte for neighbors length
|
|
179
|
+
const tBytes = bufferView.slice(3, 11); // 8 bytes for timestamp
|
|
180
|
+
const lBytes = bufferView.slice(11, 15); // 4 bytes for data length
|
|
181
|
+
const pubkey = bufferView.slice(15, 47); // 32 bytes for pubkey
|
|
182
|
+
const associatedId = readAssociatedId ? this.#idFromPublicKey(pubkey) : null;
|
|
183
|
+
const neighLength = neighborsCount * this.idLength;
|
|
184
|
+
const timestamp = this.converter.bytes8ToNumber(tBytes);
|
|
185
|
+
const dataLength = this.converter.bytes4ToNumber(lBytes);
|
|
186
|
+
return { marker, dataCode, neighLength, timestamp, dataLength, pubkey, associatedId };
|
|
187
|
+
}
|
|
188
|
+
/** @param {Uint8Array | ArrayBuffer} serialized @return {GossipMessage | null } */
|
|
189
|
+
readGossipMessage(serialized) {
|
|
190
|
+
if (this.verbose > 3) console.log(`%creadGossipMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
191
|
+
if (this.verbose > 3) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
192
|
+
try { // 1, 1, 1, 8, 4, 32, X, 64, 1
|
|
193
|
+
const { marker, dataCode, neighLength, timestamp, dataLength, pubkey, associatedId } = this.readBufferHeader(serialized);
|
|
194
|
+
const topic = GOSSIP.MARKERS_BYTES[marker];
|
|
195
|
+
if (topic === undefined) throw new Error(`Failed to deserialize gossip message: unknown marker byte ${d[0]}.`);
|
|
196
|
+
const NDBL = neighLength + dataLength;
|
|
197
|
+
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
198
|
+
const deserializedData = this.#bytesToData(dataCode, serialized.slice(47 + neighLength, 47 + NDBL));
|
|
199
|
+
const signatureStart = 47 + NDBL;
|
|
200
|
+
const signature = serialized.slice(signatureStart, signatureStart + IDENTITY.SIGNATURE_LENGTH);
|
|
201
|
+
const HOPS = serialized[serialized.length - 1];
|
|
202
|
+
const expectedEnd = signatureStart + IDENTITY.SIGNATURE_LENGTH + 1;
|
|
203
|
+
const senderId = associatedId;
|
|
204
|
+
return new GossipMessage(topic, timestamp, neighbors, HOPS, senderId, pubkey, deserializedData, signature, signatureStart, expectedEnd);
|
|
205
|
+
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${topic || 'unknown'} gossip message:`, error.stack); }
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/** @param {Uint8Array | ArrayBuffer} serialized @return {DirectMessage | ReroutedDirectMessage | null} */
|
|
209
|
+
readUnicastMessage(serialized) {
|
|
210
|
+
if (this.verbose > 3) console.log(`%creadUnicastMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
211
|
+
if (this.verbose > 3) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
212
|
+
try { // 1, 1, 1, 8, 4, 32, X, 1, X, 64
|
|
213
|
+
const { marker, dataCode, neighLength, timestamp, dataLength, pubkey } = this.readBufferHeader(serialized, false);
|
|
214
|
+
const type = UNICAST.MARKERS_BYTES[marker];
|
|
215
|
+
if (type === undefined) throw new Error(`Failed to deserialize unicast message: unknown marker byte ${d[0]}.`);
|
|
216
|
+
const NDBL = neighLength + dataLength;
|
|
217
|
+
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
218
|
+
const deserializedData = this.#bytesToData(dataCode, serialized.slice(47 + neighLength, 47 + NDBL));
|
|
219
|
+
const routeLength = serialized[47 + NDBL];
|
|
220
|
+
const routeBytesLength = routeLength * this.idLength;
|
|
221
|
+
const signatureStart = 47 + NDBL + 1 + routeBytesLength;
|
|
222
|
+
const routeBytes = serialized.slice(47 + NDBL + 1, signatureStart);
|
|
223
|
+
const route = this.#bytesToIds(routeBytes);
|
|
224
|
+
const initialMessageEnd = signatureStart + IDENTITY.SIGNATURE_LENGTH;
|
|
225
|
+
const signature = serialized.slice(signatureStart, initialMessageEnd);
|
|
226
|
+
const isPatched = (serialized.length > initialMessageEnd);
|
|
227
|
+
|
|
228
|
+
if (!isPatched) return new DirectMessage(type, timestamp, neighbors, route, pubkey, deserializedData, signature, signatureStart, initialMessageEnd);
|
|
229
|
+
|
|
230
|
+
const rerouterPubkey = serialized.slice(initialMessageEnd, initialMessageEnd + 32);
|
|
231
|
+
const newRoute = this.#bytesToIds(serialized.slice(initialMessageEnd + 32, serialized.length - IDENTITY.SIGNATURE_LENGTH));
|
|
232
|
+
const rerouterSignature = serialized.slice(serialized.length - IDENTITY.SIGNATURE_LENGTH);
|
|
233
|
+
return new ReroutedDirectMessage(type, timestamp, neighbors, route, pubkey, deserializedData, signature, rerouterPubkey, newRoute, rerouterSignature, serialized.length);
|
|
234
|
+
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${type || 'unknown'} unicast message:`, error.stack); }
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/** @param {Uint8Array} serialized */
|
|
238
|
+
#bytesToIds(serialized) {
|
|
239
|
+
const ids = [];
|
|
240
|
+
const idLength = this.idLength;
|
|
241
|
+
if (serialized.length % idLength !== 0) throw new Error('Failed to parse ids: invalid serialized length.');
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < serialized.length / idLength; i++) {
|
|
244
|
+
const idBytes = serialized.slice(i * idLength, (i + 1) * idLength);
|
|
245
|
+
if (IDENTITY.ARE_IDS_HEX) ids.push(this.converter.bytesToHex(idBytes, IDENTITY.ID_LENGTH));
|
|
246
|
+
else ids.push(this.converter.bytesToString(idBytes));
|
|
247
|
+
}
|
|
248
|
+
return ids;
|
|
249
|
+
}
|
|
250
|
+
/** @param {1 | 2 | 3} dataCode @param {Uint8Array} dataBytes @return {string | Uint8Array | Object} */
|
|
251
|
+
#bytesToData(dataCode, dataBytes) {
|
|
252
|
+
if (dataCode === 1) return this.converter.bytesToString(dataBytes);
|
|
253
|
+
if (dataCode === 2) return dataBytes;
|
|
254
|
+
if (dataCode === 3) return JSON.parse(this.converter.bytesToString(dataBytes));
|
|
255
|
+
throw new Error(`Failed to parse data: unknown data code '${dataCode}'.`);
|
|
256
|
+
}
|
|
257
|
+
}
|