trac-msb 0.2.6 → 0.2.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.
Files changed (146) hide show
  1. package/.github/workflows/publish.yml +9 -16
  2. package/docs/networking-dualstack-plan.md +75 -0
  3. package/docs/networking-layer-redesign.md +155 -0
  4. package/msb.mjs +11 -23
  5. package/package.json +2 -3
  6. package/rpc/{create_server.mjs → create_server.js} +2 -2
  7. package/rpc/{handlers.mjs → handlers.js} +5 -5
  8. package/rpc/routes/{index.mjs → index.js} +1 -1
  9. package/rpc/routes/{v1.mjs → v1.js} +1 -1
  10. package/rpc/{rpc_server.mjs → rpc_server.js} +1 -1
  11. package/rpc/rpc_services.js +4 -4
  12. package/src/config/config.js +137 -0
  13. package/src/config/env.js +61 -0
  14. package/src/core/network/Network.js +131 -72
  15. package/src/core/network/identity/NetworkWalletFactory.js +3 -4
  16. package/src/core/network/messaging/NetworkMessages.js +12 -11
  17. package/src/core/network/messaging/handlers/GetRequestHandler.js +5 -4
  18. package/src/core/network/messaging/handlers/ResponseHandler.js +4 -5
  19. package/src/core/network/messaging/handlers/RoleOperationHandler.js +17 -19
  20. package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +44 -38
  21. package/src/core/network/messaging/handlers/TransferOperationHandler.js +29 -25
  22. package/src/core/network/messaging/handlers/base/BaseOperationHandler.js +20 -21
  23. package/src/core/network/messaging/routes/NetworkMessageRouter.js +24 -20
  24. package/src/core/network/messaging/validators/AdminResponse.js +2 -2
  25. package/src/core/network/messaging/validators/CustomNodeResponse.js +2 -2
  26. package/src/core/network/messaging/validators/PartialBootstrapDeployment.js +3 -3
  27. package/src/core/network/messaging/validators/PartialRoleAccess.js +15 -12
  28. package/src/core/network/messaging/validators/PartialTransaction.js +9 -10
  29. package/src/core/network/messaging/validators/PartialTransfer.js +10 -7
  30. package/src/core/network/messaging/validators/ValidatorResponse.js +2 -2
  31. package/src/core/network/messaging/validators/base/BaseResponse.js +13 -5
  32. package/src/core/network/messaging/validators/base/PartialOperation.js +37 -21
  33. package/src/core/network/services/ConnectionManager.js +248 -62
  34. package/src/core/network/services/MessageOrchestrator.js +83 -0
  35. package/src/core/network/services/TransactionPoolService.js +9 -8
  36. package/src/core/network/services/ValidatorObserverService.js +95 -34
  37. package/src/core/state/State.js +136 -139
  38. package/src/core/state/utils/address.js +18 -16
  39. package/src/core/state/utils/adminEntry.js +17 -16
  40. package/src/core/state/utils/deploymentEntry.js +15 -15
  41. package/src/core/state/utils/transaction.js +3 -95
  42. package/src/index.js +153 -201
  43. package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +36 -32
  44. package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +39 -42
  45. package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +20 -20
  46. package/src/messages/partialStateMessages/PartialStateMessageOperations.js +29 -22
  47. package/src/utils/check.js +21 -17
  48. package/src/utils/cliCommands.js +11 -11
  49. package/src/utils/constants.js +2 -9
  50. package/src/utils/fileUtils.js +1 -4
  51. package/src/utils/helpers.js +9 -20
  52. package/src/utils/migrationUtils.js +2 -2
  53. package/src/utils/normalizers.js +10 -9
  54. package/tests/acceptance/v1/account/account.test.mjs +2 -2
  55. package/tests/acceptance/v1/balance/balance.test.mjs +1 -1
  56. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +11 -2
  57. package/tests/acceptance/v1/rpc.test.mjs +9 -9
  58. package/tests/acceptance/v1/tx/tx.test.mjs +4 -2
  59. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +7 -3
  60. package/tests/fixtures/check.fixtures.js +42 -42
  61. package/tests/fixtures/protobuf.fixtures.js +27 -26
  62. package/tests/helpers/StateNetworkFactory.js +3 -5
  63. package/tests/helpers/autobaseTestHelpers.js +48 -2
  64. package/tests/helpers/config.js +3 -0
  65. package/tests/helpers/setupApplyTests.js +89 -82
  66. package/tests/helpers/transactionPayloads.mjs +26 -12
  67. package/tests/integration/apply/addAdmin/addAdminBasic.test.js +10 -9
  68. package/tests/integration/apply/addAdmin/addAdminRecovery.test.js +20 -19
  69. package/tests/integration/apply/addIndexer.test.js +23 -21
  70. package/tests/integration/apply/addWhitelist.test.js +9 -9
  71. package/tests/integration/apply/addWriter.test.js +33 -32
  72. package/tests/integration/apply/banValidator.test.js +16 -9
  73. package/tests/integration/apply/postTx/invalidSubValues.test.js +4 -4
  74. package/tests/integration/apply/postTx/postTx.test.js +7 -33
  75. package/tests/integration/apply/removeIndexer.test.js +11 -7
  76. package/tests/integration/apply/removeWriter.test.js +20 -19
  77. package/tests/integration/apply/transfer.test.js +18 -16
  78. package/tests/unit/messageOperations/assembleAddIndexerMessage.test.js +2 -2
  79. package/tests/unit/messageOperations/assembleAddWriterMessage.test.js +2 -1
  80. package/tests/unit/messageOperations/assembleAdminMessage.test.js +9 -10
  81. package/tests/unit/messageOperations/assembleBanWriterMessage.test.js +3 -2
  82. package/tests/unit/messageOperations/assemblePostTransaction.test.js +25 -43
  83. package/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js +2 -2
  84. package/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js +2 -2
  85. package/tests/unit/messageOperations/assembleWhitelistMessages.test.js +5 -4
  86. package/tests/unit/messageOperations/commonsStateMessageOperationsTest.js +4 -3
  87. package/tests/unit/network/ConnectionManager.test.js +41 -70
  88. package/tests/unit/network/NetworkWalletFactory.test.js +14 -14
  89. package/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js +6 -6
  90. package/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js +8 -8
  91. package/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js +6 -5
  92. package/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js +24 -23
  93. package/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js +10 -16
  94. package/tests/unit/state/apply/addWriter/addWriterValidatorRewardScenario.js +2 -1
  95. package/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js +45 -41
  96. package/tests/unit/state/apply/adminRecovery/state.apply.adminRecovery.test.js +3 -7
  97. package/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js +17 -16
  98. package/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js +3 -4
  99. package/tests/unit/state/apply/balanceInitialization/nodeEntryBalanceUpdateFailureScenario.js +2 -1
  100. package/tests/unit/state/apply/banValidator/banValidatorBanAndReWhitelistScenario.js +2 -1
  101. package/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js +23 -25
  102. package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentDuplicateRegistrationScenario.js +2 -1
  103. package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js +19 -18
  104. package/tests/unit/state/apply/common/access-control/adminConsistencyMismatchScenario.js +5 -4
  105. package/tests/unit/state/apply/common/access-control/adminPublicKeyDecodeFailureScenario.js +4 -3
  106. package/tests/unit/state/apply/common/balances/base/requesterBalanceScenarioBase.js +2 -1
  107. package/tests/unit/state/apply/common/commonScenarioHelper.js +3 -4
  108. package/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js +2 -2
  109. package/tests/unit/state/apply/common/payload-structure/invalidHashValidationScenario.js +2 -2
  110. package/tests/unit/state/apply/common/requester/requesterNodeEntryBufferMissingScenario.js +2 -1
  111. package/tests/unit/state/apply/common/requester/requesterNodeEntryDecodeFailureScenario.js +2 -1
  112. package/tests/unit/state/apply/common/validatorConsistency/base/validatorConsistencyScenarioBase.js +2 -1
  113. package/tests/unit/state/apply/common/validatorEntryValidation/base/validatorEntryValidationScenarioBase.js +2 -1
  114. package/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js +11 -10
  115. package/tests/unit/state/apply/removeIndexer/removeIndexerScenarioHelpers.js +6 -5
  116. package/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js +6 -7
  117. package/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js +35 -34
  118. package/tests/unit/state/apply/transfer/transferScenarioHelpers.js +44 -43
  119. package/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js +26 -25
  120. package/tests/unit/state/apply/txOperation/txOperationTransferFeeGuardScenarioFactory.js +2 -1
  121. package/tests/unit/state/stateModule.test.js +0 -1
  122. package/tests/unit/state/stateTestUtils.js +7 -3
  123. package/tests/unit/state/utils/address.test.js +3 -3
  124. package/tests/unit/state/utils/adminEntry.test.js +10 -9
  125. package/tests/unit/utils/check/adminControlOperation.test.js +3 -3
  126. package/tests/unit/utils/check/balanceInitializationOperation.test.js +2 -2
  127. package/tests/unit/utils/check/bootstrapDeploymentOperation.test.js +2 -3
  128. package/tests/unit/utils/check/common.test.js +7 -6
  129. package/tests/unit/utils/check/coreAdminOperation.test.js +3 -3
  130. package/tests/unit/utils/check/roleAccessOperation.test.js +3 -2
  131. package/tests/unit/utils/check/transactionOperation.test.js +3 -3
  132. package/tests/unit/utils/check/transferOperation.test.js +3 -3
  133. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +2 -1
  134. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +2 -1
  135. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +7 -0
  136. package/tests/unit/utils/utils.test.js +0 -1
  137. package/src/core/state/utils/indexerEntry.js +0 -105
  138. package/src/utils/crypto.js +0 -11
  139. package/tests/unit/state/utils/indexerEntry.test.js +0 -83
  140. package/tests/unit/state/utils/transaction.test.js +0 -97
  141. package/tests/unit/utils/crypto/createHash.test.js +0 -15
  142. /package/rpc/{constants.mjs → constants.js} +0 -0
  143. /package/rpc/{cors.mjs → cors.js} +0 -0
  144. /package/rpc/utils/{confirmedParameter.mjs → confirmedParameter.js} +0 -0
  145. /package/rpc/utils/{helpers.mjs → helpers.js} +0 -0
  146. /package/rpc/utils/{url.mjs → url.js} +0 -0
