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.
- package/.github/workflows/publish.yml +9 -16
- package/docs/networking-dualstack-plan.md +75 -0
- package/docs/networking-layer-redesign.md +155 -0
- package/msb.mjs +11 -23
- package/package.json +2 -3
- package/rpc/{create_server.mjs → create_server.js} +2 -2
- package/rpc/{handlers.mjs → handlers.js} +5 -5
- package/rpc/routes/{index.mjs → index.js} +1 -1
- package/rpc/routes/{v1.mjs → v1.js} +1 -1
- package/rpc/{rpc_server.mjs → rpc_server.js} +1 -1
- package/rpc/rpc_services.js +4 -4
- package/src/config/config.js +137 -0
- package/src/config/env.js +61 -0
- package/src/core/network/Network.js +131 -72
- package/src/core/network/identity/NetworkWalletFactory.js +3 -4
- package/src/core/network/messaging/NetworkMessages.js +12 -11
- package/src/core/network/messaging/handlers/GetRequestHandler.js +5 -4
- package/src/core/network/messaging/handlers/ResponseHandler.js +4 -5
- package/src/core/network/messaging/handlers/RoleOperationHandler.js +17 -19
- package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +44 -38
- package/src/core/network/messaging/handlers/TransferOperationHandler.js +29 -25
- package/src/core/network/messaging/handlers/base/BaseOperationHandler.js +20 -21
- package/src/core/network/messaging/routes/NetworkMessageRouter.js +24 -20
- package/src/core/network/messaging/validators/AdminResponse.js +2 -2
- package/src/core/network/messaging/validators/CustomNodeResponse.js +2 -2
- package/src/core/network/messaging/validators/PartialBootstrapDeployment.js +3 -3
- package/src/core/network/messaging/validators/PartialRoleAccess.js +15 -12
- package/src/core/network/messaging/validators/PartialTransaction.js +9 -10
- package/src/core/network/messaging/validators/PartialTransfer.js +10 -7
- package/src/core/network/messaging/validators/ValidatorResponse.js +2 -2
- package/src/core/network/messaging/validators/base/BaseResponse.js +13 -5
- package/src/core/network/messaging/validators/base/PartialOperation.js +37 -21
- package/src/core/network/services/ConnectionManager.js +248 -62
- package/src/core/network/services/MessageOrchestrator.js +83 -0
- package/src/core/network/services/TransactionPoolService.js +9 -8
- package/src/core/network/services/ValidatorObserverService.js +95 -34
- package/src/core/state/State.js +136 -139
- package/src/core/state/utils/address.js +18 -16
- package/src/core/state/utils/adminEntry.js +17 -16
- package/src/core/state/utils/deploymentEntry.js +15 -15
- package/src/core/state/utils/transaction.js +3 -95
- package/src/index.js +153 -201
- package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +36 -32
- package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +39 -42
- package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +20 -20
- package/src/messages/partialStateMessages/PartialStateMessageOperations.js +29 -22
- package/src/utils/check.js +21 -17
- package/src/utils/cliCommands.js +11 -11
- package/src/utils/constants.js +2 -9
- package/src/utils/fileUtils.js +1 -4
- package/src/utils/helpers.js +9 -20
- package/src/utils/migrationUtils.js +2 -2
- package/src/utils/normalizers.js +10 -9
- package/tests/acceptance/v1/account/account.test.mjs +2 -2
- package/tests/acceptance/v1/balance/balance.test.mjs +1 -1
- package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +11 -2
- package/tests/acceptance/v1/rpc.test.mjs +9 -9
- package/tests/acceptance/v1/tx/tx.test.mjs +4 -2
- package/tests/acceptance/v1/tx-details/tx-details.test.mjs +7 -3
- package/tests/fixtures/check.fixtures.js +42 -42
- package/tests/fixtures/protobuf.fixtures.js +27 -26
- package/tests/helpers/StateNetworkFactory.js +3 -5
- package/tests/helpers/autobaseTestHelpers.js +48 -2
- package/tests/helpers/config.js +3 -0
- package/tests/helpers/setupApplyTests.js +89 -82
- package/tests/helpers/transactionPayloads.mjs +26 -12
- package/tests/integration/apply/addAdmin/addAdminBasic.test.js +10 -9
- package/tests/integration/apply/addAdmin/addAdminRecovery.test.js +20 -19
- package/tests/integration/apply/addIndexer.test.js +23 -21
- package/tests/integration/apply/addWhitelist.test.js +9 -9
- package/tests/integration/apply/addWriter.test.js +33 -32
- package/tests/integration/apply/banValidator.test.js +16 -9
- package/tests/integration/apply/postTx/invalidSubValues.test.js +4 -4
- package/tests/integration/apply/postTx/postTx.test.js +7 -33
- package/tests/integration/apply/removeIndexer.test.js +11 -7
- package/tests/integration/apply/removeWriter.test.js +20 -19
- package/tests/integration/apply/transfer.test.js +18 -16
- package/tests/unit/messageOperations/assembleAddIndexerMessage.test.js +2 -2
- package/tests/unit/messageOperations/assembleAddWriterMessage.test.js +2 -1
- package/tests/unit/messageOperations/assembleAdminMessage.test.js +9 -10
- package/tests/unit/messageOperations/assembleBanWriterMessage.test.js +3 -2
- package/tests/unit/messageOperations/assemblePostTransaction.test.js +25 -43
- package/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js +2 -2
- package/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js +2 -2
- package/tests/unit/messageOperations/assembleWhitelistMessages.test.js +5 -4
- package/tests/unit/messageOperations/commonsStateMessageOperationsTest.js +4 -3
- package/tests/unit/network/ConnectionManager.test.js +41 -70
- package/tests/unit/network/NetworkWalletFactory.test.js +14 -14
- package/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js +6 -6
- package/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js +8 -8
- package/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js +6 -5
- package/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js +24 -23
- package/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js +10 -16
- package/tests/unit/state/apply/addWriter/addWriterValidatorRewardScenario.js +2 -1
- package/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js +45 -41
- package/tests/unit/state/apply/adminRecovery/state.apply.adminRecovery.test.js +3 -7
- package/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js +17 -16
- package/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js +3 -4
- package/tests/unit/state/apply/balanceInitialization/nodeEntryBalanceUpdateFailureScenario.js +2 -1
- package/tests/unit/state/apply/banValidator/banValidatorBanAndReWhitelistScenario.js +2 -1
- package/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js +23 -25
- package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentDuplicateRegistrationScenario.js +2 -1
- package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js +19 -18
- package/tests/unit/state/apply/common/access-control/adminConsistencyMismatchScenario.js +5 -4
- package/tests/unit/state/apply/common/access-control/adminPublicKeyDecodeFailureScenario.js +4 -3
- package/tests/unit/state/apply/common/balances/base/requesterBalanceScenarioBase.js +2 -1
- package/tests/unit/state/apply/common/commonScenarioHelper.js +3 -4
- package/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js +2 -2
- package/tests/unit/state/apply/common/payload-structure/invalidHashValidationScenario.js +2 -2
- package/tests/unit/state/apply/common/requester/requesterNodeEntryBufferMissingScenario.js +2 -1
- package/tests/unit/state/apply/common/requester/requesterNodeEntryDecodeFailureScenario.js +2 -1
- package/tests/unit/state/apply/common/validatorConsistency/base/validatorConsistencyScenarioBase.js +2 -1
- package/tests/unit/state/apply/common/validatorEntryValidation/base/validatorEntryValidationScenarioBase.js +2 -1
- package/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js +11 -10
- package/tests/unit/state/apply/removeIndexer/removeIndexerScenarioHelpers.js +6 -5
- package/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js +6 -7
- package/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js +35 -34
- package/tests/unit/state/apply/transfer/transferScenarioHelpers.js +44 -43
- package/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js +26 -25
- package/tests/unit/state/apply/txOperation/txOperationTransferFeeGuardScenarioFactory.js +2 -1
- package/tests/unit/state/stateModule.test.js +0 -1
- package/tests/unit/state/stateTestUtils.js +7 -3
- package/tests/unit/state/utils/address.test.js +3 -3
- package/tests/unit/state/utils/adminEntry.test.js +10 -9
- package/tests/unit/utils/check/adminControlOperation.test.js +3 -3
- package/tests/unit/utils/check/balanceInitializationOperation.test.js +2 -2
- package/tests/unit/utils/check/bootstrapDeploymentOperation.test.js +2 -3
- package/tests/unit/utils/check/common.test.js +7 -6
- package/tests/unit/utils/check/coreAdminOperation.test.js +3 -3
- package/tests/unit/utils/check/roleAccessOperation.test.js +3 -2
- package/tests/unit/utils/check/transactionOperation.test.js +3 -3
- package/tests/unit/utils/check/transferOperation.test.js +3 -3
- package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +2 -1
- package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +2 -1
- package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +7 -0
- package/tests/unit/utils/utils.test.js +0 -1
- package/src/core/state/utils/indexerEntry.js +0 -105
- package/src/utils/crypto.js +0 -11
- package/tests/unit/state/utils/indexerEntry.test.js +0 -83
- package/tests/unit/state/utils/transaction.test.js +0 -97
- package/tests/unit/utils/crypto/createHash.test.js +0 -15
- /package/rpc/{constants.mjs → constants.js} +0 -0
- /package/rpc/{cors.mjs → cors.js} +0 -0
- /package/rpc/utils/{confirmedParameter.mjs → confirmedParameter.js} +0 -0
- /package/rpc/utils/{helpers.mjs → helpers.js} +0 -0
- /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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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.#
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
179
|
+
const {va, vn, vs} = operation;
|
|
176
180
|
|
|
177
|
-
const condition =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.#
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
96
|
+
let publicKeyHex = this.#toHexString(publicKey);
|
|
97
|
+
if (this.maxConnectionsReached()) {
|
|
98
|
+
debugLog(`addValidator: max connections reached.`);
|
|
99
|
+
return false;
|
|
47
100
|
}
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
171
|
+
const publicKeyHex = this.#toHexString(publicKey);
|
|
172
|
+
return this.exists(publicKeyHex) && this.#validators.get(publicKeyHex).connection !== null;
|
|
70
173
|
}
|
|
71
174
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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('
|
|
84
|
-
console.log('
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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.#
|
|
97
|
-
this.#
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
111
|
-
const
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
8
|
+
#config;
|
|
9
9
|
#tx_pool = [];
|
|
10
10
|
#scheduler = null;
|
|
11
11
|
|
|
12
|
-
|
|
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.#
|
|
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.
|
|
36
|
+
if (!this.#config.enableWallet) {
|
|
36
37
|
console.info('TransactionPoolService can not start. Wallet is not enabled');
|
|
37
38
|
return;
|
|
38
39
|
}
|