trac-msb 0.2.12 → 0.2.13

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 (114) hide show
  1. package/package.json +9 -4
  2. package/proto/network/v1/enums/message_type.proto +16 -0
  3. package/proto/network/v1/enums/result_code.proto +84 -0
  4. package/proto/network/v1/messages/broadcast_transaction_request.proto +9 -0
  5. package/proto/network/v1/messages/broadcast_transaction_response.proto +13 -0
  6. package/proto/network/v1/messages/liveness_request.proto +8 -0
  7. package/proto/network/v1/messages/liveness_response.proto +11 -0
  8. package/proto/network/v1/network_message.proto +22 -0
  9. package/rpc/rpc_services.js +22 -4
  10. package/scripts/generate-protobufs.js +37 -12
  11. package/src/config/config.js +26 -5
  12. package/src/config/env.js +25 -11
  13. package/src/core/network/Network.js +73 -36
  14. package/src/core/network/protocols/LegacyProtocol.js +21 -11
  15. package/src/core/network/protocols/NetworkMessages.js +38 -17
  16. package/src/core/network/protocols/ProtocolInterface.js +14 -2
  17. package/src/core/network/protocols/ProtocolSession.js +144 -17
  18. package/src/core/network/protocols/V1Protocol.js +37 -18
  19. package/src/core/network/protocols/connectionPolicies.js +88 -0
  20. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +25 -19
  21. package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +23 -12
  22. package/src/core/network/protocols/legacy/handlers/{GetRequestHandler.js → LegacyGetRequestHandler.js} +6 -6
  23. package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +23 -0
  24. package/src/core/network/protocols/{shared/handlers/RoleOperationHandler.js → legacy/handlers/LegacyRoleOperationHandler.js} +18 -11
  25. package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +28 -17
  26. package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +17 -11
  27. package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +27 -0
  28. package/src/core/network/protocols/shared/validators/{PartialBootstrapDeployment.js → PartialBootstrapDeploymentValidator.js} +9 -4
  29. package/src/core/network/protocols/shared/validators/{base/PartialOperation.js → PartialOperationValidator.js} +47 -25
  30. package/src/core/network/protocols/shared/validators/{PartialRoleAccess.js → PartialRoleAccessValidator.js} +51 -17
  31. package/src/core/network/protocols/shared/validators/{PartialTransaction.js → PartialTransactionValidator.js} +21 -7
  32. package/src/core/network/protocols/shared/validators/{PartialTransfer.js → PartialTransferValidator.js} +26 -9
  33. package/src/core/network/protocols/v1/NetworkMessageRouter.js +91 -7
  34. package/src/core/network/protocols/v1/V1ProtocolError.js +91 -0
  35. package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +65 -0
  36. package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +389 -0
  37. package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +87 -0
  38. package/src/core/network/protocols/v1/validators/V1BaseOperation.js +211 -0
  39. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +26 -0
  40. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +276 -0
  41. package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +15 -0
  42. package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +17 -0
  43. package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +210 -0
  44. package/src/core/network/services/ConnectionManager.js +146 -94
  45. package/src/core/network/services/MessageOrchestrator.js +151 -27
  46. package/src/core/network/services/PendingRequestService.js +172 -0
  47. package/src/core/network/services/TransactionCommitService.js +149 -0
  48. package/src/core/network/services/TransactionPoolService.js +129 -18
  49. package/src/core/network/services/TransactionRateLimiterService.js +52 -34
  50. package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
  51. package/src/core/network/services/ValidatorObserverService.js +18 -26
  52. package/src/core/state/State.js +70 -19
  53. package/src/index.js +5 -4
  54. package/src/messages/network/v1/NetworkMessageBuilder.js +59 -79
  55. package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
  56. package/src/utils/Scheduler.js +0 -8
  57. package/src/utils/constants.js +71 -5
  58. package/src/utils/deepEqualApplyPayload.js +40 -0
  59. package/src/utils/helpers.js +10 -1
  60. package/src/utils/logger.js +25 -0
  61. package/src/utils/normalizers.js +38 -0
  62. package/src/utils/protobuf/networkV1.generated.cjs +2460 -0
  63. package/src/utils/protobuf/operationHelpers.js +24 -3
  64. package/tests/acceptance/v1/account/account.test.mjs +8 -2
  65. package/tests/acceptance/v1/tx/tx.test.mjs +23 -1
  66. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +34 -6
  67. package/tests/fixtures/networkV1.fixtures.js +2 -28
  68. package/tests/helpers/transactionPayloads.mjs +2 -2
  69. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +239 -79
  70. package/tests/unit/messages/network/NetworkMessageDirector.test.js +223 -77
  71. package/tests/unit/network/LegacyNetworkMessageRouter.test.js +54 -0
  72. package/tests/unit/network/ProtocolSession.test.js +127 -0
  73. package/tests/unit/network/networkModule.test.js +4 -1
  74. package/tests/unit/network/services/ConnectionManager.test.js +450 -0
  75. package/tests/unit/network/services/MessageOrchestrator.test.js +445 -0
  76. package/tests/unit/network/services/PendingRequestService.test.js +431 -0
  77. package/tests/unit/network/services/TransactionCommitService.test.js +246 -0
  78. package/tests/unit/network/services/TransactionPoolService.test.js +489 -0
  79. package/tests/unit/network/services/TransactionRateLimiterService.test.js +139 -0
  80. package/tests/unit/network/services/ValidatorHealthCheckService.test.js +115 -0
  81. package/tests/unit/network/services/services.test.js +17 -0
  82. package/tests/unit/network/utils/v1TestUtils.js +153 -0
  83. package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +151 -0
  84. package/tests/unit/network/v1/V1BaseOperation.test.js +356 -0
  85. package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +129 -0
  86. package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +53 -0
  87. package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +512 -0
  88. package/tests/unit/network/v1/V1LivenessRequest.test.js +32 -0
  89. package/tests/unit/network/v1/V1LivenessResponse.test.js +45 -0
  90. package/tests/unit/network/v1/V1ResultCode.test.js +84 -0
  91. package/tests/unit/network/v1/V1ValidationSchema.test.js +13 -0
  92. package/tests/unit/network/v1/connectionPolicies.test.js +49 -0
  93. package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +284 -0
  94. package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +794 -0
  95. package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +193 -0
  96. package/tests/unit/network/v1/v1.handlers.test.js +15 -0
  97. package/tests/unit/network/v1/v1.test.js +19 -0
  98. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +119 -0
  99. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +136 -0
  100. package/tests/unit/network/v1/v1ValidationSchema/common.test.js +308 -0
  101. package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +90 -0
  102. package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +133 -0
  103. package/tests/unit/unit.test.js +2 -2
  104. package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +102 -0
  105. package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
  106. package/tests/unit/utils/utils.test.js +1 -0
  107. package/.github/workflows/acceptance-tests.yml +0 -38
  108. package/.github/workflows/lint-pr-title.yml +0 -26
  109. package/.github/workflows/publish.yml +0 -33
  110. package/.github/workflows/unit-tests.yml +0 -34
  111. package/proto/network.proto +0 -74
  112. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +0 -37
  113. package/src/utils/protobuf/network.cjs +0 -840
  114. package/tests/unit/network/ConnectionManager.test.js +0 -191
