trac-msb 0.2.20 → 0.2.23
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/SECURITY.md +3 -5
- 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 +18 -13
- package/src/core/state/State.js +7 -12
package/SECURITY.md
CHANGED
|
@@ -6,13 +6,11 @@ The following table shows which versions of this project are currently receiving
|
|
|
6
6
|
|
|
7
7
|
| Version | Supported | Pear compatibility |
|
|
8
8
|
| -------- | ------------------ | ------------------ |
|
|
9
|
-
| 0.2.
|
|
10
|
-
| 0.2.18 | :white_check_mark: | Pear v2 only |
|
|
11
|
-
| 0.2.17 | :white_check_mark: | Pear v2 only |
|
|
9
|
+
| 0.2.21 | :white_check_mark: | Pear v2 and v3 |
|
|
12
10
|
| main | :warning: unstable | Pear v2 and v3 |
|
|
13
|
-
| < 0.2.
|
|
11
|
+
| < 0.2.21 | :x: | N/A |
|
|
14
12
|
|
|
15
|
-
Versions older than 0.2.
|
|
13
|
+
Versions older than 0.2.21 are deprecated.
|
|
16
14
|
Please upgrade to the latest release to ensure you receive security fixes.
|
|
17
15
|
|
|
18
16
|
---
|
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.23",
|
|
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
|
}
|
|
@@ -373,8 +374,8 @@ class ValidatorObserverService {
|
|
|
373
374
|
* Rules:
|
|
374
375
|
* - Skips invalid/malformed entries.
|
|
375
376
|
* - Skips non-admin indexers.
|
|
376
|
-
* - Deduplicates
|
|
377
|
-
* - Removes stale connections
|
|
377
|
+
* - Deduplicates active writers by wallet address.
|
|
378
|
+
* - Removes stale connections only when no active writer remains for an address.
|
|
378
379
|
* - Includes only valid and active writer nodes.
|
|
379
380
|
* @param {Object} adminEntry - The current admin node entry.
|
|
380
381
|
* @returns {Promise<Array<Object>>} A promise resolving to the deduplicated array of active writers.
|
|
@@ -385,13 +386,13 @@ class ValidatorObserverService {
|
|
|
385
386
|
this.#keyDecodeCache.clear();
|
|
386
387
|
}
|
|
387
388
|
|
|
388
|
-
//
|
|
389
|
-
// occupies only a single slot despite the append-only nature of Autobase
|
|
389
|
+
// Historical writer keys can share an address with an active replacement.
|
|
390
390
|
const activeMap = new Map();
|
|
391
|
+
const removedMap = new Map();
|
|
391
392
|
|
|
392
393
|
try {
|
|
393
394
|
for await (const { key, value } of this.#state.base.system.list()) {
|
|
394
|
-
if (!this.#shouldRun())
|
|
395
|
+
if (!this.#shouldRun()) return Array.from(activeMap.values());
|
|
395
396
|
|
|
396
397
|
if (!key || b4a.byteLength(key) !== WRITER_BYTE_LENGTH) continue;
|
|
397
398
|
if (!value) continue;
|
|
@@ -426,16 +427,20 @@ class ValidatorObserverService {
|
|
|
426
427
|
|
|
427
428
|
if (value.isIndexer && addr !== adminEntry?.address) continue;
|
|
428
429
|
|
|
429
|
-
// Handles soft-delete: drops stale connections and removes the node from the pool
|
|
430
430
|
if (value.isRemoved) {
|
|
431
|
-
|
|
432
|
-
activeMap.delete(addr);
|
|
431
|
+
removedMap.set(addr, publicKey);
|
|
433
432
|
continue;
|
|
434
433
|
}
|
|
435
434
|
|
|
436
|
-
// Registers or overwrites to maintain a single source of truth per machine
|
|
437
435
|
activeMap.set(addr, { key, address: addr, publicKey, publicKeyHex });
|
|
438
436
|
}
|
|
437
|
+
|
|
438
|
+
// Writer iteration order does not represent key age. Only a complete scan
|
|
439
|
+
// can establish that a removed writer has no active replacement.
|
|
440
|
+
for (const [addr, publicKey] of removedMap) {
|
|
441
|
+
if (!this.#shouldRun()) break;
|
|
442
|
+
if (!activeMap.has(addr)) this.#enforceNoStaleConnections(publicKey);
|
|
443
|
+
}
|
|
439
444
|
} catch (err) {
|
|
440
445
|
this.#logger.error(`Autobase writer scan error: ${err.message}`);
|
|
441
446
|
}
|
|
@@ -462,4 +467,4 @@ class ValidatorObserverService {
|
|
|
462
467
|
}
|
|
463
468
|
}
|
|
464
469
|
|
|
465
|
-
export default ValidatorObserverService;
|
|
470
|
+
export default ValidatorObserverService;
|
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) {
|