trac-msb 0.2.9 → 0.2.11
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/CODE_OF_CONDUCT.md +128 -0
- package/README.md +33 -18
- package/docker-compose.yml +1 -0
- package/docs/trac_network_http_api.openapi.yaml +889 -0
- package/msb.mjs +4 -21
- package/package.json +16 -12
- 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/handlers.js +163 -90
- package/rpc/routes/v1.js +3 -1
- package/rpc/rpc_server.js +3 -3
- package/rpc/rpc_services.js +45 -31
- package/rpc/utils/helpers.js +82 -51
- package/scripts/generate-protobufs.js +37 -12
- package/src/config/args.js +46 -0
- package/src/config/config.js +99 -5
- package/src/config/env.js +86 -7
- package/src/core/network/Network.js +79 -46
- 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 +26 -20
- package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +25 -15
- 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} +20 -13
- package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +29 -18
- package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +18 -12
- package/src/core/network/protocols/legacy/validators/base/BaseResponse.js +1 -1
- 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 +147 -95
- package/src/core/network/services/MessageOrchestrator.js +152 -28
- 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 +133 -22
- package/src/core/network/services/TransactionRateLimiterService.js +57 -42
- package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
- package/src/core/network/services/ValidatorObserverService.js +23 -32
- package/src/core/state/State.js +72 -22
- package/src/index.js +8 -5
- package/src/messages/network/v1/NetworkMessageBuilder.js +61 -81
- package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
- package/src/messages/state/ApplyStateMessageBuilder.js +1 -1
- package/src/utils/Scheduler.js +0 -8
- package/src/utils/check.js +1 -1
- package/src/utils/constants.js +68 -19
- package/src/utils/deepEqualApplyPayload.js +40 -0
- package/src/utils/fileUtils.js +13 -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/src/utils/type.js +26 -0
- package/tests/acceptance/v1/account/account.test.mjs +8 -2
- package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
- package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
- package/tests/acceptance/v1/health/health.test.mjs +33 -0
- package/tests/acceptance/v1/rpc.test.mjs +3 -2
- package/tests/acceptance/v1/tx/tx.test.mjs +50 -17
- package/tests/acceptance/v1/tx-details/tx-details.test.mjs +60 -18
- package/tests/fixtures/check.fixtures.js +33 -32
- package/tests/fixtures/networkV1.fixtures.js +2 -27
- package/tests/fixtures/protobuf.fixtures.js +33 -32
- package/tests/helpers/StateNetworkFactory.js +2 -2
- package/tests/helpers/address.js +6 -0
- package/tests/helpers/autobaseTestHelpers.js +2 -1
- package/tests/helpers/config.js +2 -1
- package/tests/helpers/setupApplyTests.js +6 -10
- package/tests/helpers/transactionPayloads.mjs +2 -2
- package/tests/unit/messages/network/NetworkMessageBuilder.test.js +241 -81
- package/tests/unit/messages/network/NetworkMessageDirector.test.js +225 -81
- 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/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
- package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
- package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
- package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
- package/tests/unit/utils/type/type.test.js +25 -0
- package/tests/unit/utils/utils.test.js +2 -0
- package/.github/workflows/acceptance-tests.yml +0 -42
- package/.github/workflows/publish.yml +0 -33
- package/.github/workflows/unit-tests.yml +0 -40
- 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,172 @@
|
|
|
1
|
+
import {NetworkOperationType, ResultCode} from '../../../utils/constants.js';
|
|
2
|
+
import {isHexString} from '../../../utils/helpers.js';
|
|
3
|
+
import {V1ProtocolError, V1TimeoutError, V1UnexpectedError} from "../protocols/v1/V1ProtocolError.js";
|
|
4
|
+
import {Config} from '../../../config/config.js';
|
|
5
|
+
import b4a from 'b4a';
|
|
6
|
+
|
|
7
|
+
const PEER_PUBLIC_KEY_HEX_LENGTH = 64;
|
|
8
|
+
|
|
9
|
+
class PendingRequestService {
|
|
10
|
+
#pendingRequests;
|
|
11
|
+
#requestMessageTypes = [NetworkOperationType.LIVENESS_REQUEST, NetworkOperationType.BROADCAST_TRANSACTION_REQUEST];
|
|
12
|
+
#config;
|
|
13
|
+
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.#pendingRequests = new Map(); // Map<id, pendingRequestEntry>
|
|
16
|
+
this.#config = config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
has(id) {
|
|
20
|
+
return this.#pendingRequests.has(id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
isProbePending(peerPubKeyHex) {
|
|
24
|
+
for (const [, entry] of this.#pendingRequests) {
|
|
25
|
+
if (entry.requestedTo === peerPubKeyHex && entry.requestType === NetworkOperationType.LIVENESS_REQUEST) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#validateRegisterInput(peerPubKeyHex, message) {
|
|
33
|
+
if (!isHexString(peerPubKeyHex) || peerPubKeyHex.length !== PEER_PUBLIC_KEY_HEX_LENGTH) {
|
|
34
|
+
throw new Error('Invalid peer public key. Expected 32-byte hex string.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!message || typeof message !== 'object') {
|
|
38
|
+
throw new Error('Pending request message must be an object.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof message.id !== 'string' || message.id.length === 0) {
|
|
42
|
+
throw new Error('Pending request ID must be a non-empty string.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!this.#requestMessageTypes.includes(message.type)) {
|
|
46
|
+
throw new Error('Unsupported pending request type.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
@returns {Promise}
|
|
52
|
+
*/
|
|
53
|
+
registerPendingRequest(peerPubKeyHex, message) {
|
|
54
|
+
this.#validateRegisterInput(peerPubKeyHex, message);
|
|
55
|
+
const id = message.id;
|
|
56
|
+
if (this.#pendingRequests.size >= this.#config.maxPendingRequestsInPendingRequestsService) {
|
|
57
|
+
throw new Error('Maximum number of pending requests reached.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (this.#pendingRequests.has(id)) {
|
|
61
|
+
throw new Error(`Pending request with ID ${id} from peer ${peerPubKeyHex} already exists.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entry = {
|
|
65
|
+
id: id,
|
|
66
|
+
requestType: message.type,
|
|
67
|
+
requestTxData: this.#extractRequestTxData(message),
|
|
68
|
+
requestedTo: peerPubKeyHex,
|
|
69
|
+
timeoutMs: this.#config.pendingRequestTimeout,
|
|
70
|
+
timeoutId: null,
|
|
71
|
+
resolve: null,
|
|
72
|
+
reject: null,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const promise = new Promise((resolve, reject) => {
|
|
76
|
+
entry.resolve = resolve;
|
|
77
|
+
entry.reject = reject;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
entry.timeoutId = setTimeout(() => {
|
|
81
|
+
this.rejectPendingRequest(
|
|
82
|
+
id,
|
|
83
|
+
new V1TimeoutError(
|
|
84
|
+
`Pending request with ID ${id} from peer ${peerPubKeyHex} timed out after ${entry.timeoutMs} ms.`,
|
|
85
|
+
true
|
|
86
|
+
));
|
|
87
|
+
|
|
88
|
+
}, entry.timeoutMs);
|
|
89
|
+
|
|
90
|
+
this.#pendingRequests.set(id, entry);
|
|
91
|
+
return promise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#extractRequestTxData(message) {
|
|
95
|
+
if (message.type !== NetworkOperationType.BROADCAST_TRANSACTION_REQUEST) return null;
|
|
96
|
+
const txData = message.broadcast_transaction_request?.data;
|
|
97
|
+
return b4a.isBuffer(txData) ? txData : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getAndDeletePendingRequest(id) {
|
|
101
|
+
const entry = this.#pendingRequests.get(id);
|
|
102
|
+
if (!entry) return null;
|
|
103
|
+
|
|
104
|
+
clearTimeout(entry.timeoutId);
|
|
105
|
+
this.#pendingRequests.delete(id);
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getPendingRequest(id) {
|
|
110
|
+
const entry = this.#pendingRequests.get(id);
|
|
111
|
+
if (!entry) return null;
|
|
112
|
+
return entry;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// for now, we are resolving only resultCode, but we can extend it in the future if needed...
|
|
116
|
+
resolvePendingRequest(id, resultCode = ResultCode.OK) {
|
|
117
|
+
const entry = this.getAndDeletePendingRequest(id);
|
|
118
|
+
if (!entry) return false;
|
|
119
|
+
entry.resolve(resultCode);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
rejectPendingRequest(id, error) {
|
|
124
|
+
const entry = this.getAndDeletePendingRequest(id);
|
|
125
|
+
if (!entry) return false;
|
|
126
|
+
const err = error instanceof V1ProtocolError
|
|
127
|
+
? error
|
|
128
|
+
: new V1UnexpectedError(error?.message ?? 'Unexpected error');
|
|
129
|
+
entry.reject(err);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
rejectPendingRequestsForPeer(peerPubKeyHex, error) {
|
|
134
|
+
const idsToReject = [];
|
|
135
|
+
for (const [id, entry] of this.#pendingRequests) {
|
|
136
|
+
if (entry.requestedTo === peerPubKeyHex) idsToReject.push(id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const id of idsToReject) {
|
|
140
|
+
this.rejectPendingRequest(id, error);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return idsToReject.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stopPendingRequestTimeout(id) {
|
|
147
|
+
const entry = this.#pendingRequests.get(id);
|
|
148
|
+
if (!entry) return false;
|
|
149
|
+
|
|
150
|
+
clearTimeout(entry.timeoutId);
|
|
151
|
+
entry.timeoutId = null;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
close() {
|
|
156
|
+
for (const [id, entry] of this.#pendingRequests) {
|
|
157
|
+
clearTimeout(entry.timeoutId);
|
|
158
|
+
try {
|
|
159
|
+
entry.reject(
|
|
160
|
+
new V1UnexpectedError(
|
|
161
|
+
`Pending request ${id} cancelled (shutdown).`,
|
|
162
|
+
false)
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`PendingRequestService.close: failed to reject pending request ${id}:`, error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
this.#pendingRequests.clear();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default PendingRequestService;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {isHexString} from '../../../utils/helpers.js';
|
|
2
|
+
import {TRANSACTION_COMMIT_SERVICE_BUFFER_SIZE} from '../../../utils/constants.js';
|
|
3
|
+
|
|
4
|
+
const TX_HASH_HEX_STRING_LENGTH = 64;
|
|
5
|
+
|
|
6
|
+
class TransactionCommitService {
|
|
7
|
+
#pendingCommits;
|
|
8
|
+
#config;
|
|
9
|
+
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.#pendingCommits = new Map(); // Map<txHash, pendingCommitEntry>
|
|
12
|
+
this.#config = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#assertTxHash(txHash) {
|
|
16
|
+
if (!isHexString(txHash) || txHash.length !== TX_HASH_HEX_STRING_LENGTH) {
|
|
17
|
+
throw new PendingCommitInvalidTxHashError(txHash);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
has(txHash) {
|
|
22
|
+
this.#assertTxHash(txHash);
|
|
23
|
+
return this.#pendingCommits.has(txHash);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
@returns {Promise}
|
|
28
|
+
*/
|
|
29
|
+
registerPendingCommit(txHash) {
|
|
30
|
+
this.#assertTxHash(txHash);
|
|
31
|
+
|
|
32
|
+
if (this.#pendingCommits.size >= TRANSACTION_COMMIT_SERVICE_BUFFER_SIZE) {
|
|
33
|
+
throw new PendingCommitBufferFullError(TRANSACTION_COMMIT_SERVICE_BUFFER_SIZE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (this.#pendingCommits.has(txHash)) {
|
|
37
|
+
throw new PendingCommitAlreadyExistsError(txHash);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const timeoutMs = this.#config.txCommitTimeout;
|
|
41
|
+
|
|
42
|
+
const entry = {
|
|
43
|
+
txHash,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
timeoutId: null,
|
|
46
|
+
resolve: null,
|
|
47
|
+
reject: null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const promise = new Promise((resolve, reject) => {
|
|
51
|
+
entry.resolve = resolve;
|
|
52
|
+
entry.reject = reject;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
entry.timeoutId = setTimeout(() => {
|
|
56
|
+
this.rejectPendingCommit(
|
|
57
|
+
txHash,
|
|
58
|
+
new PendingCommitTimeoutError(txHash, timeoutMs)
|
|
59
|
+
);
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
|
|
62
|
+
this.#pendingCommits.set(txHash, entry);
|
|
63
|
+
return promise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getAndDeletePendingCommit(txHash) {
|
|
67
|
+
this.#assertTxHash(txHash);
|
|
68
|
+
const entry = this.#pendingCommits.get(txHash);
|
|
69
|
+
if (!entry) return null;
|
|
70
|
+
|
|
71
|
+
clearTimeout(entry.timeoutId);
|
|
72
|
+
this.#pendingCommits.delete(txHash);
|
|
73
|
+
return entry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resolvePendingCommit(txHash, receipt = null) {
|
|
77
|
+
this.#assertTxHash(txHash);
|
|
78
|
+
const entry = this.getAndDeletePendingCommit(txHash);
|
|
79
|
+
if (!entry) return false;
|
|
80
|
+
entry.resolve(receipt);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rejectPendingCommit(txHash, error) {
|
|
85
|
+
this.#assertTxHash(txHash);
|
|
86
|
+
const entry = this.getAndDeletePendingCommit(txHash);
|
|
87
|
+
if (!entry) return false;
|
|
88
|
+
|
|
89
|
+
entry.reject(
|
|
90
|
+
error instanceof Error
|
|
91
|
+
? error
|
|
92
|
+
: new PendingCommitUnexpectedError('Unexpected commit error')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
close() {
|
|
99
|
+
for (const [txHash, entry] of this.#pendingCommits) {
|
|
100
|
+
clearTimeout(entry.timeoutId);
|
|
101
|
+
try {
|
|
102
|
+
entry.reject(
|
|
103
|
+
new PendingCommitCancelledError(txHash)
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(`TransactionCommitService.close: failed to reject pending commit ${txHash}:`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.#pendingCommits.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default TransactionCommitService;
|
|
114
|
+
|
|
115
|
+
export class PendingCommitInvalidTxHashError extends Error {
|
|
116
|
+
constructor(txHash) {
|
|
117
|
+
super(`Invalid txHash format: ${txHash}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class PendingCommitBufferFullError extends Error {
|
|
122
|
+
constructor(limit) {
|
|
123
|
+
super(`Maximum number of pending commits reached (limit=${limit}).`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class PendingCommitAlreadyExistsError extends Error {
|
|
128
|
+
constructor(txHash) {
|
|
129
|
+
super(`Pending commit for txHash ${txHash} already exists.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class PendingCommitTimeoutError extends Error {
|
|
134
|
+
constructor(txHash, timeoutMs) {
|
|
135
|
+
super(`Pending commit for txHash ${txHash} timed out after ${timeoutMs} ms.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class PendingCommitCancelledError extends Error {
|
|
140
|
+
constructor(txHash) {
|
|
141
|
+
super(`Pending commit ${txHash} cancelled (shutdown).`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class PendingCommitUnexpectedError extends Error {
|
|
146
|
+
constructor(message = 'Unexpected commit error') {
|
|
147
|
+
super(message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,37 +1,40 @@
|
|
|
1
1
|
// PoolService.js
|
|
2
|
-
import { BATCH_SIZE
|
|
2
|
+
import { BATCH_SIZE } from '../../../utils/constants.js';
|
|
3
3
|
import Scheduler from '../../../utils/Scheduler.js';
|
|
4
|
+
import Denque from "denque";
|
|
5
|
+
import b4a from "b4a";
|
|
4
6
|
|
|
5
7
|
class TransactionPoolService {
|
|
6
8
|
#state;
|
|
7
9
|
#address;
|
|
8
10
|
#config;
|
|
9
|
-
#
|
|
11
|
+
#txPool = new Denque();
|
|
12
|
+
#transactionCommitService
|
|
10
13
|
#scheduler = null;
|
|
14
|
+
#queuedTxHashes
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* @param {State} state
|
|
14
18
|
* @param {string} address
|
|
15
|
-
* @param {
|
|
19
|
+
* @param {TransactionCommitService} transactionCommitService
|
|
20
|
+
* @param {Config} config
|
|
16
21
|
**/
|
|
17
|
-
constructor(state, address, config) {
|
|
22
|
+
constructor(state, address, transactionCommitService, config) {
|
|
18
23
|
this.#state = state;
|
|
19
24
|
this.#address = address;
|
|
25
|
+
this.#transactionCommitService = transactionCommitService;
|
|
26
|
+
this.#queuedTxHashes = new Set(); // to improve lookup performance when checking for duplicate transactions
|
|
20
27
|
this.#config = config;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
get
|
|
24
|
-
return this.#
|
|
30
|
+
get txPool() {
|
|
31
|
+
return this.#txPool;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
get state() {
|
|
28
35
|
return this.#state;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
get address() {
|
|
32
|
-
return this.#address;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
38
|
async start() {
|
|
36
39
|
if (!this.#config.enableWallet) {
|
|
37
40
|
console.info('TransactionPoolService can not start. Wallet is not enabled');
|
|
@@ -49,10 +52,10 @@ class TransactionPoolService {
|
|
|
49
52
|
async #worker(next) {
|
|
50
53
|
try {
|
|
51
54
|
await this.#processTransactions();
|
|
52
|
-
if (this.#
|
|
55
|
+
if (this.#txPool.size() > 0) {
|
|
53
56
|
next(0);
|
|
54
57
|
} else {
|
|
55
|
-
next(
|
|
58
|
+
next(this.#config.processIntervalMs);
|
|
56
59
|
}
|
|
57
60
|
} catch (error) {
|
|
58
61
|
throw new Error(`TransactionPoolService worker error: ${error.message}`);
|
|
@@ -60,40 +63,148 @@ class TransactionPoolService {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
#createScheduler() {
|
|
63
|
-
return new Scheduler((next) => this.#worker(next),
|
|
66
|
+
return new Scheduler((next) => this.#worker(next), this.#config.processIntervalMs);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
async #processTransactions() {
|
|
67
70
|
const canValidate = await this.#checkValidationPermissions();
|
|
68
|
-
if (canValidate
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
if (!canValidate || this.#txPool.size() === 0) return;
|
|
72
|
+
|
|
73
|
+
const batchItems = this.#prepareBatch();
|
|
74
|
+
const encodedBatch = batchItems.map(item => item.encodedTx);
|
|
75
|
+
const batchTxHashes = batchItems.map(item => item.txHash);
|
|
76
|
+
try {
|
|
77
|
+
const receipts = await this.#state.appendWithProofOfPublication(encodedBatch, batchTxHashes);
|
|
78
|
+
|
|
79
|
+
const receiptsByHash = new Map();
|
|
80
|
+
for (const receipt of receipts) {
|
|
81
|
+
if (receipt.txHash) receiptsByHash.set(receipt.txHash, receipt);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const item of batchItems) {
|
|
85
|
+
const receipt = receiptsByHash.get(item.txHash);
|
|
86
|
+
|
|
87
|
+
if (!receipt) {
|
|
88
|
+
this.#transactionCommitService.rejectPendingCommit(
|
|
89
|
+
item.txHash,
|
|
90
|
+
new TransactionPoolMissingCommitReceiptError(item.txHash)
|
|
91
|
+
);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!receipt.proof) {
|
|
96
|
+
this.#transactionCommitService.rejectPendingCommit(
|
|
97
|
+
item.txHash,
|
|
98
|
+
new TransactionPoolProofUnavailableError(
|
|
99
|
+
item.txHash,
|
|
100
|
+
receipt.blockNumber,
|
|
101
|
+
receipt.proofError,
|
|
102
|
+
receipt.timestamp
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.#transactionCommitService.resolvePendingCommit(item.txHash, receipt);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
for (const item of batchItems) {
|
|
112
|
+
this.#transactionCommitService.rejectPendingCommit(item.txHash, error);
|
|
113
|
+
}
|
|
114
|
+
console.error(
|
|
115
|
+
`TransactionPoolService: failed to process batch (size=${batchItems.length}): ${error?.message ?? 'unknown error'}`
|
|
116
|
+
);
|
|
71
117
|
}
|
|
72
118
|
}
|
|
73
119
|
|
|
74
120
|
async #checkValidationPermissions() {
|
|
75
121
|
const isAdminAllowedToValidate = await this.state.isAdminAllowedToValidate();
|
|
76
|
-
const isNodeAllowedToValidate = await this.state.allowedToValidate(this
|
|
122
|
+
const isNodeAllowedToValidate = await this.state.allowedToValidate(this.#address);
|
|
77
123
|
return isNodeAllowedToValidate || isAdminAllowedToValidate;
|
|
78
124
|
}
|
|
79
125
|
|
|
80
126
|
#prepareBatch() {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
127
|
+
const batch = [];
|
|
128
|
+
const batchSize = Math.min(this.#txPool.size(), BATCH_SIZE);
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < batchSize; i++) {
|
|
131
|
+
const tx = this.#txPool.shift();
|
|
132
|
+
this.#queuedTxHashes.delete(tx.txHash);
|
|
133
|
+
batch.push(tx);
|
|
134
|
+
}
|
|
84
135
|
return batch;
|
|
85
136
|
}
|
|
86
137
|
|
|
87
|
-
addTransaction(
|
|
88
|
-
this.
|
|
138
|
+
addTransaction(txHash, encodedTx) {
|
|
139
|
+
this.validateEnqueue();
|
|
140
|
+
if (!txHash || !encodedTx || typeof txHash !== 'string' || !b4a.isBuffer(encodedTx)) {
|
|
141
|
+
throw new TransactionPoolInvalidIncomingDataError()
|
|
142
|
+
}
|
|
143
|
+
if (this.hasTransaction(txHash)) {
|
|
144
|
+
throw new TransactionPoolAlreadyQueuedError(txHash);
|
|
145
|
+
}
|
|
146
|
+
this.#queuedTxHashes.add(txHash);
|
|
147
|
+
const txData = { txHash, encodedTx };
|
|
148
|
+
this.txPool.push(txData);
|
|
89
149
|
}
|
|
90
150
|
|
|
91
151
|
async stopPool(waitForCurrent = true) {
|
|
92
152
|
if (!this.#scheduler) return;
|
|
93
153
|
await this.#scheduler.stop(waitForCurrent);
|
|
94
154
|
this.#scheduler = null;
|
|
155
|
+
this.#queuedTxHashes.clear();
|
|
156
|
+
this.#txPool.clear();
|
|
95
157
|
console.info('TransactionPoolService: closing gracefully...');
|
|
96
158
|
}
|
|
159
|
+
|
|
160
|
+
validateEnqueue() {
|
|
161
|
+
if (this.#txPool.size() >= this.#config.txPoolSize) {
|
|
162
|
+
throw new TransactionPoolFullError(this.#config.txPoolSize);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hasTransaction(txHash) {
|
|
167
|
+
return this.#queuedTxHashes.has(txHash);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class TransactionPoolProofUnavailableError extends Error {
|
|
172
|
+
constructor(txHash, blockNumber, reason = 'unknown', timestamp = 0) {
|
|
173
|
+
const timestampValue = timestamp instanceof Date ? timestamp.getTime() : timestamp;
|
|
174
|
+
const safeTimestamp = Number.isSafeInteger(timestampValue) ? timestampValue : 0;
|
|
175
|
+
super(`Proof unavailable for txHash ${txHash} at block ${blockNumber} at ${safeTimestamp}. Reason: ${reason}`);
|
|
176
|
+
this.txHash = txHash;
|
|
177
|
+
this.blockNumber = blockNumber;
|
|
178
|
+
this.timestamp = safeTimestamp;
|
|
179
|
+
this.reason = reason;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export class TransactionPoolMissingCommitReceiptError extends Error {
|
|
184
|
+
constructor(txHash) {
|
|
185
|
+
super(`Missing commit receipt for txHash ${txHash}`);
|
|
186
|
+
this.txHash = txHash;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class TransactionPoolInvalidIncomingDataError extends Error {
|
|
191
|
+
constructor(message = 'Invalid transaction pool incoming data') {
|
|
192
|
+
super(message);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class TransactionPoolFullError extends Error {
|
|
197
|
+
constructor(maxSize) {
|
|
198
|
+
super(`Transaction pool is full. Maximum size of ${maxSize} reached.`);
|
|
199
|
+
this.maxSize = maxSize
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export class TransactionPoolAlreadyQueuedError extends Error {
|
|
205
|
+
constructor(txHash) {
|
|
206
|
+
super(`Transaction with hash ${txHash} is already queued in the transaction pool.`);
|
|
207
|
+
}
|
|
97
208
|
}
|
|
98
209
|
|
|
99
210
|
export default TransactionPoolService;
|