@@ -0,0 +1,27 @@
1
+ import {V1ProtocolError} from '../../v1/V1ProtocolError.js';
2
+
3
+ /**
4
+ * Shared validator rejection error.
5
+ *
6
+ * Used by shared validators in `src/core/network/protocols/shared/validators/**` so they can
7
+ * reject remote peer input with a stable `resultCode`.
8
+ *
9
+ * Note: this currently extends `V1ProtocolError` so the v1 protocol can consistently treat shared
10
+ * validator rejections as typed protocol errors. Legacy protocol codepaths remain compatible because
11
+ * this is still an `Error` and preserves `.message`.
12
+ *
13
+ * Default `endConnection = true` because shared-validator rejections are typically triggered
14
+ * by invalid/malicious peer payloads and should terminate the peer connection after responding.
15
+ */
16
+ export class SharedValidatorRejectionError extends V1ProtocolError {
17
+ /**
18
+ * @param {number} resultCode Stable rejection reason (a `ResultCode` value).
19
+ * @param {string} message Human-readable error message.
20
+ * @param {boolean} [endConnection=true] Whether the transport should end the connection after responding.
21
+ */
22
+ constructor(resultCode, message, endConnection = true) {
23
+ super(resultCode, message, endConnection);
24
+ }
25
+ }
26
+
27
+ export default SharedValidatorRejectionError;
@@ -1,6 +1,8 @@
1
- import PartialOperation from './base/PartialOperation.js';
1
+ import PartialOperationValidator from './PartialOperationValidator.js';
2
+ import {ResultCode} from "../../../../../utils/constants.js";
3
+ import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
2
4
 
