trac-msb 0.1.75 → 0.1.77-msb-r2-dev.0

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.
Files changed (161) hide show
  1. package/.github/workflows/CI.yml +40 -0
  2. package/README.md +12 -0
  3. package/docs/TIP/TIP-000.md +49 -0
  4. package/docs/TIP/TIP-001.md +220 -0
  5. package/msb.mjs +34 -6
  6. package/package.json +32 -9
  7. package/proto/applyOperations.proto +117 -0
  8. package/rpc/constants.mjs +2 -0
  9. package/rpc/cors.mjs +15 -0
  10. package/rpc/create_server.mjs +39 -0
  11. package/rpc/handlers.mjs +167 -0
  12. package/rpc/router.mjs +23 -0
  13. package/rpc/rpc_server.mjs +10 -0
  14. package/rpc/utils/helpers.mjs +67 -0
  15. package/scripts/generate-protobufs.js +42 -0
  16. package/src/core/network/Network.js +259 -0
  17. package/src/core/network/messaging/NetworkMessages.js +63 -0
  18. package/src/core/network/messaging/handlers/GetRequestHandler.js +112 -0
  19. package/src/core/network/messaging/handlers/OperationHandler.js +114 -0
  20. package/src/core/network/messaging/handlers/ResponseHandler.js +112 -0
  21. package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +143 -0
  22. package/src/core/network/messaging/handlers/TransferOperationHandler.js +52 -0
  23. package/src/core/network/messaging/handlers/WhitelistedEventHandler.js +88 -0
  24. package/src/core/network/messaging/handlers/base/BaseOperationHandler.js +63 -0
  25. package/src/core/network/messaging/routes/NetworkMessageRouter.js +104 -0
  26. package/src/core/network/messaging/validators/AdminResponse.js +62 -0
  27. package/src/core/network/messaging/validators/CustomNodeResponse.js +49 -0
  28. package/src/core/network/messaging/validators/PartialBootstrapDeployment.js +174 -0
  29. package/src/core/network/messaging/validators/PartialRoleAccess.js +229 -0
  30. package/src/core/network/messaging/validators/PartialTransaction.js +206 -0
  31. package/src/core/network/messaging/validators/PartialTransfer.js +205 -0
  32. package/src/core/network/messaging/validators/ValidatorResponse.js +72 -0
  33. package/src/core/network/messaging/validators/WhitelistEvent.js +117 -0
  34. package/src/core/network/messaging/validators/base/BaseResponse.js +92 -0
  35. package/src/core/network/messaging/validators/base/PartialOperation.js +8 -0
  36. package/src/core/network/services/PoolService.js +43 -0
  37. package/src/core/network/services/TransactionRateLimiterService.js +121 -0
  38. package/src/core/network/services/ValidatorObserverService.js +97 -0
  39. package/src/core/state/State.js +3643 -0
  40. package/src/core/state/utils/address.js +71 -0
  41. package/src/core/state/utils/adminEntry.js +67 -0
  42. package/src/core/state/utils/balance.js +302 -0
  43. package/src/core/state/utils/deploymentEntry.js +75 -0
  44. package/src/core/state/utils/indexerEntry.js +105 -0
  45. package/src/core/state/utils/lengthEntry.js +94 -0
  46. package/src/core/state/utils/nodeEntry.js +346 -0
  47. package/src/core/state/utils/roles.js +53 -0
  48. package/src/core/state/utils/transaction.js +117 -0
  49. package/src/index.js +996 -815
  50. package/src/messages/base/StateBuilder.js +25 -0
  51. package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +401 -0
  52. package/src/messages/completeStateMessages/CompleteStateMessageDirector.js +252 -0
  53. package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +307 -0
  54. package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +271 -0
  55. package/src/messages/partialStateMessages/PartialStateMessageDirector.js +137 -0
  56. package/src/messages/partialStateMessages/PartialStateMessageOperations.js +131 -0
  57. package/src/utils/amountSerialization.js +110 -0
  58. package/src/utils/buffer.js +62 -0
  59. package/src/utils/check.js +475 -114
  60. package/src/utils/cli.js +106 -0
  61. package/src/utils/constants.js +92 -41
  62. package/src/utils/crypto.js +11 -0
  63. package/src/utils/fileUtils.js +79 -14
  64. package/src/utils/helpers.js +99 -0
  65. package/src/utils/normalizers.js +85 -0
  66. package/src/utils/operations.js +95 -0
  67. package/src/utils/protobuf/applyOperations.cjs +1373 -0
  68. package/src/utils/protobuf/operationHelpers.js +50 -0
  69. package/src/utils/transactionUtils.js +58 -0
  70. package/test/acceptance/rpc.test.mjs +145 -0
  71. package/test/all.test.js +10 -8
  72. package/test/apply/addAdmin/addAdminBasic.test.js +68 -0
  73. package/test/apply/addAdmin/addAdminRecovery.test.js +117 -0
  74. package/test/apply/addIndexer.test.js +261 -0
  75. package/test/apply/addWhitelist.test.js +53 -0
  76. package/test/apply/addWriter.test.js +396 -0
  77. package/test/apply/apply.test.js +17 -0
  78. package/test/apply/banValidator.test.js +122 -0
  79. package/test/apply/postTx.test.js +227 -0
  80. package/test/apply/removeIndexer.test.js +142 -0
  81. package/test/apply/removeWriter.test.js +215 -0
  82. package/test/buffer/buffer.test.js +190 -0
  83. package/test/check/adminControlOperation.test.js +156 -0
  84. package/test/check/balanceInitializationOperation.test.js +54 -0
  85. package/test/check/bootstrapDeploymentOperation.test.js +105 -0
  86. package/test/check/check.test.js +17 -0
  87. package/test/check/common.test.js +418 -0
  88. package/test/check/coreAdminOperation.test.js +95 -0
  89. package/test/check/roleAccessOperation.test.js +254 -0
  90. package/test/check/transactionOperation.test.js +107 -0
  91. package/test/check/transferOperation.test.js +102 -0
  92. package/test/fileUtils/readBalanceMigrationFile.test.js +62 -0
  93. package/test/fileUtils/readPublicKeysFromFile.test.js +53 -0
  94. package/test/fixtures/apply.fixtures.js +41 -0
  95. package/test/fixtures/assembleMessage.fixtures.js +40 -0
  96. package/test/fixtures/check.fixtures.js +427 -0
  97. package/test/fixtures/protobuf.fixtures.js +289 -0
  98. package/test/functions/amountSerialization.test.js +237 -0
  99. package/test/functions/applyOperations.test.js +91 -0
  100. package/test/functions/createHash.test.js +15 -0
  101. package/test/functions/functions.test.js +14 -0
  102. package/test/functions/isHexString.test.js +12 -0
  103. package/test/functions/normalizeHex.test.js +82 -0
  104. package/test/messageOperations/assembleAddIndexerMessage.test.js +21 -0
  105. package/test/messageOperations/assembleAddWriterMessage.test.js +16 -0
  106. package/test/messageOperations/assembleAdminMessage.test.js +69 -0
  107. package/test/messageOperations/assembleBanWriterMessage.test.js +16 -0
  108. package/test/messageOperations/assemblePostTransaction.test.js +442 -0
  109. package/test/messageOperations/assembleRemoveIndexerMessage.test.js +19 -0
  110. package/test/messageOperations/assembleRemoveWriterMessage.test.js +17 -0
  111. package/test/messageOperations/assembleWhitelistMessages.test.js +58 -0
  112. package/test/messageOperations/commonsStateMessageOperationsTest.js +277 -0
  113. package/test/messageOperations/stateMessageOperations.test.js +19 -0
  114. package/test/protobuf/protobuf.test.js +185 -0
  115. package/test/state/State.test.js +80 -0
  116. package/test/state/stateTestUtils.js +24 -0
  117. package/test/state/stateTests.test.js +23 -0
  118. package/test/state/utils/address.test.js +63 -0
  119. package/test/state/utils/adminEntry.test.js +51 -0
  120. package/test/state/utils/balance.test.js +320 -0
  121. package/test/state/utils/indexerEntry.test.js +83 -0
  122. package/test/state/utils/lengthEntry.test.js +54 -0
  123. package/test/state/utils/nodeEntry.test.js +298 -0
  124. package/test/state/utils/roles.test.js +44 -0
  125. package/test/state/utils/transaction.test.js +97 -0
  126. package/test/utils/regexHelper.js +3 -0
  127. package/test/utils/setupApplyTests.js +496 -0
  128. package/whitelist/.gitkeep +0 -0
  129. package/Whitelist/pubkeys.csv +0 -1
  130. package/dump/LICENSE +0 -201
  131. package/dump/NOTICE +0 -17
  132. package/dump/README.md +0 -66
  133. package/dump/Whitelist/pubkeys.csv +0 -1
  134. package/dump/msb.mjs +0 -14
  135. package/dump/package.json +0 -40
  136. package/dump/src/index.js +0 -1018
  137. package/dump/src/network.js +0 -295
  138. package/dump/src/utils/check.js +0 -163
  139. package/dump/src/utils/constants.js +0 -63
  140. package/dump/src/utils/fileUtils.js +0 -31
  141. package/dump/src/utils/functions.js +0 -70
  142. package/dump/src/utils/msgUtils.js +0 -137
  143. package/dump/test/all.test.js +0 -18
  144. package/dump/test/check.test.js +0 -21
  145. package/dump/test/fileUtils.test.js +0 -16
  146. package/dump/test/functions.test.js +0 -22
  147. package/src/network.js +0 -295
  148. package/src/utils/functions.js +0 -70
  149. package/src/utils/msgUtils.js +0 -137
  150. package/stores2/nzoomlol2/CORESTORE +0 -4
  151. package/stores2/nzoomlol2/db/000004.log +0 -0
  152. package/stores2/nzoomlol2/db/CURRENT +0 -1
  153. package/stores2/nzoomlol2/db/IDENTITY +0 -1
  154. package/stores2/nzoomlol2/db/LOG +0 -456
  155. package/stores2/nzoomlol2/db/MANIFEST-000005 +0 -0
  156. package/stores2/nzoomlol2/db/OPTIONS-000007 +0 -330
  157. package/stores2/nzoomlol2/db/keypair.json +0 -5
  158. package/test/check.test.js +0 -21
  159. package/test/fileUtils.test.js +0 -16
  160. package/test/functions.test.js +0 -22
  161. /package/{stores2/nzoomlol2/db/LOCK → migration/.gitkeep} +0 -0
