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.
- package/package.json +9 -4
- package/proto/network/v1/enums/message_type.proto +16 -0
- package/proto/network/v1/enums/result_code.proto +84 -0
- package/proto/network/v1/messages/broadcast_transaction_request.proto +9 -0
- package/proto/network/v1/messages/broadcast_transaction_response.proto +13 -0
- package/proto/network/v1/messages/liveness_request.proto +8 -0
- package/proto/network/v1/messages/liveness_response.proto +11 -0
- package/proto/network/v1/network_message.proto +22 -0
- package/rpc/rpc_services.js +22 -4
- package/scripts/generate-protobufs.js +37 -12
- package/src/config/config.js +26 -5
- package/src/config/env.js +25 -11
- package/src/core/network/Network.js +73 -36
- package/src/core/network/protocols/LegacyProtocol.js +21 -11
- package/src/core/network/protocols/NetworkMessages.js +38 -17
- package/src/core/network/protocols/ProtocolInterface.js +14 -2
- package/src/core/network/protocols/ProtocolSession.js +144 -17
- package/src/core/network/protocols/V1Protocol.js +37 -18
- package/src/core/network/protocols/connectionPolicies.js +88 -0
- package/src/core/network/protocols/legacy/NetworkMessageRouter.js +25 -19
- package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +23 -12
- package/src/core/network/protocols/legacy/handlers/{GetRequestHandler.js → LegacyGetRequestHandler.js} +6 -6
- package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +23 -0
- package/src/core/network/protocols/{shared/handlers/RoleOperationHandler.js → legacy/handlers/LegacyRoleOperationHandler.js} +18 -11
- package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +28 -17
- package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +17 -11
- package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +27 -0
- package/src/core/network/protocols/shared/validators/{PartialBootstrapDeployment.js → PartialBootstrapDeploymentValidator.js} +9 -4
- package/src/core/network/protocols/shared/validators/{base/PartialOperation.js → PartialOperationValidator.js} +47 -25
- package/src/core/network/protocols/shared/validators/{PartialRoleAccess.js → PartialRoleAccessValidator.js} +51 -17
- package/src/core/network/protocols/shared/validators/{PartialTransaction.js → PartialTransactionValidator.js} +21 -7
- package/src/core/network/protocols/shared/validators/{PartialTransfer.js → PartialTransferValidator.js} +26 -9
- package/src/core/network/protocols/v1/NetworkMessageRouter.js +91 -7
- package/src/core/network/protocols/v1/V1ProtocolError.js +91 -0
- package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +65 -0
- package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +389 -0
- package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +87 -0
- package/src/core/network/protocols/v1/validators/V1BaseOperation.js +211 -0
- package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +26 -0
- package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +276 -0
- package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +15 -0
- package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +17 -0
- package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +210 -0
- package/src/core/network/services/ConnectionManager.js +146 -94
- package/src/core/network/services/MessageOrchestrator.js +151 -27
- package/src/core/network/services/PendingRequestService.js +172 -0
- package/src/core/network/services/TransactionCommitService.js +149 -0
- package/src/core/network/services/TransactionPoolService.js +129 -18
- package/src/core/network/services/TransactionRateLimiterService.js +52 -34
- package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
- package/src/core/network/services/ValidatorObserverService.js +18 -26
- package/src/core/state/State.js +70 -19
- package/src/index.js +5 -4
- package/src/messages/network/v1/NetworkMessageBuilder.js +59 -79
- package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
- package/src/utils/Scheduler.js +0 -8
- package/src/utils/constants.js +71 -5
- package/src/utils/deepEqualApplyPayload.js +40 -0
- package/src/utils/helpers.js +10 -1
- package/src/utils/logger.js +25 -0
- package/src/utils/normalizers.js +38 -0
- package/src/utils/protobuf/networkV1.generated.cjs +2460 -0
- package/src/utils/protobuf/operationHelpers.js +24 -3
- package/tests/acceptance/v1/account/account.test.mjs +8 -2
- package/tests/acceptance/v1/tx/tx.test.mjs +23 -1
- package/tests/acceptance/v1/tx-details/tx-details.test.mjs +34 -6
- package/tests/fixtures/networkV1.fixtures.js +2 -28
- package/tests/helpers/transactionPayloads.mjs +2 -2
- package/tests/unit/messages/network/NetworkMessageBuilder.test.js +239 -79
- package/tests/unit/messages/network/NetworkMessageDirector.test.js +223 -77
- package/tests/unit/network/LegacyNetworkMessageRouter.test.js +54 -0
- package/tests/unit/network/ProtocolSession.test.js +127 -0
- package/tests/unit/network/networkModule.test.js +4 -1
- package/tests/unit/network/services/ConnectionManager.test.js +450 -0
- package/tests/unit/network/services/MessageOrchestrator.test.js +445 -0
- package/tests/unit/network/services/PendingRequestService.test.js +431 -0
- package/tests/unit/network/services/TransactionCommitService.test.js +246 -0
- package/tests/unit/network/services/TransactionPoolService.test.js +489 -0
- package/tests/unit/network/services/TransactionRateLimiterService.test.js +139 -0
- package/tests/unit/network/services/ValidatorHealthCheckService.test.js +115 -0
- package/tests/unit/network/services/services.test.js +17 -0
- package/tests/unit/network/utils/v1TestUtils.js +153 -0
- package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +151 -0
- package/tests/unit/network/v1/V1BaseOperation.test.js +356 -0
- package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +129 -0
- package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +53 -0
- package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +512 -0
- package/tests/unit/network/v1/V1LivenessRequest.test.js +32 -0
- package/tests/unit/network/v1/V1LivenessResponse.test.js +45 -0
- package/tests/unit/network/v1/V1ResultCode.test.js +84 -0
- package/tests/unit/network/v1/V1ValidationSchema.test.js +13 -0
- package/tests/unit/network/v1/connectionPolicies.test.js +49 -0
- package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +284 -0
- package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +794 -0
- package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +193 -0
- package/tests/unit/network/v1/v1.handlers.test.js +15 -0
- package/tests/unit/network/v1/v1.test.js +19 -0
- package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +119 -0
- package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +136 -0
- package/tests/unit/network/v1/v1ValidationSchema/common.test.js +308 -0
- package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +90 -0
- package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +133 -0
- package/tests/unit/unit.test.js +2 -2
- package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +102 -0
- package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
- package/tests/unit/utils/utils.test.js +1 -0
- package/.github/workflows/acceptance-tests.yml +0 -38
- package/.github/workflows/lint-pr-title.yml +0 -26
- package/.github/workflows/publish.yml +0 -33
- package/.github/workflows/unit-tests.yml +0 -34
- package/proto/network.proto +0 -74
- package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +0 -37
- package/src/utils/protobuf/network.cjs +0 -840
- 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
|
|
1
|
+
import PartialOperationValidator from './PartialOperationValidator.js';
|
|
2
|
+
import {ResultCode} from "../../../../../utils/constants.js";
|
|
3
|
+
import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
|
|
2
4
|
|
|
3
|
-
class
|
|
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
|
|
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
|
|
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 '
|
|
4
|
-
import {bufferToAddress} from "
|
|
5
|
-
import {createMessage} from "
|
|
6
|
-
import {OperationType} from "
|
|
7
|
-
import {bufferToBigInt} from "
|
|
8
|
-
import {FEE} from "
|
|
9
|
-
import * as operationsUtils from '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4
|
+
import PartialOperationValidator from './PartialOperationValidator.js';
|
|
5
|
+
import {ResultCode} from "../../../../../utils/constants.js";
|
|
6
|
+
import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
|
|
5
7
|
|
|
6
|
-
class
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
+
import {ResultCode} from "../../../../../utils/constants.js";
|
|
6
|
+
import PartialOperationValidator from './PartialOperationValidator.js';
|
|
7
|
+
import SharedValidatorRejectionError from '../errors/SharedValidatorRejectionError.js';
|
|
6
8
|
|
|
7
|
-
class
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
96
|
+
export default PartialTransferValidator;
|
|
@@ -1,15 +1,99 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
7
|
-
|
|
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
|
-
|
|
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
|