3
- class PartialBootstrapDeployment extends PartialOperation {
5
+ class PartialBootstrapDeploymentValidator extends PartialOperationValidator {
4
6
  constructor(state, selfAddress , config) {
5
7
  super(state, selfAddress , config);
6
8
  }
@@ -26,9 +28,12 @@ class PartialBootstrapDeployment extends PartialOperation {
26
28
  async validateBootstrapRegistration(payload) {
27
29
  const bootstrapString = payload.bdo.bs.toString('hex');
28
30
  if (null !== await this.state.getRegisteredBootstrapEntryUnsigned(bootstrapString)) {
29
- throw new Error(`Bootstrap with hash ${bootstrapString} already exists in the state. Bootstrap must be unique.`);
31
+ throw new SharedValidatorRejectionError(
32
+ ResultCode.BOOTSTRAP_ALREADY_EXISTS,
33
+ `Bootstrap with hash ${bootstrapString} already exists in the state. Bootstrap must be unique.`
34
+ );
30
35
  }
31
36
  }
32
37
  }
33
38
 
34
- export default PartialBootstrapDeployment;
39
+ export default PartialBootstrapDeploymentValidator;
@@ -1,18 +1,19 @@
1
1
  import b4a from 'b4a';
2
2
  import PeerWallet from 'trac-wallet';
3
- import Check from '../../../../../../utils/check.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";
9
- import * as operationsUtils from '../../../../../../utils/applyOperations.js';
3
+ import Check from '../../../../../utils/check.js';
4
+ import {bufferToAddress} from "../../../../state/utils/address.js";
5
+ import {createMessage} from "../../../../../utils/buffer.js";
6
+ import {OperationType, ResultCode} from "../../../../../utils/constants.js";
7
+ import {bufferToBigInt} from "../../../../../utils/amountSerialization.js";
8
+ import {FEE} from "../../../../state/utils/transaction.js";
9
+ import * as operationsUtils from '../../../../../utils/applyOperations.js';
10
+ import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
10
11
 
11
12
  const MAX_AMOUNT = BigInt('0xffffffffffffffffffffffffffffffff');
12
13
  const FEE_BIGINT = bufferToBigInt(FEE);
13
14
  const PUBLIC_KEY_LENGTH = 32;
14
15
 
15
- class PartialOperation {
16
+ class PartialOperationValidator {
16
17
  #state;
17
18
  #check;
18
19
  #config
@@ -36,18 +37,18 @@ class PartialOperation {
36
37
  }
37
38
 
38
39
  async validate(payload) {
39
- throw new Error("Method 'validate()' must be implemented.");
40
+ throw new SharedValidatorRejectionError(ResultCode.UNEXPECTED_ERROR, "Method 'validate()' must be implemented.");
40
41
  }
41
42
 
42
43
  isPayloadSchemaValid(payload) {
43
44
  if (!payload || !payload.type) {
44
- throw new Error('Payload or payload type is missing.');
45
+ throw new SharedValidatorRejectionError(ResultCode.TX_INVALID_PAYLOAD, 'Payload or payload type is missing.');
45
46
  }
46
47
 
47
48
  const selectedValidator = this.#selectCheckSchemaValidator(payload.type);
48
49
  const isPayloadValid = selectedValidator(payload);
49
50
  if (!isPayloadValid) {
50
- throw new Error(`Payload is invalid.`);
51
+ throw new SharedValidatorRejectionError(ResultCode.SCHEMA_VALIDATION_FAILED, 'Payload is invalid.');
51
52
  }
52
53
  }
53
54
 
@@ -64,21 +65,24 @@ class PartialOperation {
64
65
  case OperationType.TRANSFER:
65
66
  return this.check.validateTransferOperation.bind(this.check);
66
67
  default:
67
- throw new Error(`Unknown operation type: ${type}`);
68
+ throw new SharedValidatorRejectionError(
69
+ ResultCode.OPERATION_TYPE_UNKNOWN,
70
+ `Unknown operation type: ${type}`
71
+ );
68
72
  }
69
73
  }
70
74
 
71
75
  validateRequesterAddress(payload) {
72
76
  const incomingAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
73
77
  if (!incomingAddress) {
74
- throw new Error('Invalid requesting address in payload.');
78
+ throw new SharedValidatorRejectionError(ResultCode.REQUESTER_ADDRESS_INVALID, 'Invalid requesting address in payload.');
75
79
  }
76
80
 
77
81
  const incomingPublicKey = PeerWallet.decodeBech32mSafe(incomingAddress);
78
82
 
79
83
  // TODO: We can add check if public key belongs to the Ed25519 curve. Validate signature already checks that but it would be amazing to catch it earlier.
80
84
  if (!incomingPublicKey || incomingPublicKey.length !== PUBLIC_KEY_LENGTH) {
81
- throw new Error('Invalid requesting public key in payload.');
85
+ throw new SharedValidatorRejectionError(ResultCode.REQUESTER_PUBLIC_KEY_INVALID, 'Invalid requesting public key in payload.');
82
86
  }
83
87
  }
84
88
 
@@ -127,7 +131,10 @@ class PartialOperation {
127
131
  OperationType.TRANSFER
128
132
  ];
129
133
  default:
130
- throw new Error(`Unknown operation type: ${payload.type}`);
134
+ throw new SharedValidatorRejectionError(
135
+ ResultCode.OPERATION_TYPE_UNKNOWN,
136
+ `Unknown operation type: ${payload.type}`
137
+ );
131
138
  }
132
139
  }
133
140
 
@@ -143,11 +150,14 @@ class PartialOperation {
143
150
  const messageHash = await PeerWallet.blake3(message);
144
151
  const payloadHash = operation.tx;
145
152
  if (!b4a.equals(payloadHash, messageHash)) {
146
- throw new Error('Regenerated transaction does not match incoming transaction in payload.');
153
+ throw new SharedValidatorRejectionError(
154
+ ResultCode.TX_HASH_MISMATCH,
155
+ 'Regenerated transaction does not match incoming transaction in payload.'
156
+ );
147
157
  }
148
158
 
149
159
  if (!PeerWallet.verify(incomingSignature, messageHash, incomingPublicKey)) {
150
- throw new Error('Invalid signature in payload.');
160
+ throw new SharedValidatorRejectionError(ResultCode.TX_SIGNATURE_INVALID, 'Invalid signature in payload.');
151
161
  }
152
162
  }
153
163
 
@@ -158,7 +168,7 @@ class PartialOperation {
158
168
  const incomingTxv = operation.txv
159
169
 
160
170
  if (!b4a.equals(currentTxv, incomingTxv)) {
161
- throw new Error(`Transaction has expired.`);
171
+ throw new SharedValidatorRejectionError(ResultCode.TX_EXPIRED, 'Transaction has expired.');
162
172
  }
163
173
  }
164
174
 
@@ -169,7 +179,10 @@ class PartialOperation {
169
179
  const txHex = tx.toString('hex');
170
180
 
171
181
  if (await this.state.get(txHex) !== null) {
172
- throw new Error(`Transaction with hash ${txHex} already exists in the state.`);
182
+ throw new SharedValidatorRejectionError(
183
+ ResultCode.TX_ALREADY_EXISTS,
184
+ `Transaction with hash ${txHex} already exists in the state.`
185
+ );
173
186
  }
174
187
  }
175
188
 
@@ -180,7 +193,10 @@ class PartialOperation {
180
193
 
181
194
  const condition = va === undefined && vn === undefined && vs === undefined
182
195
  if (!condition) {
183
- throw new Error('Transfer operation must not be completed already (va, vn, vs must be undefined).');
196
+ throw new SharedValidatorRejectionError(
197
+ ResultCode.OPERATION_ALREADY_COMPLETED,
198
+ 'Transfer operation must not be completed already (va, vn, vs must be undefined).'
199
+ );
184
200
  }
185
201
  }
186
202
 
@@ -194,12 +210,12 @@ class PartialOperation {
194
210
  }
195
211
 
196
212
  if (!requesterEntry) {
197
- throw new Error('Requester address not found in state');
213
+ throw new SharedValidatorRejectionError(ResultCode.REQUESTER_NOT_FOUND, 'Requester address not found in state');
198
214
  }
199
215
 
200
216
  const requesterBalance = bufferToBigInt(requesterEntry.balance);
201
217
  if (requesterBalance < FEE_BIGINT) {
202
- throw new Error('Insufficient balance to cover transaction fee.');
218
+ throw new SharedValidatorRejectionError(ResultCode.INSUFFICIENT_FEE_BALANCE, 'Insufficient balance to cover transaction fee.');
203
219
  }
204
220
  }
