trac-msb 0.2.21 → 0.2.24
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 +2 -2
- package/src/config/config.js +5 -0
- package/src/config/env.js +10 -7
- package/src/core/network/Network.js +6 -2
- package/src/core/network/services/ConnectionManager.js +49 -3
- package/src/core/network/services/ValidatorObserverService.js +4 -3
- package/src/core/state/State.js +7 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trac-msb",
|
|
3
3
|
"main": "msb.mjs",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.24",
|
|
5
5
|
"pear": {
|
|
6
6
|
"name": "trac-msb",
|
|
7
7
|
"type": "terminal"
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"hyperbee": "2.26.5",
|
|
55
55
|
"hypercore": "11.18.3",
|
|
56
56
|
"hypercore-crypto": "3.6.1",
|
|
57
|
-
"hyperdht": "6.
|
|
57
|
+
"hyperdht": "6.29.6",
|
|
58
58
|
"hyperswarm": "4.14.2",
|
|
59
59
|
"hyperschema": "1.17.1",
|
|
60
60
|
"lodash": "^4.18.1",
|
package/src/config/config.js
CHANGED
|
@@ -214,6 +214,11 @@ export class Config {
|
|
|
214
214
|
return this.#config.validatorHealthCheckInterval
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
get validatorHealthCheckFailureThreshold() {
|
|
218
|
+
if (this.#isOverriden('validatorHealthCheckFailureThreshold')) return this.#options.validatorHealthCheckFailureThreshold
|
|
219
|
+
return this.#config.validatorHealthCheckFailureThreshold
|
|
220
|
+
}
|
|
221
|
+
|
|
217
222
|
get maxPendingRequestsInPendingRequestsService() {
|
|
218
223
|
return this.#config.maxPendingRequestsInPendingRequestsService
|
|
219
224
|
}
|
package/src/config/env.js
CHANGED
|
@@ -52,10 +52,11 @@ const configData = {
|
|
|
52
52
|
rateLimitConnectionTimeoutMs: 60_000, // Rate limiting constants
|
|
53
53
|
rateLimitMaxTransactionsPerSecond: 50, // Rate limiting constants
|
|
54
54
|
maxPendingRequestsInPendingRequestsService: 50_000, // Maximum number of pending requests in PendingRequestService (This value should not exceed 256MB)
|
|
55
|
-
pendingRequestTimeout:
|
|
56
|
-
txCommitTimeout:
|
|
55
|
+
pendingRequestTimeout: 10_000, // Deadline for a pending V1 response (ms), including health checks
|
|
56
|
+
txCommitTimeout: 5_000, // Wait for local append and publication proof (ms); shorter than the response deadline
|
|
57
57
|
txPoolSize: 1000, // size of transaction pool
|
|
58
58
|
validatorHealthCheckInterval: 5 * 60 * 1000, // How often to check validator health (ms)
|
|
59
|
+
validatorHealthCheckFailureThreshold: 3, // Consecutive failed health checks before a validator is removed
|
|
59
60
|
storesDirectory: 'stores/',
|
|
60
61
|
storeName: 'testnet',
|
|
61
62
|
},
|
|
@@ -101,10 +102,11 @@ const configData = {
|
|
|
101
102
|
rateLimitConnectionTimeoutMs: 60_000, // Rate limiting constants
|
|
102
103
|
rateLimitMaxTransactionsPerSecond: 50, // Rate limiting constants
|
|
103
104
|
maxPendingRequestsInPendingRequestsService: 50_000, // Maximum number of pending requests in PendingRequestService (This value should not exceed 256MB)
|
|
104
|
-
pendingRequestTimeout:
|
|
105
|
-
txCommitTimeout:
|
|
105
|
+
pendingRequestTimeout: 10_000, // Deadline for a pending V1 response (ms), including health checks
|
|
106
|
+
txCommitTimeout: 5_000, // Wait for local append and publication proof (ms); shorter than the response deadline
|
|
106
107
|
txPoolSize: 1000, // size of transaction pool
|
|
107
108
|
validatorHealthCheckInterval: 5 * 60 * 1000, // How often to check validator health (ms)
|
|
109
|
+
validatorHealthCheckFailureThreshold: 3, // Consecutive failed health checks before a validator is removed
|
|
108
110
|
storesDirectory: 'stores/',
|
|
109
111
|
storeName: 'mainnet',
|
|
110
112
|
},
|
|
@@ -150,10 +152,11 @@ const configData = {
|
|
|
150
152
|
rateLimitConnectionTimeoutMs: 60_000, // Rate limiting constants
|
|
151
153
|
rateLimitMaxTransactionsPerSecond: 50, // Rate limiting constants
|
|
152
154
|
maxPendingRequestsInPendingRequestsService: 50_000, // Maximum number of pending requests in PendingRequestService (This value should not exceed 256MB)
|
|
153
|
-
pendingRequestTimeout:
|
|
154
|
-
txCommitTimeout:
|
|
155
|
+
pendingRequestTimeout: 10_000, // Deadline for a pending V1 response (ms), including health checks
|
|
156
|
+
txCommitTimeout: 5_000, // Wait for local append and publication proof (ms); shorter than the response deadline
|
|
155
157
|
txPoolSize: 1000, // size of transaction pool
|
|
156
158
|
validatorHealthCheckInterval: 1_000, // How often to check validator health (ms)
|
|
159
|
+
validatorHealthCheckFailureThreshold: 3, // Consecutive failed health checks before a validator is removed
|
|
157
160
|
storesDirectory: 'stores/',
|
|
158
161
|
storeName: 'development',
|
|
159
162
|
}
|
|
@@ -161,4 +164,4 @@ const configData = {
|
|
|
161
164
|
|
|
162
165
|
export const createConfig = (environment, options) => {
|
|
163
166
|
return new Config(options, configData[environment])
|
|
164
|
-
}
|
|
167
|
+
}
|
|
@@ -263,7 +263,10 @@ class Network extends ReadyResource {
|
|
|
263
263
|
new Error('Connection closed before response')
|
|
264
264
|
);
|
|
265
265
|
this.#swarm?.leavePeer(connection.remotePublicKey);
|
|
266
|
-
this
|
|
266
|
+
// only act on the connection this event belongs to
|
|
267
|
+
if (this.#validatorConnectionManager.isCurrent(publicKey, connection)) {
|
|
268
|
+
this.#validatorConnectionManager.remove(publicKey);
|
|
269
|
+
}
|
|
267
270
|
if (connection.protocolSession) {
|
|
268
271
|
try {
|
|
269
272
|
connection.protocolSession.close();
|
|
@@ -350,7 +353,8 @@ class Network extends ReadyResource {
|
|
|
350
353
|
this.#pendingConnections.set(publicKey, { type, timeoutId });
|
|
351
354
|
|
|
352
355
|
const target = b4a.from(publicKey, 'hex');
|
|
353
|
-
|
|
356
|
+
// A retained peer may have lost explicit retries after leavePeer().
|
|
357
|
+
if (!this.#swarm.peers.get(publicKey)?.explicit) {
|
|
354
358
|
this.#swarm.joinPeer(target);
|
|
355
359
|
}
|
|
356
360
|
|
|
@@ -6,6 +6,8 @@ import {Logger} from "../../../utils/logger.js";
|
|
|
6
6
|
* @typedef {import('hyperswarm').Connection} Connection
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const DEFAULT_HEALTH_CHECK_FAILURE_THRESHOLD = 3;
|
|
10
|
+
|
|
9
11
|
export class ConnectionManagerError extends Error {
|
|
10
12
|
constructor(message) {
|
|
11
13
|
super(message);
|
|
@@ -18,6 +20,8 @@ class ConnectionManager {
|
|
|
18
20
|
#maxValidators
|
|
19
21
|
#config
|
|
20
22
|
#healthCheckService
|
|
23
|
+
#healthCheckFailures
|
|
24
|
+
#healthCheckFailureThreshold
|
|
21
25
|
#boundedHealthCheckHandler
|
|
22
26
|
#logger
|
|
23
27
|
// Note: #validators is using publicKey (Buffer) as key
|
|
@@ -28,8 +32,11 @@ class ConnectionManager {
|
|
|
28
32
|
**/
|
|
29
33
|
constructor(config) {
|
|
30
34
|
this.#validators = new Map();
|
|
35
|
+
// consecutive failed health checks per validator
|
|
36
|
+
this.#healthCheckFailures = new Map();
|
|
31
37
|
this.#config = config
|
|
32
38
|
this.#maxValidators = config.maxValidators
|
|
39
|
+
this.#healthCheckFailureThreshold = this.#resolveHealthCheckFailureThreshold(config);
|
|
33
40
|
this.#boundedHealthCheckHandler = this.#healthCheckHandler.bind(this);
|
|
34
41
|
this.#logger = new Logger(config)
|
|
35
42
|
}
|
|
@@ -98,10 +105,15 @@ class ConnectionManager {
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
if (!success) {
|
|
101
|
-
|
|
102
|
-
this.
|
|
103
|
-
this.#
|
|
108
|
+
const failures = this.#countHealthCheckFailure(publicKey);
|
|
109
|
+
this.#logger.debug(`healthCheck: liveness request failed. Address = ${targetAddress}; Request ID = ${requestId}; Consecutive failures = ${failures}`);
|
|
110
|
+
if (failures >= this.#healthCheckFailureThreshold) {
|
|
111
|
+
this.#logger.debug(`healthCheck: failure threshold reached, removing validator. Address = ${targetAddress}; Request ID = ${requestId}`);
|
|
112
|
+
this.remove(publicKey);
|
|
113
|
+
this.#stopHealthCheck(publicKey);
|
|
114
|
+
}
|
|
104
115
|
} else {
|
|
116
|
+
this.#clearHealthCheckFailures(publicKey);
|
|
105
117
|
this.#logger.debug(`healthCheck: success. Address = ${targetAddress}; Request ID = ${requestId}`);
|
|
106
118
|
}
|
|
107
119
|
};
|
|
@@ -123,6 +135,23 @@ class ConnectionManager {
|
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
#resolveHealthCheckFailureThreshold(config) {
|
|
139
|
+
const threshold = Number(config.validatorHealthCheckFailureThreshold);
|
|
140
|
+
if (!Number.isInteger(threshold) || threshold < 1) return DEFAULT_HEALTH_CHECK_FAILURE_THRESHOLD;
|
|
141
|
+
return threshold;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#countHealthCheckFailure(publicKey) {
|
|
145
|
+
const publicKeyHex = this.#toHexString(publicKey);
|
|
146
|
+
const failures = (this.#healthCheckFailures.get(publicKeyHex) || 0) + 1;
|
|
147
|
+
this.#healthCheckFailures.set(publicKeyHex, failures);
|
|
148
|
+
return failures;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#clearHealthCheckFailures(publicKey) {
|
|
152
|
+
this.#healthCheckFailures.delete(this.#toHexString(publicKey));
|
|
153
|
+
}
|
|
154
|
+
|
|
126
155
|
/**
|
|
127
156
|
* Retrieves the Hyperswarm connection object for a given validator public key.
|
|
128
157
|
* @param {String | Buffer} publicKey - The public key (Buffer or hex string) of the validator.
|
|
@@ -134,6 +163,18 @@ class ConnectionManager {
|
|
|
134
163
|
return entry ? entry.connection : undefined;
|
|
135
164
|
}
|
|
136
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Checks whether a connection is the one currently tracked for a validator.
|
|
168
|
+
* @param {String | Buffer} publicKey - The public key (Buffer or hex string) of the validator.
|
|
169
|
+
* @param {Object} connection - The connection object to compare with the tracked one.
|
|
170
|
+
* @returns {Boolean} - Returns true if the tracked connection is the given connection, false otherwise.
|
|
171
|
+
*/
|
|
172
|
+
isCurrent(publicKey, connection) {
|
|
173
|
+
const publicKeyHex = this.#toHexString(publicKey);
|
|
174
|
+
const entry = this.#validators.get(publicKeyHex);
|
|
175
|
+
return !!entry && entry.connection === connection;
|
|
176
|
+
}
|
|
177
|
+
|
|
137
178
|
/**
|
|
138
179
|
* Sends a message through a specific validator without increasing sent messages count.
|
|
139
180
|
* @param {Object} message - The message to send to the validator.
|
|
@@ -194,6 +235,7 @@ class ConnectionManager {
|
|
|
194
235
|
this.#logger.debug(`remove: removing validator ${publicKeyToAddress(publicKey, this.#config)}`);
|
|
195
236
|
const publicKeyHex = this.#toHexString(publicKey);
|
|
196
237
|
this.#stopHealthCheck(publicKeyHex);
|
|
238
|
+
this.#clearHealthCheckFailures(publicKeyHex);
|
|
197
239
|
if (this.exists(publicKeyHex)) {
|
|
198
240
|
const entry = this.#validators.get(publicKeyHex);
|
|
199
241
|
if (endConnection && entry && entry.connection && typeof entry.connection.end === 'function') {
|
|
@@ -326,8 +368,11 @@ class ConnectionManager {
|
|
|
326
368
|
return;
|
|
327
369
|
}
|
|
328
370
|
this.#validators.set(publicKeyHex, {connection, sent: 0});
|
|
371
|
+
this.#clearHealthCheckFailures(publicKeyHex);
|
|
329
372
|
connection.on('close', () => {
|
|
330
373
|
this.#logger.debug(`#append: connection closing for validator ${publicKeyToAddress(publicKey, this.#config)}`);
|
|
374
|
+
// only act on the connection this event belongs to
|
|
375
|
+
if (!this.isCurrent(publicKeyHex, connection)) return;
|
|
331
376
|
this.remove(publicKeyHex);
|
|
332
377
|
this.#logger.debug(`#append: connection closed for validator ${publicKeyToAddress(publicKey, this.#config)}`);
|
|
333
378
|
});
|
|
@@ -350,6 +395,7 @@ class ConnectionManager {
|
|
|
350
395
|
} else {
|
|
351
396
|
this.#validators.set(publicKeyHex, {connection, sent: 0});
|
|
352
397
|
}
|
|
398
|
+
this.#clearHealthCheckFailures(publicKeyHex);
|
|
353
399
|
}
|
|
354
400
|
|
|
355
401
|
#toHexString(publicKey) {
|
|
@@ -122,7 +122,7 @@ class ValidatorObserverService {
|
|
|
122
122
|
*
|
|
123
123
|
* If the count of fully connected writers exceeds the threshold:
|
|
124
124
|
* - The admin node is excluded from the validator pool (handled in selection phase).
|
|
125
|
-
* - Non-admin nodes
|
|
125
|
+
* - Non-admin nodes detach the admin from the validator pool, preserving replication.
|
|
126
126
|
*
|
|
127
127
|
* NOTE:
|
|
128
128
|
* The admin is not forcibly disconnected from all roles.
|
|
@@ -144,7 +144,7 @@ class ValidatorObserverService {
|
|
|
144
144
|
// Non-admin nodes: ensure the admin is not kept as a validator connection
|
|
145
145
|
const adminWriter = writers.find((w) => w.address === adminEntry?.address);
|
|
146
146
|
if (adminWriter?.publicKey) {
|
|
147
|
-
this.#network.validatorConnectionManager.remove(adminWriter.publicKey);
|
|
147
|
+
this.#network.validatorConnectionManager.remove(adminWriter.publicKey, { endConnection: false });
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -156,7 +156,8 @@ class ValidatorObserverService {
|
|
|
156
156
|
const manager = this.#network.validatorConnectionManager;
|
|
157
157
|
|
|
158
158
|
if (manager.connected(publicKey)) {
|
|
159
|
-
|
|
159
|
+
// leave the transport to the swarm
|
|
160
|
+
manager.remove(publicKey, { endConnection: false });
|
|
160
161
|
this.#logger.debug(`Removed stale validator connection: ${b4a.toString(publicKey, "hex")}`);
|
|
161
162
|
}
|
|
162
163
|
}
|
package/src/core/state/State.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
TRAC_NAMESPACE,
|
|
16
16
|
CustomEventType
|
|
17
17
|
} from '../../utils/constants.js';
|
|
18
|
-
import { isHexString, sleep
|
|
18
|
+
import { isHexString, sleep } from '../../utils/helpers.js';
|
|
19
19
|
import tracCryptoApi from 'trac-crypto-api';
|
|
20
20
|
import Check from '../../utils/check.js';
|
|
21
21
|
import { safeDecodeApplyOperation } from '../../utils/protobuf/operationHelpers.js';
|
|
@@ -372,18 +372,13 @@ class State extends ReadyResource {
|
|
|
372
372
|
throw new Error("Invalid hash format");
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
for await (const entry of historyStream) {
|
|
382
|
-
if (isTransactionRecordPut(entry) && entry.key === hash) {
|
|
383
|
-
return entry.seq;
|
|
384
|
-
}
|
|
375
|
+
const snapshot = this.#base.view.checkout(this.getSignedLength());
|
|
376
|
+
try {
|
|
377
|
+
const entry = await snapshot.get(hash);
|
|
378
|
+
return entry ? entry.seq : null;
|
|
379
|
+
} finally {
|
|
380
|
+
await snapshot.close();
|
|
385
381
|
}
|
|
386
|
-
return null;
|
|
387
382
|
}
|
|
388
383
|
|
|
389
384
|
async confirmedTransactionsBetween(startSignedLength, endSignedLength) {
|