@@ -0,0 +1,167 @@
1
+ import {decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure} from "./utils/helpers.mjs"
2
+ import { MAX_SIGNED_LENGTH, SIGNED_LENGTH_OFFSET } from "./constants.mjs";
3
+
4
+ export async function handleBalance({ req, respond, msbInstance }) {
5
+ const [path, queryString] = req.url.split("?");
6
+ const parts = path.split("/").filter(Boolean);
7
+ const address = parts[1];
8
+
9
+ let confirmed = true; // default
10
+ if (queryString) {
11
+ const params = new URLSearchParams(queryString);
12
+ if (params.has("confirmed")) {
13
+ confirmed = params.get("confirmed") === "true";
14
+ }
15
+ }
16
+
17
+ if (!address) {
18
+ res.writeHead(400, { 'Content-Type': 'application/json' });
19
+ res.end(JSON.stringify({ error: 'Wallet address is required' }));
20
+ return;
21
+ }
22
+
23
+ const commandString =`/get_balance ${address} ${confirmed}`;
24
+ const nodeInfo = await msbInstance.handleCommand(commandString);
25
+ const balance = nodeInfo?.balance || 0;
26
+ respond(200, { address, balance });
27
+ }
28
+
29
+ export async function handleTxv({ msbInstance, respond }) {
30
+ const commandString = '/get_txv';
31
+ const txvRaw = await msbInstance.handleCommand(commandString);
32
+ const txv = txvRaw.toString('hex');
33
+ respond(200, { txv });
34
+ }
35
+
36
+ export async function handleFee({ msbInstance, respond }) {
37
+ const commandString = '/get_fee';
38
+ const fee = await msbInstance.handleCommand(commandString);
39
+ respond(200, { fee });
40
+ }
41
+
42
+ export async function handleConfirmedLength({ msbInstance, respond }) {
43
+ const commandString = '/confirmed_length';
44
+ const confirmed_length = await msbInstance.handleCommand(commandString);
45
+ respond(200, { confirmed_length });
46
+ }
47
+
48
+ export async function handleBroadcastTransaction({ msbInstance, respond, req }) {
49
+ let body = '';
50
+ req.on('data', chunk => {
51
+ body += chunk.toString();
52
+ });
53
+
54
+ req.on('end', async () => {
55
+ const { payload } = JSON.parse(body);
56
+ if (!payload) {
57
+ return respond(400, { error: 'Payload is missing.' });
58
+ }
59
+
60
+ if (!isBase64(payload)) {
61
+ return respond(400, { error: 'Payload must be a valid base64 string.' });
62
+ }
63
+
64
+ const decodedPayload = decodeBase64Payload(payload);
65
+ validatePayloadStructure(decodedPayload);
66
+ const sanitizedPayload = sanitizeTransferPayload(decodedPayload);
67
+ const result = await msbInstance.handleCommand('/broadcast_transaction', null, sanitizedPayload);
68
+ respond(200, { result });
69
+ });
70
+ }
71
+
72
+ export async function handleTxHashes({ msbInstance, respond, req }) {
73
+ const startSignedLengthStr = req.url.split('/')[2];
74
+ const endSignedLengthStr = req.url.split('/')[3];
75
+
76
+ const startSignedLength = parseInt(startSignedLengthStr);
77
+ const endSignedLength = parseInt(endSignedLengthStr);
78
+
79
+ // 1. Check if the parsed values are valid numbers
80
+ if (isNaN(startSignedLength) || isNaN(endSignedLength)) {
81
+ return respond(400, { error: 'Params must be integer' });
82
+ }
83
+
84
+ // 2. Check for non-negative numbers
85
+ // The requirement is "non-negative," which includes 0.
86
+ if (startSignedLength < 0 || endSignedLength < 0) {
87
+ return respond(400, { error: 'Params must be non-negative' });
88
+ }
89
+
90
+ // 3. endSignedLength must be >= startSignedLength
91
+ if (endSignedLength < startSignedLength) {
92
+ return respond(400, { error: 'endSignedLength must be greater than or equal to startSignedLength.' });
93
+ }
94
+
95
+ if (endSignedLength - startSignedLength > MAX_SIGNED_LENGTH) {
96
+ return respond(400, { error: `The max range for signedLength must be ${MAX_SIGNED_LENGTH}.` });
97
+ }
98
+
99
+ // 4. Get current confirmed length
100
+ const currentConfirmedLength = await msbInstance.handleCommand('/confirmed_length');
101
+
102
+ // 5. Adjust the end index to not exceed the confirmed length.
103
+ const adjustedEndLength = Math.min(endSignedLength, currentConfirmedLength) + SIGNED_LENGTH_OFFSET // apply an offset to include in the end
104
+
105
+ // 6. Fetch txs hashes for the adjusted range, assuming the command takes start and end index.
106
+ const commandString = `/get_txs_hashes ${startSignedLength} ${adjustedEndLength}`;
107
+ const { hashes } = await msbInstance.handleCommand(commandString);
108
+ respond(200, { hashes });
109
+ }
110
+
111
+ export async function handleUnconfirmedLength({ msbInstance, respond }) {
112
+ const commandString = '/unconfirmed_length';
113
+ const unconfirmed_length = await msbInstance.handleCommand(commandString);
114
+ respond(200, { unconfirmed_length });
115
+ }
116
+
117
+ export async function handleTransactionDetails({ msbInstance, respond, req }) {
118
+ const hash = req.url.split('/')[2];
119
+ const commandString = `/get_tx_details ${hash}`;
120
+ const txDetails = await msbInstance.handleCommand(commandString);
121
+ respond(200, { txDetails });
122
+ }
123
+
124
+ export async function handleFetchBulkTxPayloads({ msbInstance, respond, req }) {
125
+ let body = ''
126
+ let bytesRead = 0;
127
+ let limitBytes = 1_000_000;
128
+
129
+ req.on('data', chunk => {
130
+ bytesRead += chunk.length;
131
+ if (bytesRead > limitBytes) { respond(413, { error: 'Request body too large.' }) }
132
+ body += chunk.toString();
133
+ });
134
+
135
+ req.on('end', async () => {
136
+ if (body === null || body === ''){
137
+ return respond(400, { error: 'Missing payload.' });
138
+ }
139
+
140
+ const sanitizedPayload = sanitizeBulkPayloadsRequestBody(body);
141
+
142
+ if (sanitizedPayload === null){
143
+ return respond(400, { error: 'Invalid payload.' });
144
+ }
145
+
146
+ const { hashes } = sanitizedPayload;
147
+
148
+ if (!Array.isArray(hashes) || hashes.length === 0) {
149
+ return respond(400, { error: 'Missing hash list.' });
150
+ }
151
+
152
+ if (hashes.length > 1500) {
153
+ return respond(413, { error: 'Too many hashes. Max 1500 allowed per request.' });
154
+ }
155
+
156
+ const uniqueHashes = [...new Set(hashes)];
157
+
158
+ const commandResult = await msbInstance.handleCommand( `/get_tx_payloads_bulk`, null, uniqueHashes)
159
+
160
+ const responseString = JSON.stringify(commandResult);
161
+ if (Buffer.byteLength(responseString, 'utf8') > 2_000_000) {
162
+ return respond(413, { error: 'Response too large. Reduce number of hashes.'});
163
+ }
164
+
165
+ return respond(200, commandResult);
166
+ })
167
+ }
package/rpc/router.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ handleBalance,
3
+ handleTxv,
4
+ handleFee,
5
+ handleConfirmedLength,
6
+ handleBroadcastTransaction,
7
+ handleTxHashes,
8
+ handleUnconfirmedLength,
9
+ handleTransactionDetails,
10
+ handleFetchBulkTxPayloads
11
+ } from './handlers.mjs';
12
+
13
+ export const routes = [
14
+ { method: 'GET', path: '/balance', handler: handleBalance },
15
+ { method: 'GET', path: '/txv', handler: handleTxv },
16
+ { method: 'GET', path: '/fee', handler: handleFee },
17
+ { method: 'GET', path: '/confirmed-length', handler: handleConfirmedLength },
18
+ { method: 'POST', path: '/broadcast-transaction', handler: handleBroadcastTransaction },
19
+ { method: 'GET', path: '/tx-hashes/', handler: handleTxHashes },
20
+ { method: 'GET', path: '/unconfirmed-length', handler: handleUnconfirmedLength },
21
+ { method: 'GET', path: '/tx', handler: handleTransactionDetails },
22
+ { method: 'POST', path: '/tx-payloads-bulk', handler: handleFetchBulkTxPayloads },
23
+ ];
@@ -0,0 +1,10 @@
1
+ import { createServer } from "./create_server.mjs";
2
+
3
+ // Called by msb.mjs file
4
+ export function startRpcServer(msbInstance, port) {
5
+ const server = createServer(msbInstance)
6
+
7
+ return server.listen(port, () => {
8
+ console.log(`Running RPC with https at https://localhost:${port}`);
9
+ });
10
+ }
@@ -0,0 +1,67 @@
1
+ import b4a from "b4a"
2
+
3
+ export function decodeBase64Payload(base64) {
4
+ let decodedPayloadString
5
+ try {
6
+ decodedPayloadString = b4a.from(base64, "base64").toString("utf-8")
7
+ } catch (err) {
8
+ throw new Error("Failed to decode base64 payload.")
9
+ }
10
+
11
+ try {
12
+ return JSON.parse(decodedPayloadString)
13
+ } catch (err) {
14
+ throw new Error("Decoded payload is not valid JSON.")
15
+ }
16
+ }
17
+
18
+ export function isBase64(str) {
19
+ const base64Regex =
20
+ /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
21
+ return base64Regex.test(str)
22
+ }
23
+
24
+ export function validatePayloadStructure(payload) {
25
+ if (
26
+ typeof payload !== "object" ||
27
+ payload === null ||
28
+ typeof payload.type !== "number" ||
29
+ typeof payload.address !== "string" ||
30
+ typeof payload.tro !== "object"
31
+ ) {
32
+ throw new Error("Invalid payload structure.")
33
+ }
34
+ }
35
+
36
+ export function sanitizeTransferPayload(payload) {
37
+ if (payload.address && typeof payload.address === "string") {
38
+ payload.address = payload.address.trim()
39
+ }
40
+
41
+ if (payload.tro && typeof payload.tro === "object") {
42
+ for (const [key, value] of Object.entries(payload.tro)) {
43
+ if (typeof value === "string") {
44
+ let sanitized = value.trim()
45
+
46
+ // normalize hex-like strings
47
+ if (/^[0-9A-F]+$/i.test(sanitized)) {
48
+ sanitized = sanitized.toLowerCase()
49
+ }
50
+
51
+ payload.tro[key] = sanitized
52
+ }
53
+ }
54
+ }
55
+
56
+ return payload
57
+ }
58
+
59
+ export function sanitizeBulkPayloadsRequestBody(body) {
60
+ const cleanBody = body
61
+ .replace(/^\uFEFF/, '')
62
+ .replace(/\r/g, '')
63
+ .replace(/\0/g, '')
64
+ .trim();
65
+
66
+ return JSON.parse(cleanBody);
67
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import { fileURLToPath } from 'url';
5
+ import { execSync } from 'child_process';
6
+
7
+ function generateCJSFromProto(inputPath, outputPath) {
8
+ execSync(`protocol-buffers "${inputPath}" -o "${outputPath}"`);
9
+ console.log(`${outputPath} has been generated.`);
10
+ }
11
+
12
+ function transformToUseB4a(outputPath) {
13
+ let content = fs.readFileSync(outputPath, 'utf-8');
14
+ content = content.replace(/\bBuffer\.([a-zA-Z]+)/g, 'b4a.$1');
15
+ content = `var b4a = require('b4a');\n` + content;
16
+ fs.writeFileSync(outputPath, content, 'utf-8');
17
+ console.log(`${outputPath} has been modified to use b4a.`);
18
+ }
19
+
20
+
21
+ function main() {
22
+ const directoryName = path.dirname(fileURLToPath(import.meta.url));
23
+
24
+ const inputDir = path.join(directoryName, '../proto');
25
+ const outputDir = path.join(directoryName, '../src/utils/protobuf');
26
+
27
+ fs.mkdirSync(outputDir, { recursive: true });
28
+
29
+ const files = fs.readdirSync(inputDir).filter(f => f.endsWith('.proto'));
30
+
31
+ for (const file of files) {
32
+ const name = path.basename(file, '.proto');
33
+ const inputPath = path.join(inputDir, file);
34
+ const outputPath = path.join(outputDir, `${name}.cjs`);
35
+
36
+ generateCJSFromProto(inputPath, outputPath);
37
+ transformToUseB4a(outputPath);
38
+ }
39
+ }
40
+
41
+ main();
42
+
@@ -0,0 +1,259 @@
1
+ import ReadyResource from 'ready-resource';
2
+ import Hyperswarm from 'hyperswarm';
3
+ import w from 'protomux-wakeup';
4
+ import b4a from 'b4a';
5
+
6
+ import PoolService from './services/PoolService.js';
7
+ import ValidatorObserverService from './services/ValidatorObserverService.js';
8
+ import NetworkMessages from './messaging/NetworkMessages.js';
9
+ import { sleep } from '../../utils/helpers.js';
10
+ import {
11
+ TRAC_NAMESPACE,
12
+ MAX_PEERS,
13
+ MAX_PARALLEL,
14
+ MAX_SERVER_CONNECTIONS,
15
+ MAX_CLIENT_CONNECTIONS,
16
+ NETWORK_MESSAGE_TYPES,
17
+ DHT_BOOTSTRAPS
18
+ } from '../../utils/constants.js';
19
+
20
+ const wakeup = new w();
21
+
22
+ class Network extends ReadyResource {
23
+ #dht_bootstrap = DHT_BOOTSTRAPS;
24
+ #swarm = null;
25
+ #enable_wallet;
26
+ #channel;
27
+ #networkMessages;
28
+ #poolService;
29
+ #validatorObserverService;
30
+
31
+ constructor(state, channel, options = {}) {
32
+ super();
33
+ this.#enable_wallet = options.enable_wallet !== false;
34
+ this.#channel = channel;
35
+ this.#poolService = new PoolService(state)
36
+ this.#validatorObserverService = new ValidatorObserverService(this, state, options)
37
+ this.#networkMessages = new NetworkMessages(this, options);
38
+ //TODO: move streams maybe to HASHMAP? To discuss because this change will affect the whole network module and it's usage. It is not a priority right now
39
+ //However, it gives us more flexibility in the future, because we can create set of streams. Maybe in this case exist better data structure?
40
+ this.admin_stream = null;
41
+ this.admin = null;
42
+ this.validator_stream = null;
43
+ this.validator = null;
44
+ this.custom_stream = null;
45
+ this.custom_node = null;
46
+ }
47
+
48
+ get swarm() {
49
+ return this.#swarm;
50
+ }
51
+
52
+ get channel() {
53
+ return this.#channel;
54
+ }
55
+
56
+ get poolService() {
57
+ return this.#poolService;
58
+ }
59
+
60
+ get validatorObserverService() {
61
+ return this.#validatorObserverService;
62
+ }
63
+
64
+ async _open() {
65
+ console.log('Network initialization...');
66
+ this.poolService.start();
67
+ }
68
+
69
+ async _close() {
70
+ console.log('Network: closing gracefully...');
71
+ this.poolService.stopPool();
72
+ await sleep(100);
73
+
74
+ if (this.#validatorObserverService.enable_validator_observer) {
75
+ this.#validatorObserverService.stopValidatorObserver();
76
+ }
77
+
78
+ await sleep(5_000);
79
+
80
+ if (this.#swarm !== null) {
81
+ this.#swarm.destroy();
82
+ }
83
+ }
84
+
85
+ startValidatorObserver(address) {
86
+ this.#validatorObserverService.startValidatorObserver();
87
+ this.#validatorObserverService.validatorObserver(address);
88
+ }
89
+
90
+ async replicate(
91
+ state,
92
+ store,
93
+ wallet,
94
+ ) {
95
+ if (!this.#swarm) {
96
+ const keyPair = await this.initializeNetworkingKeyPair(store, wallet);
97
+ this.#swarm = new Hyperswarm({
98
+ keyPair,
99
+ bootstrap: this.#dht_bootstrap,
100
+ maxPeers: MAX_PEERS,
101
+ maxParallel: MAX_PARALLEL,
102
+ maxServerConnections: MAX_SERVER_CONNECTIONS,
103
+ maxClientConnections: MAX_CLIENT_CONNECTIONS
104
+ });
105
+
106
+ console.log(`Channel: ${b4a.toString(this.#channel)}`);
107
+ this.#networkMessages.initializeMessageRouter(state, wallet);
108
+
109
+ this.#swarm.on('connection', async (connection) => {
110
+ const { message_channel, message } = this.#networkMessages.setupProtomuxMessages(connection);
111
+ connection.messenger = message;
112
+
113
+ connection.on('close', () => {
114
+ if (this.validator_stream === connection) {
115
+ this.validator_stream = null;
116
+ this.validator = null;
117
+ }
118
+
119
+ if (this.admin_stream === connection) {
120
+ this.admin_stream = null;
121
+ this.admin = null;
122
+ }
123
+
124
+ if (this.custom_stream === connection) {
125
+ this.custom_stream = null;
126
+ this.custom_node = null;
127
+ }
128
+
129
+ message_channel.close()
130
+ });
131
+
132
+ // ATTENTION: Must be called AFTER the protomux init above
133
+ const stream = store.replicate(connection);
134
+ wakeup.addStream(stream);
135
+
136
+ connection.on('error', (error) => {
137
+ if (
138
+ error && error.message && (
139
+ error.message.includes('connection reset by peer') ||
140
+ error.message.includes('Duplicate connection')
141
+ )
142
+ ) {
143
+ // TODO: decide if we want to handle this error in a specific way. It generates a lot of logs.
144
+ return;
145
+ }
146
+ console.error(error.message)
147
+
148
+ });
149
+
150
+ });
151
+
152
+ this.#swarm.join(this.#channel, { server: true, client: true });
153
+ await this.#swarm.flush();
154
+ }
155
+ }
156
+
157
+ async initializeNetworkingKeyPair(store, wallet) {
158
+ if (!this.#enable_wallet) {
159
+ return await store.createKeyPair(TRAC_NAMESPACE);
160
+ } else {
161
+ return {
162
+ publicKey: wallet.publicKey,
163
+ secretKey: wallet.secretKey
164
+ };
165
+ }
166
+ }
167
+
168
+ async tryConnect(publicKey, type = null) {
169
+
170
+ if (null === this.#swarm) throw new Error('Network swarm is not initialized');
171
+
172
+ if (this.validator_stream !== null && publicKey !== b4a.toString(this.validator_stream.remotePublicKey, 'hex')) {
173
+ this.#swarm.leavePeer(this.validator_stream.remotePublicKey);
174
+ this.validator_stream = null;
175
+ this.validator = null;
176
+ }
177
+ // trying to join a peer from the global swarm
178
+
179
+ if (false === this.#swarm.peers.has(publicKey)) {
180
+ this.#swarm.joinPeer(b4a.from(publicKey, 'hex'));
181
+ let cnt = 0;
182
+ while (false === this.#swarm.peers.has(publicKey)) {
183
+ if (cnt >= 1500) break;
184
+ await sleep(10);
185
+ cnt += 1;
186
+ }
187
+ }
188
+
189
+ if (this.#swarm.peers.has(publicKey)) {
190
+ let stream;
191
+ const peerInfo = this.#swarm.peers.get(publicKey)
192
+ stream = this.#swarm._allConnections.get(peerInfo.publicKey)
193
+
194
+ if (stream !== undefined && stream.messenger !== undefined) {
195
+ await this.#sendRequestByType(stream, type);
196
+ }
197
+ }
198
+ }
199
+
200
+ async isConnected(publicKey) {
201
+ return this.#swarm.peers.has(publicKey) &&
202
+ this.#swarm.peers.get(publicKey).connectedTime != -1
203
+ }
204
+
205
+ async #sendRequestByType(stream, type) {
206
+ const waitFor = {
207
+ validator: () => this.validator_stream,
208
+ admin: () => this.admin_stream,
209
+ node: () => this.custom_stream
210
+ }[type];
211
+
212
+ if (type === 'validator') {
213
+ await stream.messenger.send(NETWORK_MESSAGE_TYPES.GET.VALIDATOR);
214
+ } else if (type === 'admin') {
215
+ await stream.messenger.send(NETWORK_MESSAGE_TYPES.GET.ADMIN);
216
+ } else if (type === 'node') {
217
+ await stream.messenger.send(NETWORK_MESSAGE_TYPES.GET.NODE);
218
+ } else {
219
+ return;
220
+ }
221
+ await this.spinLock(() => !waitFor)
222
+ };
223
+
224
+ async spinLock(conditionFn, maxIterations = 1500, intervalMs = 10) {
225
+ let counter = 0;
226
+ while (conditionFn() && counter < maxIterations) {
227
+ await sleep(intervalMs);
228
+ counter++;
229
+ }
230
+ }
231
+
232
+ async sendMessageToNode(nodePublicKey, message) {
233
+ try {
234
+ if (!nodePublicKey || !message) {
235
+ return;
236
+ }
237
+ await this.tryConnect(nodePublicKey, 'node');
238
+
239
+ await this.spinLock(() =>
240
+ this.custom_stream === null ||
241
+ !b4a.equals(this.custom_node, b4a.from(nodePublicKey, 'hex'))
242
+ );
243
+ if (
244
+ this.custom_stream !== null &&
245
+ this.custom_node !== null &&
246
+ b4a.equals(this.custom_node, b4a.from(nodePublicKey, 'hex'))
247
+ ) {
248
+ await this.custom_stream.messenger.send(message);
249
+ } else {
250
+ throw new Error(`Failed to send message to node: ${nodePublicKey}`);
251
+ }
252
+
253
+ } catch (e) {
254
+ console.log(e)
255
+ }
256
+ }
257
+ }
258
+
259
+ export default Network;
@@ -0,0 +1,63 @@
1
+
2
+ import Protomux from 'protomux';
3
+ import b4a from 'b4a';
4
+ import c from 'compact-encoding';
5
+ import NetworkMessageRouter from './routes/NetworkMessageRouter.js';
6
+
7
+ class NetworkMessages {
8
+ #messageRouter;
9
+ #network;
10
+ #options;
11
+
12
+ constructor(network, options = {}) {
13
+ this.#network = network;
14
+ this.#options = options;
15
+ }
16
+
17
+ get network() {
18
+ return this.#network;
19
+ }
20
+
21
+ initializeMessageRouter(state, wallet) {
22
+ this.#messageRouter = new NetworkMessageRouter(
23
+ this.network,
24
+ state,
25
+ wallet,
26
+ this.#options
27
+ );
28
+ }
29
+
30
+ setupProtomuxMessages(connection) {
31
+ const mux = Protomux.from(connection);
32
+ connection.userData = mux;
33
+ const message_channel = mux.createChannel({
34
+ protocol: b4a.toString(this.network.channel, 'utf8'),
35
+ onopen() { },
36
+ onclose() { }
37
+ });
38
+
39
+ message_channel.open();
40
+
41
+ const messageProtomux = message_channel.addMessage({
42
+ encoding: c.json,
43
+ onmessage: async (incomingMessage) => {
44
+ try {
45
+ if (typeof incomingMessage === 'object' || typeof incomingMessage === 'string') {
46
+ await this.#messageRouter.route(incomingMessage, connection, messageProtomux);
47
+ } else {
48
+ throw new Error('NetworkMessages: Received message is undefined');
49
+ }
50
+ } catch (error) {
51
+ throw new Error(`NetworkMessages: Failed to handle incoming message: ${error.message}`);
52
+ } finally {
53
+ this.network.swarm.leavePeer(connection.remotePublicKey);
54
+ }
55
+ }
56
+ });
57
+
58
+ connection.messenger = messageProtomux;
59
+ return { message_channel, message: messageProtomux };
60
+ }
61
+ }
62
+
63
+ export default NetworkMessages;