205
221
 
@@ -209,7 +225,10 @@ class PartialOperation {
209
225
  const operation = payload[operationKey];
210
226
  const bs = operation.bs;
211
227
  if (b4a.equals(this.#config.bootstrap, bs)) {
212
- throw new Error(`External bootstrap is the same as MSB bootstrap: ${bs.toString('hex')}`);
228
+ throw new SharedValidatorRejectionError(
229
+ ResultCode.EXTERNAL_BOOTSTRAP_EQUALS_MSB_BOOTSTRAP,
230
+ `External bootstrap is the same as MSB bootstrap: ${bs.toString('hex')}`
231
+ );
213
232
  }
214
233
  }
215
234
 
@@ -223,10 +242,13 @@ class PartialOperation {
223
242
 
224
243
  const requesterAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
225
244
  if (this.#selfAddress === requesterAddress) {
226
- throw new Error('Requester address cannot be the same as the validator wallet address.');
245
+ throw new SharedValidatorRejectionError(
246
+ ResultCode.SELF_VALIDATION_FORBIDDEN,
247
+ 'Requester address cannot be the same as the validator wallet address.'
248
+ );
227
249
  }
228
250
  }
229
251
 
230
252
  }
231
253
 
232
- export default PartialOperation;
254
+ export default PartialOperationValidator;
@@ -1,10 +1,11 @@
1
1
  import b4a from 'b4a';
2
- import {OperationType} from "../../../../../utils/constants.js";
2
+ import {OperationType, ResultCode} from "../../../../../utils/constants.js";
3
3
  import {bufferToAddress} from "../../../../state/utils/address.js";
4
- import PartialOperation from './base/PartialOperation.js';
4
+ import PartialOperationValidator from './PartialOperationValidator.js';
5
5
  import {bufferToBigInt} from "../../../../../utils/amountSerialization.js";
6
+ import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
6
7
 
