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,211 @@
|
|
|
1
|
+
import V1ValidationSchema from "./V1ValidationSchema.js";
|
|
2
|
+
import {NetworkOperationType, ResultCode} from "../../../../../utils/constants.js";
|
|
3
|
+
import PeerWallet from "trac-wallet";
|
|
4
|
+
import b4a from "b4a";
|
|
5
|
+
import {
|
|
6
|
+
createMessage,
|
|
7
|
+
encodeCapabilities,
|
|
8
|
+
idToBuffer,
|
|
9
|
+
safeWriteUInt32BE,
|
|
10
|
+
timestampToBuffer
|
|
11
|
+
} from "../../../../../utils/buffer.js";
|
|
12
|
+
import {
|
|
13
|
+
V1InvalidPayloadError,
|
|
14
|
+
V1ProtocolError,
|
|
15
|
+
V1SignatureInvalidError,
|
|
16
|
+
V1UnexpectedError,
|
|
17
|
+
} from "../V1ProtocolError.js";
|
|
18
|
+
import _ from 'lodash';
|
|
19
|
+
class V1BaseOperation {
|
|
20
|
+
#v1ValidationSchema
|
|
21
|
+
#config
|
|
22
|
+
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.#config = config;
|
|
25
|
+
this.#v1ValidationSchema = new V1ValidationSchema();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async validate(payload, connection, pendingRequestServiceEntry) {
|
|
29
|
+
throw new Error("Method 'validate()' must be implemented.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isPayloadSchemaValid(payload) {
|
|
33
|
+
if (_.isNil(payload?.type)) {
|
|
34
|
+
throw new V1InvalidPayloadError('Payload or payload type is missing.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const selectedValidator = this.#selectCheckSchemaValidator(payload.type);
|
|
38
|
+
const isPayloadValid = selectedValidator(payload);
|
|
39
|
+
if (!isPayloadValid) {
|
|
40
|
+
throw new V1InvalidPayloadError('Payload is invalid.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async validateSignature(payload, remotePublicKey) {
|
|
45
|
+
let signature;
|
|
46
|
+
let message;
|
|
47
|
+
try {
|
|
48
|
+
const result = this.#buildSignatureMessage(payload);
|
|
49
|
+
signature = result.signature;
|
|
50
|
+
message = result.message;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof V1ProtocolError) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
throw new V1InvalidPayloadError(`Failed to build signature message: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let hash;
|
|
59
|
+
try {
|
|
60
|
+
hash = await PeerWallet.blake3(message);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new V1InvalidPayloadError('Failed to hash signature message.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let verified = false;
|
|
66
|
+
try {
|
|
67
|
+
verified = PeerWallet.verify(signature, hash, remotePublicKey);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
verified = false;
|
|
70
|
+
}
|
|
71
|
+
if (!verified) {
|
|
72
|
+
throw new V1SignatureInvalidError('signature verification failed.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#buildSignatureMessage(payload) {
|
|
77
|
+
if (!Number.isInteger(payload.type)) {
|
|
78
|
+
throw new V1InvalidPayloadError('Operation type must be an integer.');
|
|
79
|
+
}
|
|
80
|
+
if (payload.type === 0) {
|
|
81
|
+
throw new V1InvalidPayloadError('Operation type is unspecified.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const idBuf = idToBuffer(payload.id);
|
|
85
|
+
const tsBuf = timestampToBuffer(payload.timestamp);
|
|
86
|
+
const capsBuf = encodeCapabilities(payload.capabilities ?? []);
|
|
87
|
+
|
|
88
|
+
switch (payload.type) {
|
|
89
|
+
case NetworkOperationType.LIVENESS_REQUEST: {
|
|
90
|
+
const nonce = payload.liveness_request.nonce;
|
|
91
|
+
const signature = payload.liveness_request.signature;
|
|
92
|
+
const message = createMessage(payload.type, idBuf, tsBuf, nonce, capsBuf);
|
|
93
|
+
return {signature, message};
|
|
94
|
+
}
|
|
95
|
+
case NetworkOperationType.LIVENESS_RESPONSE: {
|
|
96
|
+
const nonce = payload.liveness_response.nonce;
|
|
97
|
+
const signature = payload.liveness_response.signature;
|
|
98
|
+
const result = payload.liveness_response.result;
|
|
99
|
+
const message = createMessage(payload.type, idBuf, tsBuf, nonce, safeWriteUInt32BE(result, 0), capsBuf);
|
|
100
|
+
return {signature, message};
|
|
101
|
+
}
|
|
102
|
+
case NetworkOperationType.BROADCAST_TRANSACTION_REQUEST: {
|
|
103
|
+
const data = payload.broadcast_transaction_request.data;
|
|
104
|
+
const nonce = payload.broadcast_transaction_request.nonce;
|
|
105
|
+
const signature = payload.broadcast_transaction_request.signature;
|
|
106
|
+
const message = createMessage(payload.type, idBuf, tsBuf, data, nonce, capsBuf);
|
|
107
|
+
return {signature, message};
|
|
108
|
+
}
|
|
109
|
+
case NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE: {
|
|
110
|
+
const nonce = payload.broadcast_transaction_response.nonce;
|
|
111
|
+
const signature = payload.broadcast_transaction_response.signature;
|
|
112
|
+
const result = payload.broadcast_transaction_response.result;
|
|
113
|
+
const proofRaw = payload.broadcast_transaction_response.proof;
|
|
114
|
+
const proof = b4a.isBuffer(proofRaw) ? proofRaw : b4a.alloc(0);
|
|
115
|
+
const hasProof = proof.length > 0;
|
|
116
|
+
const timestampRaw = payload.broadcast_transaction_response.timestamp;
|
|
117
|
+
const timestamp = Number.isSafeInteger(timestampRaw) ? timestampRaw : 0;
|
|
118
|
+
const hasTimestamp = timestamp > 0;
|
|
119
|
+
|
|
120
|
+
if (result === ResultCode.OK) {
|
|
121
|
+
if (!hasProof || !hasTimestamp) {
|
|
122
|
+
throw new V1InvalidPayloadError('Result code OK requires non-empty proof and timestamp > 0.');
|
|
123
|
+
}
|
|
124
|
+
} else if (result === ResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE) {
|
|
125
|
+
if (hasProof) {
|
|
126
|
+
throw new V1InvalidPayloadError('Result code TX_ACCEPTED_PROOF_UNAVAILABLE requires empty proof.');
|
|
127
|
+
}
|
|
128
|
+
if (!hasTimestamp) {
|
|
129
|
+
throw new V1InvalidPayloadError('Result code TX_ACCEPTED_PROOF_UNAVAILABLE requires timestamp > 0.');
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
if (hasProof) {
|
|
133
|
+
throw new V1InvalidPayloadError('Non-OK result code requires empty proof.');
|
|
134
|
+
}
|
|
135
|
+
if (timestamp !== 0) {
|
|
136
|
+
throw new V1InvalidPayloadError('Non-OK result code requires timestamp to be 0, except TX_ACCEPTED_PROOF_UNAVAILABLE.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const message = createMessage(
|
|
141
|
+
payload.type,
|
|
142
|
+
idBuf,
|
|
143
|
+
tsBuf,
|
|
144
|
+
nonce,
|
|
145
|
+
proof,
|
|
146
|
+
timestampToBuffer(timestamp),
|
|
147
|
+
safeWriteUInt32BE(result, 0),
|
|
148
|
+
capsBuf
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return {signature, message};
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
throw new V1UnexpectedError(`Unknown operation type: ${payload.type}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#selectCheckSchemaValidator(type) {
|
|
159
|
+
if (!Number.isInteger(type)) {
|
|
160
|
+
throw new V1InvalidPayloadError('Operation type must be an integer.');
|
|
161
|
+
}
|
|
162
|
+
if (type === 0) {
|
|
163
|
+
throw new V1InvalidPayloadError('Operation type is unspecified.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
switch (type) {
|
|
167
|
+
case NetworkOperationType.LIVENESS_REQUEST:
|
|
168
|
+
return this.#v1ValidationSchema.validateV1LivenessRequest.bind(this.#v1ValidationSchema);
|
|
169
|
+
case NetworkOperationType.LIVENESS_RESPONSE:
|
|
170
|
+
return this.#v1ValidationSchema.validateV1LivenessResponse.bind(this.#v1ValidationSchema);
|
|
171
|
+
case NetworkOperationType.BROADCAST_TRANSACTION_REQUEST:
|
|
172
|
+
return this.#v1ValidationSchema.validateV1BroadcastTransactionRequest.bind(this.#v1ValidationSchema);
|
|
173
|
+
case NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE:
|
|
174
|
+
return this.#v1ValidationSchema.validateV1BroadcastTransactionResponse.bind(this.#v1ValidationSchema);
|
|
175
|
+
default:
|
|
176
|
+
throw new V1UnexpectedError(`Unknown operation type: ${type}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
validatePeerCorrectness(remotePublicKey, pendingRequestServiceEntry) {
|
|
181
|
+
const senderPublicKeyHex = b4a.toString(remotePublicKey, 'hex');
|
|
182
|
+
if (senderPublicKeyHex !== pendingRequestServiceEntry.requestedTo) {
|
|
183
|
+
throw new V1InvalidPayloadError(
|
|
184
|
+
`Response sender mismatch. Expected ${pendingRequestServiceEntry.requestedTo}, got ${senderPublicKeyHex}.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
validateResponseType(payload, pendingRequestServiceEntry) {
|
|
190
|
+
let expectedResponseType;
|
|
191
|
+
switch (pendingRequestServiceEntry.requestType) {
|
|
192
|
+
case NetworkOperationType.LIVENESS_REQUEST:
|
|
193
|
+
expectedResponseType = NetworkOperationType.LIVENESS_RESPONSE;
|
|
194
|
+
break;
|
|
195
|
+
case NetworkOperationType.BROADCAST_TRANSACTION_REQUEST:
|
|
196
|
+
expectedResponseType = NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE;
|
|
197
|
+
break;
|
|
198
|
+
default:
|
|
199
|
+
throw new V1UnexpectedError(`Unsupported pending request type: ${pendingRequestServiceEntry.requestType}.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (payload.type !== expectedResponseType) {
|
|
203
|
+
throw new V1InvalidPayloadError(
|
|
204
|
+
`Response type mismatch for id ${pendingRequestServiceEntry.id}. Expected ${expectedResponseType}, got ${payload.type}.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default V1BaseOperation;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import V1BaseOperation from "./V1BaseOperation.js";
|
|
2
|
+
import b4a from "b4a";
|
|
3
|
+
import { MAX_PARTIAL_TX_PAYLOAD_BYTE_SIZE } from '../../../../../utils/constants.js';
|
|
4
|
+
import {V1InvalidPayloadError} from "../V1ProtocolError.js";
|
|
5
|
+
|
|
6
|
+
class V1BroadcastTransactionRequest extends V1BaseOperation {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super(config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async validate(payload, remotePublicKey) {
|
|
12
|
+
this.isPayloadSchemaValid(payload);
|
|
13
|
+
this.isDataPropertySizeValid(payload);
|
|
14
|
+
await this.validateSignature(payload, remotePublicKey);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isDataPropertySizeValid(payload) {
|
|
19
|
+
if (b4a.byteLength(payload.broadcast_transaction_request.data) > MAX_PARTIAL_TX_PAYLOAD_BYTE_SIZE) {
|
|
20
|
+
throw new V1InvalidPayloadError(`The 'data' field exceeds the maximum allowed byte size of ${MAX_PARTIAL_TX_PAYLOAD_BYTE_SIZE}. Actual size: ${b4a.byteLength(payload.broadcast_transaction_request.data)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default V1BroadcastTransactionRequest;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import Autobase from "autobase";
|
|
2
|
+
import b4a from "b4a";
|
|
3
|
+
import Hypercore from 'hypercore';
|
|
4
|
+
import V1BaseOperation from "./V1BaseOperation.js";
|
|
5
|
+
import {unsafeDecodeApplyOperation} from "../../../../../utils/protobuf/operationHelpers.js";
|
|
6
|
+
import {isDeepEqualApplyPayload} from "../../../../../utils/deepEqualApplyPayload.js";
|
|
7
|
+
import {addressToBuffer, bufferToAddress} from "../../../../state/utils/address.js";
|
|
8
|
+
import {publicKeyToAddress} from "../../../../../utils/helpers.js";
|
|
9
|
+
import Check from "../../../../../utils/check.js";
|
|
10
|
+
import {OperationType, ResultCode} from "../../../../../utils/constants.js";
|
|
11
|
+
import {V1ProtocolError} from "../V1ProtocolError.js";
|
|
12
|
+
|
|
13
|
+
const VALIDATOR_METADATA_FIELDS = new Set(["va", "vn", "vs"]);
|
|
14
|
+
|
|
15
|
+
class V1BroadcastTransactionResponse extends V1BaseOperation {
|
|
16
|
+
#config;
|
|
17
|
+
#check;
|
|
18
|
+
#state;
|
|
19
|
+
|
|
20
|
+
constructor(state, config) {
|
|
21
|
+
super(config);
|
|
22
|
+
this.#state = state;
|
|
23
|
+
this.#config = config;
|
|
24
|
+
this.#check = new Check(config);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async validate(payload, connection, pendingRequestServiceEntry) {
|
|
28
|
+
this.isPayloadSchemaValid(payload);
|
|
29
|
+
this.validateResponseType(payload, pendingRequestServiceEntry);
|
|
30
|
+
this.validatePeerCorrectness(connection.remotePublicKey, pendingRequestServiceEntry);
|
|
31
|
+
await this.validateSignature(payload, connection.remotePublicKey);
|
|
32
|
+
const resultCode = payload.broadcast_transaction_response.result;
|
|
33
|
+
// if result code is not OK, we can skip the rest of the validations.
|
|
34
|
+
this.validateIfResultCodeIsValidatorInternalError(resultCode);
|
|
35
|
+
if (resultCode === ResultCode.OK) {
|
|
36
|
+
const proofResult = await this.verifyProofOfPublication(payload);
|
|
37
|
+
const {
|
|
38
|
+
validatorDecodedTx,
|
|
39
|
+
manifest
|
|
40
|
+
} = await this.assertProofPayloadMatchesRequestPayload(proofResult, pendingRequestServiceEntry);
|
|
41
|
+
this.validateDecodedCompletePayloadSchema(validatorDecodedTx);
|
|
42
|
+
const {
|
|
43
|
+
writerKeyFromManifest,
|
|
44
|
+
validatorAddressCorrelatedWithManifest
|
|
45
|
+
} = await this.validateWritingKey(validatorDecodedTx, manifest);
|
|
46
|
+
await this.validateValidatorCorrectness(
|
|
47
|
+
validatorDecodedTx,
|
|
48
|
+
connection.remotePublicKey,
|
|
49
|
+
writerKeyFromManifest,
|
|
50
|
+
validatorAddressCorrelatedWithManifest,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validateDecodedCompletePayloadSchema(validatorDecodedTx) {
|
|
58
|
+
const type = validatorDecodedTx?.type;
|
|
59
|
+
if (!Number.isInteger(type)) {
|
|
60
|
+
throw new V1ProtocolError(
|
|
61
|
+
ResultCode.VALIDATOR_RESPONSE_TX_TYPE_INVALID,
|
|
62
|
+
'Decoded validator transaction type is missing or invalid.',
|
|
63
|
+
false
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!Object.values(OperationType).includes(type)) {
|
|
68
|
+
throw new V1ProtocolError(
|
|
69
|
+
ResultCode.VALIDATOR_RESPONSE_TX_TYPE_UNKNOWN,
|
|
70
|
+
`Decoded validator transaction type ${type} is not defined in OperationType constants.`,
|
|
71
|
+
false
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let isValid = false;
|
|
76
|
+
switch (type) {
|
|
77
|
+
case OperationType.ADD_WRITER:
|
|
78
|
+
case OperationType.REMOVE_WRITER:
|
|
79
|
+
case OperationType.ADMIN_RECOVERY:
|
|
80
|
+
isValid = this.#check.validateRoleAccessOperation(validatorDecodedTx);
|
|
81
|
+
break;
|
|
82
|
+
case OperationType.BOOTSTRAP_DEPLOYMENT:
|
|
83
|
+
isValid = this.#check.validateBootstrapDeploymentOperation(validatorDecodedTx);
|
|
84
|
+
break;
|
|
85
|
+
case OperationType.TX:
|
|
86
|
+
isValid = this.#check.validateTransactionOperation(validatorDecodedTx);
|
|
87
|
+
break;
|
|
88
|
+
case OperationType.TRANSFER:
|
|
89
|
+
isValid = this.#check.validateTransferOperation(validatorDecodedTx);
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
throw new V1ProtocolError(
|
|
93
|
+
ResultCode.VALIDATOR_RESPONSE_TX_TYPE_UNSUPPORTED,
|
|
94
|
+
`Unsupported decoded validator transaction type: ${type}`,
|
|
95
|
+
false
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isValid) {
|
|
100
|
+
throw new V1ProtocolError(
|
|
101
|
+
ResultCode.VALIDATOR_RESPONSE_SCHEMA_INVALID,
|
|
102
|
+
`Decoded validator transaction schema validation failed for type ${type}.`,
|
|
103
|
+
false
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async verifyProofOfPublication(payload) {
|
|
109
|
+
const proof = payload.broadcast_transaction_response.proof;
|
|
110
|
+
return this.#state.verifyProofOfPublication(proof);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async assertProofPayloadMatchesRequestPayload(proofResult, pendingRequestServiceEntry) {
|
|
114
|
+
const stateTxEncodedFromRequest = pendingRequestServiceEntry.requestTxData;
|
|
115
|
+
if (!b4a.isBuffer(stateTxEncodedFromRequest) || stateTxEncodedFromRequest.length === 0) {
|
|
116
|
+
throw new V1ProtocolError(
|
|
117
|
+
ResultCode.PENDING_REQUEST_MISSING_TX_DATA,
|
|
118
|
+
'Missing transaction data in pending request entry.',
|
|
119
|
+
false
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const provenBlock = proofResult.proof.block.value;
|
|
123
|
+
const manifest = proofResult.proof.manifest;
|
|
124
|
+
const stateTxEncodedFromResponse = await Autobase.decodeValue(provenBlock);
|
|
125
|
+
|
|
126
|
+
const stateTxDecodedFromRequest = unsafeDecodeApplyOperation(stateTxEncodedFromRequest);
|
|
127
|
+
const stateTxDecodedFromResponse = unsafeDecodeApplyOperation(stateTxEncodedFromResponse);
|
|
128
|
+
const strippedValidatorPayload = stripValidatorMetadata(stateTxDecodedFromResponse);
|
|
129
|
+
|
|
130
|
+
if (!isDeepEqualApplyPayload(stateTxDecodedFromRequest, strippedValidatorPayload)) {
|
|
131
|
+
throw new V1ProtocolError(
|
|
132
|
+
ResultCode.PROOF_PAYLOAD_MISMATCH,
|
|
133
|
+
'Decoded transaction payload mismatch after removing validator metadata fields.',
|
|
134
|
+
false
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return {validatorDecodedTx: stateTxDecodedFromResponse, manifest};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async validateWritingKey(validatorDecodedTx, manifest) {
|
|
141
|
+
const writerKeyFromManifest = Hypercore.key(manifest);
|
|
142
|
+
const writerKeyFromManifestHex = b4a.toString(writerKeyFromManifest, "hex");
|
|
143
|
+
|
|
144
|
+
const validatorAddressBuffer = await this.#state.getRegisteredWriterKey(writerKeyFromManifestHex);
|
|
145
|
+
if (!b4a.isBuffer(validatorAddressBuffer) || validatorAddressBuffer.length === 0) {
|
|
146
|
+
throw new V1ProtocolError(
|
|
147
|
+
ResultCode.VALIDATOR_WRITER_KEY_NOT_REGISTERED,
|
|
148
|
+
`Validator with writer key ${writerKeyFromManifestHex} is not registered.`,
|
|
149
|
+
false
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
writerKeyFromManifest,
|
|
155
|
+
validatorAddressCorrelatedWithManifest: validatorAddressBuffer
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async validateValidatorCorrectness(validatorDecodedTx, connectionRemotePublicKey, writerKeyFromManifest, validatorAddressCorrelatedWithManifest) {
|
|
160
|
+
const validatorAddressFromTx = extractRequiredVaFromDecodedTx(validatorDecodedTx);
|
|
161
|
+
const validatorAddressFromConnectionPublicKey = addressToBuffer(
|
|
162
|
+
publicKeyToAddress(connectionRemotePublicKey, this.#config),
|
|
163
|
+
this.#config.addressPrefix
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (!b4a.equals(validatorAddressFromTx, validatorAddressFromConnectionPublicKey)) {
|
|
167
|
+
throw new V1ProtocolError(
|
|
168
|
+
ResultCode.VALIDATOR_ADDRESS_MISMATCH,
|
|
169
|
+
`Validator address from transaction (${bufferToAddress(validatorAddressFromTx, this.#config.addressPrefix)}) does not match address derived from connection public key (${bufferToAddress(validatorAddressFromConnectionPublicKey, this.#config.addressPrefix)}).`,
|
|
170
|
+
false
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!b4a.equals(validatorAddressFromTx, validatorAddressCorrelatedWithManifest)) {
|
|
175
|
+
throw new V1ProtocolError(
|
|
176
|
+
ResultCode.VALIDATOR_ADDRESS_MISMATCH,
|
|
177
|
+
`Validator address from transaction (${bufferToAddress(validatorAddressFromTx, this.#config.addressPrefix)}) does not match address correlated with manifest writer key (${bufferToAddress(validatorAddressCorrelatedWithManifest, this.#config.addressPrefix)}).`,
|
|
178
|
+
false
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!b4a.equals(validatorAddressCorrelatedWithManifest, validatorAddressFromConnectionPublicKey)) {
|
|
183
|
+
throw new V1ProtocolError(
|
|
184
|
+
ResultCode.VALIDATOR_ADDRESS_MISMATCH,
|
|
185
|
+
`Validator address correlated with manifest writer key (${bufferToAddress(validatorAddressCorrelatedWithManifest, this.#config.addressPrefix)}) does not match address derived from connection public key (${bufferToAddress(validatorAddressFromConnectionPublicKey, this.#config.addressPrefix)}).`,
|
|
186
|
+
false
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const validatorAddressFromConnection = bufferToAddress(validatorAddressFromConnectionPublicKey, this.#config.addressPrefix);
|
|
191
|
+
const account = await this.#state.getNodeEntry(validatorAddressFromConnection);
|
|
192
|
+
|
|
193
|
+
if (!account) {
|
|
194
|
+
throw new V1ProtocolError(
|
|
195
|
+
ResultCode.VALIDATOR_NODE_ENTRY_NOT_FOUND,
|
|
196
|
+
`No node entry found in state for validator address derived from connection public key (${validatorAddressFromConnection}).`,
|
|
197
|
+
false
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!account.isWriter) {
|
|
202
|
+
throw new V1ProtocolError(
|
|
203
|
+
ResultCode.VALIDATOR_NODE_NOT_WRITER,
|
|
204
|
+
`Node entry found for validator address derived from connection public key (${validatorAddressFromConnection}), but it is not registered as a writer.`,
|
|
205
|
+
false
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!b4a.isBuffer(account.wk) || !b4a.equals(account.wk, writerKeyFromManifest)) {
|
|
210
|
+
throw new V1ProtocolError(
|
|
211
|
+
ResultCode.VALIDATOR_WRITER_KEY_MISMATCH,
|
|
212
|
+
`Writer key from manifest (${b4a.toString(writerKeyFromManifest, "hex")}) does not match writer key in state for validator address derived from connection public key (${validatorAddressFromConnection}).`,
|
|
213
|
+
false
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
validateIfResultCodeIsValidatorInternalError(resultCode) {
|
|
219
|
+
if (resultCode === ResultCode.TX_COMMITTED_RECEIPT_MISSING) {
|
|
220
|
+
throw new V1ProtocolError(
|
|
221
|
+
resultCode,
|
|
222
|
+
`Validator response indicates an error with result code ${resultCode}, which is an internal error code.`,
|
|
223
|
+
true
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const stripValidatorMetadata = (value) => {
|
|
230
|
+
|
|
231
|
+
if (value === null || value === undefined) return value;
|
|
232
|
+
if (b4a.isBuffer(value)) return value;
|
|
233
|
+
if (Array.isArray(value)) return value.map(stripValidatorMetadata);
|
|
234
|
+
if (typeof value !== "object") return value;
|
|
235
|
+
|
|
236
|
+
const result = {};
|
|
237
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
238
|
+
if (VALIDATOR_METADATA_FIELDS.has(key)) {
|
|
239
|
+
result[key] = null;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
result[key] = stripValidatorMetadata(nestedValue);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export function extractRequiredVaFromDecodedTx(validatorDecodedTx) {
|
|
248
|
+
if (!validatorDecodedTx || typeof validatorDecodedTx !== 'object') {
|
|
249
|
+
throw new V1ProtocolError(
|
|
250
|
+
ResultCode.VALIDATOR_TX_OBJECT_INVALID,
|
|
251
|
+
'Invalid decoded transaction: expected object',
|
|
252
|
+
false
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const operationPayload = Object.values(validatorDecodedTx).find((value) =>
|
|
257
|
+
value &&
|
|
258
|
+
typeof value === 'object' &&
|
|
259
|
+
!Array.isArray(value) &&
|
|
260
|
+
!b4a.isBuffer(value) &&
|
|
261
|
+
Object.prototype.hasOwnProperty.call(value, 'va')
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const va = operationPayload?.va;
|
|
265
|
+
|
|
266
|
+
if (!b4a.isBuffer(va)) {
|
|
267
|
+
throw new V1ProtocolError(
|
|
268
|
+
ResultCode.VALIDATOR_VA_MISSING,
|
|
269
|
+
'Missing validator address (va) in decoded transaction',
|
|
270
|
+
false
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return va;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default V1BroadcastTransactionResponse;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import V1BaseOperation from "./V1BaseOperation.js";
|
|
2
|
+
|
|
3
|
+
class V1LivenessRequest extends V1BaseOperation {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super(config);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async validate(payload, remotePublicKey) {
|
|
9
|
+
this.isPayloadSchemaValid(payload);
|
|
10
|
+
await this.validateSignature(payload, remotePublicKey);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default V1LivenessRequest;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import V1BaseOperation from "./V1BaseOperation.js";
|
|
2
|
+
|
|
3
|
+
class V1LivenessResponse extends V1BaseOperation {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super(config);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async validate(payload, connection, pendingRequestServiceEntry) {
|
|
9
|
+
this.isPayloadSchemaValid(payload);
|
|
10
|
+
this.validateResponseType(payload, pendingRequestServiceEntry);
|
|
11
|
+
this.validatePeerCorrectness(connection.remotePublicKey, pendingRequestServiceEntry);
|
|
12
|
+
await this.validateSignature(payload, connection.remotePublicKey);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default V1LivenessResponse;
|