@@ -1,13 +1,11 @@
1
1
  import b4a from 'b4a';
2
2
  import PeerWallet from 'trac-wallet';
3
-
4
3
  import Check from '../../../../../utils/check.js';
5
- import { bufferToAddress } from "../../../../state/utils/address.js";
6
- import { createMessage } from "../../../../../utils/buffer.js";
7
- import { OperationType, NETWORK_ID } from "../../../../../utils/constants.js";
8
- import { blake3Hash } from "../../../../../utils/crypto.js";
9
- import { bufferToBigInt } from "../../../../../utils/amountSerialization.js";
10
- import { FEE } from "../../../../state/utils/transaction.js";
4
+ import {bufferToAddress} from "../../../../state/utils/address.js";
5
+ import {createMessage} from "../../../../../utils/buffer.js";
6
+ import {OperationType} from "../../../../../utils/constants.js";
7
+ import {bufferToBigInt} from "../../../../../utils/amountSerialization.js";
8
+ import {FEE} from "../../../../state/utils/transaction.js";
11
9
  import * as operationsUtils from '../../../../../utils/operations.js';
12
10
 
13
11
  const MAX_AMOUNT = BigInt('0xffffffffffffffffffffffffffffffff');
@@ -17,12 +15,16 @@ const PUBLIC_KEY_LENGTH = 32;
17
15
  class PartialOperation {
18
16
  #state;
19
17
  #check;
18
+ #config
19
+ #wallet
20
20
 
21
- constructor(state) {
21
+ constructor(state, wallet, config) {
22
22
  this.#state = state;
23
- this.#check = new Check();
23
+ this.#config = config;
24
+ this.#check = new Check(this.#config);
24
25
  this.max_amount = MAX_AMOUNT;
25
26
  this.fee = FEE_BIGINT;
27
+ this.#wallet = wallet;
26
28
  }
27
29
 
28
30
  get state() {
@@ -33,7 +35,9 @@ class PartialOperation {
33
35
  return this.#check;
34
36
  }
35
37
 
36
- async validate(payload) { throw new Error("Method 'validate()' must be implemented."); }
38
+ async validate(payload) {
39
+ throw new Error("Method 'validate()' must be implemented.");
40
+ }
37
41
 
38
42
  isPayloadSchemaValid(payload) {
39
43
  if (!payload || !payload.type) {
@@ -65,7 +69,7 @@ class PartialOperation {
65
69
  }
66
70
 
67
71
  validateRequesterAddress(payload) {
68
- const incomingAddress = bufferToAddress(payload.address);
72
+ const incomingAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
69
73
  if (!incomingAddress) {
70
74
  throw new Error('Invalid requesting address in payload.');
71
75
  }
@@ -87,7 +91,7 @@ class PartialOperation {
87
91
  case OperationType.REMOVE_WRITER:
88
92
  case OperationType.ADMIN_RECOVERY:
89
93
  return [
90
- NETWORK_ID,
94
+ this.#config.networkId,
91
95
  operation.txv,
92
96
  operation.iw,
93
97
  operation.in,
@@ -95,7 +99,7 @@ class PartialOperation {
95
99
  ];
96
100
  case OperationType.BOOTSTRAP_DEPLOYMENT:
97
101
  return [
98
- NETWORK_ID,
102
+ this.#config.networkId,
99
103
  operation.txv,
100
104
  operation.bs,
101
105
  operation.ic,
@@ -104,7 +108,7 @@ class PartialOperation {
104
108
  ];
105
109
  case OperationType.TX:
106
110
  return [
107
- NETWORK_ID,
111
+ this.#config.networkId,
108
112
  operation.txv,
109
113
  operation.iw,
110
114
  operation.ch,
@@ -115,7 +119,7 @@ class PartialOperation {
115
119
  ];
116
120
  case OperationType.TRANSFER:
117
121
  return [
118
- NETWORK_ID,
122
+ this.#config.networkId,
119
123
  operation.txv,
120
124
  operation.to,
121
125
  operation.am,
@@ -131,12 +135,12 @@ class PartialOperation {
131
135
  const operationKey = operationsUtils.operationToPayload(payload.type);
132
136
  const operation = payload[operationKey];
133
137
 
134
- const incomingPublicKey = PeerWallet.decodeBech32mSafe(bufferToAddress(payload.address));
138
+ const incomingPublicKey = PeerWallet.decodeBech32mSafe(bufferToAddress(payload.address, this.#config.addressPrefix));
135
139
  const incomingSignature = operation.is;
136
140
  const messageComponents = this.#getMessageComponents(payload);
137
141
 
138
142
  const message = createMessage(...messageComponents);
139
- const messageHash = await blake3Hash(message);
143
+ const messageHash = await PeerWallet.blake3(message);
140
144
  const payloadHash = operation.tx;
141
145
  if (!b4a.equals(payloadHash, messageHash)) {
142
146
  throw new Error('Regenerated transaction does not match incoming transaction in payload.');
@@ -172,16 +176,16 @@ class PartialOperation {
172
176
  isOperationNotCompleted(payload) {
173
177
  const operationKey = operationsUtils.operationToPayload(payload.type);
174
178
  const operation = payload[operationKey];
175
- const { va, vn, vs } = operation;
179
+ const {va, vn, vs} = operation;
176
180
 
177
- const condition = !!(va === undefined && vn === undefined && vs === undefined);
181
+ const condition = va === undefined && vn === undefined && vs === undefined
178
182
  if (!condition) {
179
183
  throw new Error('Transfer operation must not be completed already (va, vn, vs must be undefined).');
180
184
  }
181
185
  }
182
186
 
183
187
  async validateRequesterBalance(payload, signed = false) {
184
- const requesterAddress = bufferToAddress(payload.address);
188
+ const requesterAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
185
189
  let requesterEntry;
186
190
  if (signed) {
187
191
  requesterEntry = await this.state.getNodeEntry(requesterAddress);
@@ -204,11 +208,23 @@ class PartialOperation {
204
208
  const operationKey = operationsUtils.operationToPayload(payload.type);
205
209
  const operation = payload[operationKey];
206
210
  const bs = operation.bs;
207
- if (b4a.equals(this.state.bootstrap, bs)) {
211
+ if (b4a.equals(this.#config.bootstrap, bs)) {
208
212
  throw new Error(`External bootstrap is the same as MSB bootstrap: ${bs.toString('hex')}`);
209
213
  }
210
214
  }
211
215
 
216
+ /*
217
+ * Guard against self-validation (RPC/orchestrator loop): a validator may receive its own submitted tx for validation.
218
+ * Even if unlikely, this must be rejected to avoid incorrect failures/punishments.
219
+ * Flow: Validator -> submits tx with tap-wallet -> RPC-> Validator -validates tx-> REJECT (self-validation)
220
+ */
221
+ validateNoSelfValidation(payload) {
222
+ const requesterAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
223
+ if (this.#wallet.address === requesterAddress) {
224
+ throw new Error('Requester address cannot be the same as the validator wallet address.');
225
+ }
226
+ }
227
+
212
228
  }
213
229
 
214
230
  export default PartialOperation;
@@ -1,121 +1,307 @@
1
- import { MAX_VALIDATORS, MAX_REQUEST_COUNT } from "../../../utils/constants.js"
2
1
  import b4a from 'b4a'
3
2
  import PeerWallet from "trac-wallet"
4
- import { TRAC_NETWORK_MSB_MAINNET_PREFIX } from 'trac-wallet/constants.js';
3
+
4
+
5
+ // -- Debug Mode --
6
+ // TODO: Implement a better debug system in the future. This is just temporary.
7
+ const DEBUG = false;
8
+ const debugLog = (...args) => {
9
+ if (DEBUG) {
10
+ console.log('DEBUG [ConnectionManager] ==> ', ...args);
11
+ }
12
+ };
5
13
 
6
14
  class ConnectionManager {
7
15
  #validators
8
- #validatorsIndex
9
- #currentValidatorIndex
10
- #requestCount
11
16
  #maxValidators
17
+ #config
12
18
 
13
- constructor({ maxValidators }) {
14
- this.#validators = {}
15
- this.#validatorsIndex = []
16
- this.#currentValidatorIndex = 0
17
- this.#requestCount = 0
18
- this.#maxValidators = maxValidators || MAX_VALIDATORS
19
+ // Note: #validators is using publicKey (Buffer) as key
20
+ // As Buffers are objects, we will rely on internal conversions done by JS to compare them.
21
+ // It would be better to handle these conversions manually by using hex strings as keys to avoid issues
22
+ /**
23
+ * @param {object} config
24
+ **/
25
+ constructor(config) {
26
+ this.#validators = new Map();
27
+ this.#config = config
28
+ this.#maxValidators = config.maxValidators
19
29
  }
20
30
 
21
- send(message, retries = 3) {
22
- if (this.#requestCount >= MAX_REQUEST_COUNT) {
23
- this.#requestCount = 0
24
- this.#updateNext()
31
+ /**
32
+ * Sends a message to a single randomly selected connected validator.
33
+ * Returns the public key (buffer) of the validator used, or throws
34
+ * if the specified validator is unavailable.
35
+ * @param {Object} message - The message to send to the validator
36
+ * @returns {String} - The public key of the validator used
37
+ */
38
+ send(message) {
39
+ const connectedValidators = this.connectedValidators();
40
+
41
+ if (connectedValidators.length === 0) {
42
+ throw new Error('ConnectionManager: no connected validators available to send message');
25
43
  }
26
- this.#requestCount++
44
+
45
+ const target = this.pickRandomValidator(connectedValidators);
46
+ const entry = this.#validators.get(target);
47
+ if (!entry || !entry.connection || !entry.connection.messenger) return null;
27
48
 
28
49
  try {
29
- this.#getConnection().messenger.send(message)
30
- } catch (e) { // Some retrying mechanism before reacting to close
31
- if (retries > 0) {
32
- this.rotate()
33
- this.send(message, retries - 1)
34
- }
50
+ entry.connection.messenger.send(message);
51
+ entry.sent = (entry.sent || 0) + 1;
52
+ } catch (e) {
53
+ // Swallow individual send errors.
35
54
  }
55
+
56
+ return target;
36
57
  }
37
58
 
59
+ /**
60
+ * Sends a message through a specific validator without increasing sent messages count.
61
+ * @param {Object} message - The message to send to the validator
62
+ * @param {String | Buffer} publicKey - A validator public key hex string to be fetched from the pool.
63
+ * @returns {Boolean} True if the message was sent, false otherwise.
64
+ */
65
+ sendSingleMessage(message, publicKey) {
66
+ let publicKeyHex = this.#toHexString(publicKey);
67
+ if (!this.exists(publicKeyHex) || !this.connected(publicKeyHex)) return false; // Fail silently
68
+
69
+ const validator = this.#validators.get(publicKeyHex);
70
+ if (!validator || !validator.connection || !validator.connection.messenger) return false;
71
+ try {
72
+ validator.connection.messenger.send(message);
73
+ } catch (e) {
74
+ // Swallow individual send errors.
75
+ }
76
+ return true; // TODO: Implement better success/failure reporting
77
+ }
78
+
79
+ /**
80
+ * Creates a blank entry for a validator in the pool without a connection.
81
+ * @param {String | Buffer} publicKey - The public key hex string of the validator to whitelist
82
+ */
83
+ // TODO: Deprecated/Unused - remove if not needed
38
84
  whiteList(publicKey) {
39
- this.#validators[publicKey] = null
85
+ let publicKeyHex = this.#toHexString(publicKey);
86
+ this.#validators.set(publicKeyHex, { connection: null, sent: 0 });
40
87
  }
41
88
 
89
+ /**
90
+ * Adds a validator to the pool if not already present.
91
+ * @param {String | Buffer} publicKey - The public key hex string of the validator to add
92
+ * @param {Object} connection - The connection object associated with the validator
93
+ * @returns {Boolean} - Returns true if the validator was added or updated, false otherwise
94
+ */
42
95
  addValidator(publicKey, connection) {
43
- if (!this.exists(publicKey) && !this.maxConnections()) {
44
- return this.#append(publicKey, connection)
45
- } else if (!this.connected(publicKey)) {
46
- return this.#update(publicKey, connection)
96
+ let publicKeyHex = this.#toHexString(publicKey);
97
+ if (this.maxConnectionsReached()) {
98
+ debugLog(`addValidator: max connections reached.`);
99
+ return false;
47
100
  }
48
-
49
- return false
101
+ debugLog(`addValidator: adding validator ${this.#toAddress(publicKeyHex)}`);
102
+ if (!this.exists(publicKeyHex)) {
103
+ debugLog(`addValidator: appending validator ${this.#toAddress(publicKeyHex)}`);
104
+ this.#append(publicKeyHex, connection);
105
+ return true;
106
+ } else if (!this.connected(publicKeyHex)) {
107
+ debugLog(`addValidator: updating validator ${this.#toAddress(publicKeyHex)}`);
108
+ this.#update(publicKeyHex, connection);
109
+ return true;
110
+ }
111
+ debugLog(`addValidator: didn't add validator ${this.#toAddress(publicKeyHex)}`);
112
+ return false; // TODO: Implement better success/failure reporting
50
113
  }
51
114
 
115
+ /**
116
+ * Removes a validator from the pool.
117
+ * @param {String | Buffer} publicKey - The public key hex string of the validator to remove
118
+ */
52
119
  remove(publicKey) {
53
- const index = this.#validatorsIndex.findIndex(current => b4a.equals(publicKey, current));
54
- if (index !== -1) {
55
- this.#validatorsIndex.splice(index, 1);
56
- delete this.#validators[publicKey]
120
+ debugLog(`remove: removing validator ${this.#toAddress(publicKey)}`);
121
+ const publicKeyHex = this.#toHexString(publicKey);
122
+ if (this.exists(publicKeyHex)) {
123
+ // Close the connection socket
124
+ const entry = this.#validators.get(publicKeyHex);
125
+ if (entry && entry.connection && typeof entry.connection.end === 'function') {
126
+ try {
127
+ entry.connection.end();
128
+ } catch (e) {
129
+ // Ignore errors on connection end
130
+ // TODO: Consider logging these errors here in verbose mode
131
+ }
132
+ }
133
+ debugLog(`remove: removing validator from map: ${this.#toAddress(publicKeyHex)}. Map size before removal: ${this.#validators.size}.`);
134
+ this.#validators.delete(publicKeyHex);
135
+ debugLog(`remove: validator removed successfully. Map size is now ${this.#validators.size}.`);
57
136
  }
58
137
  }
59
138
 
60
- maxConnections() {
139
+ /**
140
+ * Checks if the maximum number of connections has been reached.
141
+ * @returns {Boolean} - Returns true if the maximum number of connections has been reached, false otherwise.
142
+ */
143
+ // Note: this function name is a bit misleading. It checks if we have reached max connections and returns boolean
144
+ // The name leads to think it returns the number of max connections
145
+ maxConnectionsReached() {
61
146
  return this.connectionCount() >= this.#maxValidators
62
147
  }
63
148
 
149
+ /**
150
+ * Gets a list of all currently connected validators' public keys.
151
+ * @returns {Array} - An array of public key hex strings of connected validators
152
+ */
153
+ connectedValidators() {
154
+ return Array.from(this.#validators.keys()).filter(pk => this.connected(pk));
155
+ }
156
+
157
+ /**
158
+ * Gets the current number of connected validators.
159
+ * @returns {Number} - The count of connected validators
160
+ */
64
161
  connectionCount() {
65
- return this.#validatorsIndex.filter(_ => this.connected(_)).length
162
+ return this.connectedValidators().length;
66
163
  }
67
164
 
165
+ /**
166
+ * Checks if a validator is currently connected.
167
+ * @param {String | Buffer} publicKey - The public key hex string of the validator to check
168
+ * @returns {Boolean} - Returns true if the validator is connected, false otherwise
169
+ */
68
170
  connected(publicKey) {
69
- return this.exists(publicKey) && this.#validators[publicKey]?.connected
171
+ const publicKeyHex = this.#toHexString(publicKey);
172
+ return this.exists(publicKeyHex) && this.#validators.get(publicKeyHex).connection !== null;
70
173
  }
71
174
 
72
- rotate() {
73
- this.#updateNext()
74
- this.#requestCount = 0
175
+ /**
176
+ * Checks if a validator exists in the pool.
177
+ * @param {String | Buffer} publicKey - The public key hex string of the validator to check
178
+ * @returns {Boolean} - Returns true if the validator exists, false otherwise
179
+ */
180
+ exists(publicKey) {
181
+ const publicKeyHex = this.#toHexString(publicKey);
182
+ return this.#validators.has(publicKeyHex);
75
183
  }
76
184
 
77
- exists(publicKey) {
78
- return !!this.#validators[publicKey]
185
+ /**
186
+ * Gets the number of messages sent through a validator.
187
+ * @param {String | Buffer} publicKey - The public key hex string of the validator
188
+ * @returns {Number} - The count of messages sent
189
+ */
190
+ getSentCount(publicKey) {
191
+ const publicKeyHex = this.#toHexString(publicKey);
192
+ const entry = this.#validators.get(publicKeyHex);
193
+ return entry ? (entry.sent || 0) : 0;
194
+ }
195
+
196
+ /**
197
+ * Increments the count of messages sent through a validator.
198
+ * @param {String | Buffer} publicKey - The public key hex string of the validator
199
+ */
200
+ incrementSentCount(publicKey) {
201
+ const publicKeyHex = this.#toHexString(publicKey);
202
+ const entry = this.#validators.get(publicKeyHex);
203
+ if (entry) {
204
+ entry.sent = (entry.sent || 0) + 1;
205
+ }
79
206
  }
80
207
 
81
208
  prettyPrint() {
82
209
  console.log('Connection count: ', this.connectionCount())
83
- console.log('Current connection: ', this.#currentValidator())
84
- console.log('Validators: ', this.#validatorsIndex.map(val => PeerWallet.encodeBech32m(TRAC_NETWORK_MSB_MAINNET_PREFIX, val)))
210
+ console.log('Validator map keys count: ', this.#validators.size)
211
+ console.log('Validator map keys: ', Array.from(this.#validators.keys()).map(val => this.#toAddress(val)).join(', '))
212
+ }
213
+
214
+ // Note 1: This method shuffles the whole array (in practice, probably around 50 elements)
215
+ // just to fetch a small subset of it (most times, 1 element).
216
+ // There are more efficient ways to pick a small subset of validators. Consider optimizing.
217
+ // Note 2: This method is unused now, but will be kept here for future reference
218
+ // TODO: Deprecated/Unused - remove if not needed
219
+ pickRandomSubset(validators, maxTargets) {
220
+ const copy = validators.slice();
221
+ const count = Math.min(maxTargets, copy.length);
222
+
223
+ for (let i = copy.length - 1; i > 0; i--) {
224
+ const j = Math.floor(Math.random() * (i + 1));
225
+ [copy[i], copy[j]] = [copy[j], copy[i]];
226
+ }
227
+
228
+ return copy.slice(0, count);
85
229
  }
86
230
 
87
- #currentValidator() {
88
- return this.#validatorsIndex[this.#currentValidatorIndex]
231
+ /**
232
+ * Picks a random validator from the given array of validator public keys.
233
+ * @param {String[]} validatorPubKeys - An array of validator public key hex strings
234
+ * @returns {String|null} - A randomly selected validator public key
235
+ */
236
+ pickRandomValidator(validatorPubKeys) {
237
+ if (validatorPubKeys.length === 0) {
238
+ return null;
239
+ }
240
+ const index = Math.floor(Math.random() * validatorPubKeys.length);
241
+ return validatorPubKeys[index];
89
242
  }
90
243
 
91
- #getConnection() {
92
- return this.#validators[this.#currentValidator()]
244
+ /**
245
+ * Picks a random connected validator.
246
+ * @returns {String|null} - A randomly selected connected validator public key, or null if none are connected
247
+ */
248
+ pickRandomConnectedValidator() {
249
+ const connected = this.connectedValidators();
250
+ if (connected.length === 0) return null;
251
+ return this.pickRandomValidator(connected);
93
252
  }
94
253
 
254
+ /**
255
+ * Appends a new validator connection.
256
+ * @param {String|Buffer} publicKey - The public key hex string of the validator
257
+ * @param {Object} connection - The connection object
258
+ */
95
259
  #append(publicKey, connection) {
96
- this.#validatorsIndex.push(publicKey)
97
- this.#validators[publicKey] = connection
98
-
260
+ debugLog(`#append: appending validator ${this.#toAddress(publicKey)}`);
261
+ const publicKeyHex = this.#toHexString(publicKey);
262
+ if (this.#validators.has(publicKeyHex)) {
263
+ // This should never happen, but just in case, we log it
264
+ debugLog(`#append: tried to append existing validator: ${this.#toAddress(publicKey)}`);
265
+ return;
266
+ }
267
+ this.#validators.set(publicKeyHex, { connection, sent: 0 });
99
268
  connection.on('close', () => {
100
- if (this.#isRemoteEqual(publicKey)) {
101
- this.remove(publicKey)
102
- }
103
- })
269
+ debugLog(`#append: connection closing for validator ${this.#toAddress(publicKey)}`);
270
+ this.remove(publicKeyHex);
271
+ debugLog(`#append: connection closed for validator ${this.#toAddress(publicKey)}`);
272
+ });
104
273
  }
105
274
 
275
+ /**
276
+ * Updates an existing validator connection or adds it if not present.
277
+ * @param {String|Buffer} publicKey - The public key hex string of the validator
278
+ * @param {Object} connection - The connection object
279
+ */
106
280
  #update(publicKey, connection) {
107
- this.#validators[publicKey] = connection
281
+ // Note: Is there a good reason for the function 'update' to exist separately from 'append'?
282
+ // It seems that both could be merged into a single function that either adds or updates the entry.
283
+ // It would be preferable to keep them separated though, but we would need to review all usages to ensure correctness.
284
+ // Also, we should remove the 'else' branch below if we decide to keep 'update' and 'append' separated.
285
+ const publicKeyHex = this.#toHexString(publicKey);
286
+ debugLog(`#update: updating validator ${this.#toAddress(publicKey)}`);
287
+ if (this.#validators.has(publicKeyHex)) {
288
+ this.#validators.get(publicKeyHex).connection = connection;
289
+ } else {
290
+ this.#validators.set(publicKeyHex, { connection, sent: 0 });
291
+ }
108
292
  }
109
293
 
110
- #updateNext() {
111
- const next = this.#currentValidatorIndex + 1
112
- this.#currentValidatorIndex = next < this.#validatorsIndex.length ? next : 0
294
+ #toAddress(publicKey) {
295
+ const keyHex = b4a.isBuffer(publicKey) ? publicKey : b4a.from(publicKey, 'hex');
296
+ return PeerWallet.encodeBech32m(
297
+ this.#config.addressPrefix,
298
+ keyHex
299
+ );
113
300
  }
114
301
 
115
-
116
- #isRemoteEqual(publicKey) {
117
- return !!publicKey && !!this.#validators[publicKey]?.remotePublicKey && b4a.equals(this.#validators[publicKey]?.remotePublicKey, publicKey)
302
+ #toHexString(publicKey) {
303
+ return b4a.isBuffer(publicKey) ? publicKey.toString('hex') : publicKey;
118
304
  }
119
305
  }
120
306
 
121
- export default ConnectionManager;
307
+ export default ConnectionManager;
@@ -0,0 +1,83 @@
1
+ import { sleep } from '../../../utils/helpers.js';
2
+ import { operationToPayload } from '../../../utils/operations.js';
3
+ /**
4
+ * MessageOrchestrator coordinates message submission, retry, and validator management.
5
+ * It works with ConnectionManager and ledger state to ensure reliable message delivery.
6
+ */
7
+ class MessageOrchestrator {
8
+ #config;
9
+ /**
10
+ * Attempts to send a message to validators with retries and state checks.
11
+ * @param {ConnectionManager} connectionManager - The connection manager instance
12
+ * @param {object} state - The state to look for the message outcome
13
+ * @param {object} config - Configuration options:
14
+ */
15
+ constructor(connectionManager, state, config) {
16
+ this.connectionManager = connectionManager;
17
+ this.state = state;
18
+ this.#config = config;
19
+ }
20
+
21
+ /**
22
+ * Sends a message to a single randomly selected connected validator.
23
+ * @param {object} message - The message object to be sent
24
+ * @returns {Promise<boolean>} - true if successful, false otherwise
25
+ */
26
+ async send(message) {
27
+ const startTime = Date.now();
28
+ while (Date.now() - startTime < this.#config.messageValidatorResponseTimeout) {
29
+ const validator = this.connectionManager.pickRandomConnectedValidator();
30
+ if (!validator) return false;
31
+
32
+ const success = await this.#attemptSendMessage(validator, message);
33
+ if (success) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ async #attemptSendMessage(validator, message) {
41
+ let attempts = 0;
42
+ const deductedTxType = operationToPayload(message.type);
43
+ while (attempts <= this.#config.maxRetries) {
44
+ this.connectionManager.sendSingleMessage(message, validator);
45
+
46
+ const appeared = await this.waitForUnsignedState(message[deductedTxType].tx, this.#config.messageValidatorRetryDelay);
47
+ if (appeared) {
48
+ this.incrementSentCount(validator);
49
+ if (this.shouldRemove(validator)) {
50
+ this.connectionManager.remove(validator);
51
+ }
52
+ return true;
53
+ }
54
+ attempts++;
55
+ }
56
+
57
+ // If all retries fail, remove validator from pool
58
+ this.connectionManager.remove(validator);
59
+ return false;
60
+ }
61
+
62
+ async waitForUnsignedState(txHash, timeout) {
63
+ // Polls state for the transaction hash for up to timeout ms
64
+ const start = Date.now();
65
+ let entry = null;
66
+ while (Date.now() - start < timeout) {
67
+ await sleep(200);
68
+ entry = await this.state.get(txHash)
69
+ if (entry) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ incrementSentCount(validatorPubKey) {
75
+ this.connectionManager.incrementSentCount(validatorPubKey);
76
+ }
77
+
78
+ shouldRemove(validatorPubKey) {
79
+ return this.connectionManager.getSentCount(validatorPubKey) >= this.#config.messageThreshold;
80
+ }
81
+ }
82
+
83
+ export default MessageOrchestrator;
@@ -5,14 +5,19 @@ import Scheduler from '../../../utils/Scheduler.js';
5
5
  class TransactionPoolService {
6
6
  #state;
7
7
  #address;
8
- #options;
8
+ #config;
9
9
  #tx_pool = [];
10
10
  #scheduler = null;
11
11
 
12
- constructor(state, address, options = {}) {
12
+ /**
13
+ * @param {State} state
14
+ * @param {string} address
15
+ * @param {object} config
16
+ **/
17
+ constructor(state, address, config) {
13
18
  this.#state = state;
14
19
  this.#address = address;
15
- this.#options = options;
20
+ this.#config = config;
16
21
  }
17
22
 
18
23
  get tx_pool() {
@@ -27,12 +32,8 @@ class TransactionPoolService {
27
32
  return this.#address;
28
33
  }
29
34
 
30
- get options() {
31
- return this.#options;
32
- }
33
-
34
35
  async start() {
35
- if (!this.options.enable_wallet) {
36
+ if (!this.#config.enableWallet) {
36
37
  console.info('TransactionPoolService can not start. Wallet is not enabled');
37
38
  return;
38
39
  }