7
- class PartialRoleAccess extends PartialOperation {
8
+ class PartialRoleAccessValidator extends PartialOperationValidator {
8
9
  #config;
9
10
 
10
11
  constructor(state, selfAddress, config) {
@@ -42,17 +43,26 @@ class PartialRoleAccess extends PartialOperation {
42
43
  const nodeAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
43
44
  const nodeEntry = await this.state.getNodeEntry(nodeAddress);
44
45
  if (!nodeEntry) {
45
- throw new Error(`Node with address ${nodeAddress} entry does not exist.`);
46
+ throw new SharedValidatorRejectionError(
47
+ ResultCode.ROLE_NODE_ENTRY_NOT_FOUND,
48
+ `Node with address ${nodeAddress} entry does not exist.`
49
+ );
46
50
  }
47
51
 
48
52
  const isNodeAlreadyWriter = nodeEntry.isWriter;
49
53
  if (isNodeAlreadyWriter) {
50
- throw new Error(`Node with address ${nodeAddress} is already a writer.`);
54
+ throw new SharedValidatorRejectionError(
55
+ ResultCode.ROLE_NODE_ALREADY_WRITER,
56
+ `Node with address ${nodeAddress} is already a writer.`
57
+ );
51
58
  }
52
59
 
53
60
  const isNodeWhitelisted = nodeEntry.isWhitelisted;
54
61
  if (!isNodeWhitelisted) {
55
- throw new Error(`Node with address ${nodeAddress} is not whitelisted.`);
62
+ throw new SharedValidatorRejectionError(
63
+ ResultCode.ROLE_NODE_NOT_WHITELISTED,
64
+ `Node with address ${nodeAddress} is not whitelisted.`
65
+ );
56
66
  }
57
67
  return;
58
68
 
@@ -60,24 +70,33 @@ class PartialRoleAccess extends PartialOperation {
60
70
  const nodeAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
61
71
  const nodeEntry = await this.state.getNodeEntry(nodeAddress);
62
72
  if (!nodeEntry) {
63
- throw new Error(`Node with address ${nodeAddress} entry does not exist.`);
73
+ throw new SharedValidatorRejectionError(
74
+ ResultCode.ROLE_NODE_ENTRY_NOT_FOUND,
75
+ `Node with address ${nodeAddress} entry does not exist.`
76
+ );
64
77
  }
65
78
 
66
79
  const isAlreadyWriter = nodeEntry.isWriter;
67
80
  if (!isAlreadyWriter) {
68
- throw new Error(`Node with address ${nodeAddress} is not a writer.`);
81
+ throw new SharedValidatorRejectionError(
82
+ ResultCode.ROLE_NODE_NOT_WRITER,
83
+ `Node with address ${nodeAddress} is not a writer.`
84
+ );
69
85
  }
70
86
 
71
87
  const isAlreadyIndexer = nodeEntry.isIndexer;
72
88
  if (isAlreadyIndexer) {
73
- throw new Error(`Node with address ${nodeAddress} is an indexer.`);
89
+ throw new SharedValidatorRejectionError(
90
+ ResultCode.ROLE_NODE_IS_INDEXER,
91
+ `Node with address ${nodeAddress} is an indexer.`
92
+ );
74
93
  }
75
94
  return;
76
95
 
77
96
  } else if (type === OperationType.ADMIN_RECOVERY) {
78
97
  const adminEntry = await this.state.getAdminEntry();
79
98
  if (!adminEntry) {
80
- throw new Error('Admin entry does not exist.');
99
+ throw new SharedValidatorRejectionError(ResultCode.ROLE_ADMIN_ENTRY_MISSING, 'Admin entry does not exist.');
81
100
  }
82
101
 
83
102
  const adminAddressBuffer = payload.address;
@@ -87,20 +106,29 @@ class PartialRoleAccess extends PartialOperation {
87
106
  !b4a.equals(payload.rao.iw, adminEntry.wk)
88
107
  );
89
108
  if (!isRecoveryCase) {
90
- throw new Error(`Node with address ${adminAddress} is not a valid recovery case.`);
109
+ throw new SharedValidatorRejectionError(
110
+ ResultCode.ROLE_INVALID_RECOVERY_CASE,
111
+ `Node with address ${adminAddress} is not a valid recovery case.`
112
+ );
91
113
  }
92
114
 
93
115
  return;
94
116
  }
95
117
 
96
- throw new Error(`Unknown role access operation type: ${type}`);
118
+ throw new SharedValidatorRejectionError(
119
+ ResultCode.ROLE_UNKNOWN_OPERATION,
120
+ `Unknown role access operation type: ${type}`
121
+ );
97
122
  }
98
123
 
99
124
  async validateWriterKey(payload) {
100
125
  const requesterAddress = bufferToAddress(payload.address, this.#config.addressPrefix);
101
126
  const nodeEntry = await this.state.getNodeEntry(requesterAddress);
102
127
  if (!nodeEntry) {
103
- throw new Error(`Node entry not found for address ${requesterAddress}`);
128
+ throw new SharedValidatorRejectionError(
129
+ ResultCode.REQUESTER_NOT_FOUND,
130
+ `Node entry not found for address ${requesterAddress}`
131
+ );
104
132
  }
105
133
 
106
134
  const writerKey = payload.rao.iw.toString('hex');
@@ -111,7 +139,10 @@ class PartialRoleAccess extends PartialOperation {
111
139
  const isOwner = b4a.equals(addressFromRegisteredWritingKey, payload.address);
112
140
 
113
141
  if (!isCurrentWk || !isOwner) {
114
- throw new Error('Invalid writer key: either not owned by requester or different from assigned key');
142
+ throw new SharedValidatorRejectionError(
143
+ ResultCode.ROLE_INVALID_WRITER_KEY,
144
+ 'Invalid writer key: either not owned by requester or different from assigned key'
145
+ );
115
146
  }
116
147
  }
117
148
  }
@@ -126,15 +157,18 @@ class PartialRoleAccess extends PartialOperation {
126
157
  }
127
158
 
128
159
  if (!requesterEntry) {
129
- throw new Error('Requester address not found in state');
160
+ throw new SharedValidatorRejectionError(ResultCode.REQUESTER_NOT_FOUND, 'Requester address not found in state');
130
161
  }
131
162
  const requesterBalance = bufferToBigInt(requesterEntry.balance);
132
163
 
133
164
  const requiredBalance = this.fee * 11n;
134
165
  if (requesterBalance < requiredBalance) {
135
- throw new Error('Insufficient requester balance to cover role access operation FEE.');
166
+ throw new SharedValidatorRejectionError(
167
+ ResultCode.ROLE_INSUFFICIENT_FEE_BALANCE,
168
+ 'Insufficient requester balance to cover role access operation FEE.'
169
+ );
136
170
  }
137
171
  }
138
172
  }
139
173
 
140
- export default PartialRoleAccess;
174
+ export default PartialRoleAccessValidator;
@@ -1,9 +1,11 @@
1
1
  import b4a from 'b4a';
2
2
  import {safeDecodeApplyOperation} from "../../../../../utils/protobuf/operationHelpers.js";
3
3
  import deploymentEntryUtils from "../../../../state/utils/deploymentEntry.js";
4
- import PartialOperation from './base/PartialOperation.js';
4
+ import PartialOperationValidator from './PartialOperationValidator.js';
5
+ import {ResultCode} from "../../../../../utils/constants.js";
6
+ import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
5
7
 
6
- class PartialTransaction extends PartialOperation {
8
+ class PartialTransactionValidator extends PartialOperationValidator {
7
9
  #config
8
10
 
9
11
  constructor(state, selfAddress, config) {
@@ -32,14 +34,20 @@ class PartialTransaction extends PartialOperation {
32
34
 
33
35
  validateMsbBootstrap(payload) {
34
36
  if (!b4a.equals(this.#config.bootstrap, payload.txo.mbs)) {
35
- throw new Error(`Declared MSB bootstrap is different than network bootstrap in transaction operation: ${payload.txo.mbs.toString('hex')}`);
37
+ throw new SharedValidatorRejectionError(
38
+ ResultCode.MSB_BOOTSTRAP_MISMATCH,
39
+ `Declared MSB bootstrap is different than network bootstrap in transaction operation: ${payload.txo.mbs.toString('hex')}`
40
+ );
36
41
  }
37
42
  }
38
43
 
39
44
  async validateIfExternalBootstrapHasBeenDeployed(payload) {
40
45
  const externalBootstrapResult = await this.state.getRegisteredBootstrapEntry(payload.txo.bs.toString('hex'));
41
46
  if (externalBootstrapResult === null) {
42
- throw new Error(`External bootstrap with hash ${payload.txo.bs.toString('hex')} is not registered as deployment entry.`);
47
+ throw new SharedValidatorRejectionError(
48
+ ResultCode.EXTERNAL_BOOTSTRAP_NOT_DEPLOYED,
49
+ `External bootstrap with hash ${payload.txo.bs.toString('hex')} is not registered as deployment entry.`
50
+ );
43
51
  }
44
52
 
45
53
  const decodedPayload = deploymentEntryUtils.decode(externalBootstrapResult, this.#config.addressLength);
@@ -47,17 +55,23 @@ class PartialTransaction extends PartialOperation {
47
55
  const getBootstrapTransactionTxPayload = await this.state.get(txHash.toString('hex'));
48
56
 
49
57
  if (getBootstrapTransactionTxPayload === null) {
50
- throw new Error(`External bootstrap is not registered as usual tx ${externalBootstrapResult.toString('hex')}: ${payload}`);
58
+ throw new SharedValidatorRejectionError(
59
+ ResultCode.EXTERNAL_BOOTSTRAP_TX_MISSING,
60
+ `External bootstrap is not registered as usual tx ${externalBootstrapResult.toString('hex')}: ${payload}`
61
+ );
51
62
  }
52
63
 
53
64
  const decodedBootstrapDeployment = safeDecodeApplyOperation(getBootstrapTransactionTxPayload)
54
65
 
55
66
  // edge case
56
67
  if (!b4a.equals(decodedBootstrapDeployment.bdo.bs, payload.txo.bs)) {
57
- throw new Error(`External bootstrap does not match the one in the transaction payload: ${decodedBootstrapDeployment.bdo.bs.toString('hex')} !== ${payload.txo.bs.toString('hex')}`);
68
+ throw new SharedValidatorRejectionError(
69
+ ResultCode.EXTERNAL_BOOTSTRAP_MISMATCH,
70
+ `External bootstrap does not match the one in the transaction payload: ${decodedBootstrapDeployment.bdo.bs.toString('hex')} !== ${payload.txo.bs.toString('hex')}`
71
+ );
58
72
  }
59
73
  }
60
74
 
61
75
  }
62
76
 
63
- export default PartialTransaction;
77
+ export default PartialTransactionValidator;
@@ -2,9 +2,11 @@ import PeerWallet from 'trac-wallet';
2
2
 
3
3
  import {bufferToAddress} from "../../../../state/utils/address.js";
4
4
  import {bufferToBigInt} from "../../../../../utils/amountSerialization.js";
5
- import PartialOperation from './base/PartialOperation.js';
5
+ import {ResultCode} from "../../../../../utils/constants.js";
6
+ import PartialOperationValidator from './PartialOperationValidator.js';
7
+ import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
6
8
 
7
- class PartialTransfer extends PartialOperation {
9
+ class PartialTransferValidator extends PartialOperationValidator {
8
10
  #config
9
11
 
10
12
  constructor(state, selfAddress, config) {
@@ -31,12 +33,18 @@ class PartialTransfer extends PartialOperation {
31
33
  #validateRecipientAddress(payload) {
32
34
  const incomingAddress = bufferToAddress(payload.tro.to, this.#config.addressPrefix);
33
35
  if (!incomingAddress) {
34
- throw new Error('Invalid recipient address in transfer payload.');
36
+ throw new SharedValidatorRejectionError(
37
+ ResultCode.TRANSFER_RECIPIENT_ADDRESS_INVALID,
38
+ 'Invalid recipient address in transfer payload.'
39
+ );
35
40
  }
36
41
 
37
42
  const incomingPublicKey = PeerWallet.decodeBech32mSafe(incomingAddress);
38
43
  if (incomingPublicKey === null) {
39
- throw new Error('Invalid recipient public key in transfer payload.');
44
+ throw new SharedValidatorRejectionError(
45
+ ResultCode.TRANSFER_RECIPIENT_PUBLIC_KEY_INVALID,
46
+ 'Invalid recipient public key in transfer payload.'
47
+ );
40
48
  }
41
49
 
42
50
  }
@@ -47,7 +55,10 @@ class PartialTransfer extends PartialOperation {
47
55
 
48
56
  const transferAmount = bufferToBigInt(payload.tro.am);
49
57
  if (transferAmount > this.max_amount) {
50
- throw new Error('Transfer amount exceeds maximum allowed value');
58
+ throw new SharedValidatorRejectionError(
59
+ ResultCode.TRANSFER_AMOUNT_TOO_LARGE,
60
+ 'Transfer amount exceeds maximum allowed value'
61
+ );
51
62
  }
52
63
 
53
64
  const isSelfTransfer = senderAddress === recipientAddress;
@@ -55,12 +66,15 @@ class PartialTransfer extends PartialOperation {
55
66
 
56
67
  const senderEntry = await this.state.getNodeEntryUnsigned(senderAddress);
57
68
  if (!senderEntry) {
58
- throw new Error('Sender account not found');
69
+ throw new SharedValidatorRejectionError(ResultCode.TRANSFER_SENDER_NOT_FOUND, 'Sender account not found');
59
70
  }
60
71
 
61
72
  const senderBalance = bufferToBigInt(senderEntry.balance);
62
73
  if (!(senderBalance >= totalDeductedAmount)) {
63
- throw new Error('Insufficient balance for transfer' + (isSelfTransfer ? ' fee' : ' + fee'));
74
+ throw new SharedValidatorRejectionError(
75
+ ResultCode.TRANSFER_INSUFFICIENT_BALANCE,
76
+ 'Insufficient balance for transfer' + (isSelfTransfer ? ' fee' : ' + fee')
77
+ );
64
78
  }
65
79
 
66
80
  if (!isSelfTransfer) {
@@ -69,11 +83,14 @@ class PartialTransfer extends PartialOperation {
69
83
  const recipientBalance = bufferToBigInt(recipientEntry.balance);
70
84
  const newRecipientBalance = recipientBalance + transferAmount;
71
85
  if (newRecipientBalance > this.max_amount) {
72
- throw new Error('Transfer would cause recipient balance to exceed maximum allowed value');
86
+ throw new SharedValidatorRejectionError(
87
+ ResultCode.TRANSFER_RECIPIENT_BALANCE_OVERFLOW,
88
+ 'Transfer would cause recipient balance to exceed maximum allowed value'
89
+ );
73
90
  }
74
91
  }
75
92
  }
76
93
  }
77
94
  }
78
95
 
79
- export default PartialTransfer;
96
+ export default PartialTransferValidator;
@@ -1,15 +1,99 @@
1
- import { MessageHeader } from '../../../../utils/protobuf/network.cjs';
1
+ import { decodeV1networkOperation } from '../../../../utils/protobuf/operationHelpers.js'
2
+ import b4a from 'b4a'
3
+ import { NetworkOperationType, V1_PROTOCOL_PAYLOAD_MAX_SIZE } from '../../../../utils/constants.js'
4
+ import { publicKeyToAddress } from '../../../../utils/helpers.js'
5
+ import V1LivenessOperationHandler from './handlers/V1LivenessOperationHandler.js'
6
+ import V1BroadcastTransactionOperationHandler from './handlers/V1BroadcastTransactionOperationHandler.js'
2
7
 
3
8
  class NetworkMessageRouterV1 {
4
- #config;
9
+ #config
10
+ #livenessRequestHandler
11
+ #broadcastTransactionHandler
5
12
 
6
- constructor(config) {
7
- this.#config = config;
13
+ constructor(
14
+ state,
15
+ wallet,
16
+ rateLimiterService,
17
+ txPoolService,
18
+ pendingRequestsService,
19
+ transactionCommitService,
20
+ config
21
+ ) {
22
+ this.#config = config
23
+ this.#livenessRequestHandler = new V1LivenessOperationHandler(
24
+ wallet,
25
+ rateLimiterService,
26
+ pendingRequestsService,
27
+ config
28
+ );
29
+ this.#broadcastTransactionHandler = new V1BroadcastTransactionOperationHandler(
30
+ state,
31
+ wallet,
32
+ rateLimiterService,
33
+ txPoolService,
34
+ pendingRequestsService,
35
+ transactionCommitService,
36
+ config
37
+ );
8
38
  }
9
39
 
10
- async route(incomingMessage) {
11
- MessageHeader.decode(incomingMessage);
40
+ async route(incomingMessage, connection) {
41
+ if (!this.#preValidate(incomingMessage)) {
42
+ this.#disconnect(connection, 'Pre-validation failed for incoming V1 message')
43
+ return;
44
+ }
45
+ let decodedMessage;
46
+
47
+ try {
48
+ decodedMessage = decodeV1networkOperation(incomingMessage)
49
+ } catch (error) {
50
+ this.#disconnect(connection, `Failed to decode incoming V1 message: ${error.message}`)
51
+ return;
52
+ }
53
+
54
+ // TODO: Decide if we really need to check decodedMessage.type here, since this is done
55
+ // again in the next switch statement
56
+ if (!decodedMessage || !Number.isInteger(decodedMessage.type) || decodedMessage.type <= 0) {
57
+ this.#disconnect(connection, `Invalid V1 message type: ${decodedMessage?.type}`)
58
+ return;
59
+ }
60
+
61
+ // We received a v1 message, so we set the connection protocol accordingly
62
+ connection.protocolSession.setV1AsPreferredProtocol()
63
+
64
+ try {
65
+ switch (decodedMessage.type) {
66
+ case NetworkOperationType.LIVENESS_REQUEST:
67
+ await this.#livenessRequestHandler.handleRequest(decodedMessage, connection);
68
+ break;
69
+ case NetworkOperationType.LIVENESS_RESPONSE:
70
+ await this.#livenessRequestHandler.handleResponse(decodedMessage, connection);
71
+ break;
72
+
73
+ case NetworkOperationType.BROADCAST_TRANSACTION_REQUEST:
74
+ await this.#broadcastTransactionHandler.handleRequest(decodedMessage, connection);
75
+ break;
76
+
77
+ case NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE:
78
+ await this.#broadcastTransactionHandler.handleResponse(decodedMessage, connection);
79
+ break;
80
+ default:
81
+ this.#disconnect(connection, `Unsupported V1 message type: ${decodedMessage.type}`)
82
+ }
83
+ } catch (error) {
84
+ this.#disconnect(connection, `Unhandled error while routing V1 message: ${error.message}`)
85
+ }
86
+ }
87
+
88
+ #preValidate(incomingMessage) {
89
+ return !(!incomingMessage || !b4a.isBuffer(incomingMessage) || incomingMessage.length === 0 || incomingMessage.length > V1_PROTOCOL_PAYLOAD_MAX_SIZE);
90
+ }
91
+
92
+ #disconnect(connection, reason) {
93
+ const sender = publicKeyToAddress(connection.remotePublicKey, this.#config)
94
+ console.error(`NetworkMessageRouterV1: ${reason}, sender: ${sender}`)
95
+ connection.end();
12
96
  }
13
97
  }
14
98
 
15
- export default NetworkMessageRouterV1;
99
+ export default NetworkMessageRouterV1