trac-msb 0.3.0-beta.2 → 0.3.0-beta.3
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
CHANGED
|
@@ -42,9 +42,9 @@ class ValidatorMessageOrchestrator {
|
|
|
42
42
|
*
|
|
43
43
|
* @param {object} [message] Outgoing operation payload.
|
|
44
44
|
* @param {string} [message.address] Requester address (bech32m).
|
|
45
|
-
* @returns {string|null} Selected validator public key hex, or null when unavailable.
|
|
45
|
+
* @returns {Promise<string|null>} Selected validator public key hex, or null when unavailable.
|
|
46
46
|
*/
|
|
47
|
-
#pickValidatorForMessage(message) {
|
|
47
|
+
async #pickValidatorForMessage(message) {
|
|
48
48
|
const requesterAddress = message?.address;
|
|
49
49
|
|
|
50
50
|
if (!requesterAddress) return null
|
|
@@ -58,10 +58,15 @@ class ValidatorMessageOrchestrator {
|
|
|
58
58
|
return publicKeyToAddress(publicKey, this.#config) !== requesterAddress;
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
const pool = eligible.length > 0 ? eligible : connected;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const pool = [...(eligible.length > 0 ? eligible : connected)];
|
|
62
|
+
while (pool.length > 0) {
|
|
63
|
+
const index = Math.floor(Math.random() * pool.length);
|
|
64
|
+
const [publicKey] = pool.splice(index, 1);
|
|
65
|
+
const address = publicKeyToAddress(publicKey, this.#config);
|
|
66
|
+
if (await this.state.isValidatorAddress(address)) return publicKey;
|
|
67
|
+
this.validatorConnectionManager.remove(publicKey);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -76,7 +81,7 @@ class ValidatorMessageOrchestrator {
|
|
|
76
81
|
return false;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
const validatorPublicKey = this.#pickValidatorForMessage(message);
|
|
84
|
+
const validatorPublicKey = await this.#pickValidatorForMessage(message);
|
|
80
85
|
if (!validatorPublicKey) return false;
|
|
81
86
|
console.log("Sending message to validator:", publicKeyToAddress(validatorPublicKey, this.#config));
|
|
82
87
|
|
|
@@ -91,6 +96,7 @@ class ValidatorMessageOrchestrator {
|
|
|
91
96
|
|
|
92
97
|
// TODO: After Legacy is deprecated, we don't need to check preferred protocol here.
|
|
93
98
|
const validatorConnection = this.validatorConnectionManager.getConnection(validatorPublicKey);
|
|
99
|
+
if (!validatorConnection) return this.send(message, retries + 1);
|
|
94
100
|
const preferredProtocol = validatorConnection.protocolSessions.validator.preferredProtocol;
|
|
95
101
|
let success = false;
|
|
96
102
|
if (preferredProtocol === validatorConnection.protocolSessions.validator.supportedProtocols.LEGACY) {
|
|
@@ -4,6 +4,7 @@ import b4a from "b4a";
|
|
|
4
4
|
import { bufferToAddress } from "../../../core/state/utils/address.js";
|
|
5
5
|
import tracCryptoApi from "trac-crypto-api";
|
|
6
6
|
import { WRITER_BYTE_LENGTH, CONNECTION_STATUS } from "../../../utils/constants.js";
|
|
7
|
+
import { publicKeyToAddress } from "../../../utils/helpers.js";
|
|
7
8
|
|
|
8
9
|
// Internal constants
|
|
9
10
|
const VALIDATOR_CANDIDATES_PER_CYCLE = 10;
|
|
@@ -48,27 +49,19 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
48
49
|
* - Assumes writers list is already pre-filtered and deduplicated from scan phase.
|
|
49
50
|
* - Excludes self.
|
|
50
51
|
* - Excludes already connected or pending nodes.
|
|
51
|
-
* -
|
|
52
|
+
* - Revalidates roles and the admin threshold before connecting.
|
|
52
53
|
* - Randomizes selection (Fisher-Yates) to avoid bias.
|
|
53
54
|
* @param {Array<Object>} writers - The deduplicated list of active writers.
|
|
54
|
-
* @param {Object} adminEntry - The current admin node entry.
|
|
55
55
|
* @returns {{candidates: Array<Object>, slots: number}} The selected candidates and the number of available connection slots.
|
|
56
56
|
*/
|
|
57
|
-
#selectCandidates(writers
|
|
57
|
+
#selectCandidates(writers) {
|
|
58
58
|
const manager = this.#network.validatorConnectionManager;
|
|
59
|
-
const adminThreshold = this.#config.maxWritersForAdminIndexerConnection;
|
|
60
|
-
|
|
61
|
-
// Evaluates the admin blockage policy based on fully connected/alive writers
|
|
62
|
-
const aliveWritersCount = this.#countAliveWriters(writers, adminEntry);
|
|
63
|
-
const adminBlocked = adminEntry && aliveWritersCount >= adminThreshold;
|
|
64
|
-
|
|
65
59
|
// Target calculation: ensures the observer deducts its own slot from the network size
|
|
66
60
|
// only if it belongs to the valid writers list.
|
|
67
61
|
const isSelfInWriters = writers.some(w => w.address === this.#address);
|
|
68
62
|
const networkSize = isSelfInWriters ? writers.length - 1 : writers.length;
|
|
69
63
|
const target = Math.min(networkSize, this.#config.maxValidators);
|
|
70
64
|
|
|
71
|
-
const isSelfAdmin = this.#address === adminEntry?.address;
|
|
72
65
|
const totalConnected = manager.connectionCount();
|
|
73
66
|
|
|
74
67
|
const available = writers.filter((w) => {
|
|
@@ -82,9 +75,6 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
82
75
|
// Skips nodes we are already interacting with
|
|
83
76
|
if (isConnected || isPending) return false;
|
|
84
77
|
|
|
85
|
-
// The admin is actively filtered out from the pool if the threshold policy is triggered
|
|
86
|
-
if (adminBlocked && !isSelfAdmin && w.address === adminEntry?.address) return false;
|
|
87
|
-
|
|
88
78
|
return true;
|
|
89
79
|
});
|
|
90
80
|
|
|
@@ -107,37 +97,6 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
107
97
|
}
|
|
108
98
|
}
|
|
109
99
|
|
|
110
|
-
/**
|
|
111
|
-
* Enforces admin-specific connection rules.
|
|
112
|
-
*
|
|
113
|
-
* If the count of fully connected writers exceeds the threshold:
|
|
114
|
-
* - The admin node is excluded from the validator pool (handled in selection phase).
|
|
115
|
-
* - Non-admin nodes detach the admin from the validator pool, preserving replication.
|
|
116
|
-
*
|
|
117
|
-
* NOTE:
|
|
118
|
-
* The admin is not forcibly disconnected from all roles.
|
|
119
|
-
* It can still maintain connections for tx submission and observation.
|
|
120
|
-
* @param {Array<Object>} writers - The deduplicated list of active writers.
|
|
121
|
-
* @param {Object} adminEntry - The current admin node entry.
|
|
122
|
-
*/
|
|
123
|
-
#enforceAdminPolicy(writers, adminEntry) {
|
|
124
|
-
// Evaluates against alive network peers rather than dormant database entries
|
|
125
|
-
const aliveWritersCount = this.#countAliveWriters(writers, adminEntry);
|
|
126
|
-
|
|
127
|
-
if (aliveWritersCount < this.#config.maxWritersForAdminIndexerConnection) return;
|
|
128
|
-
|
|
129
|
-
this.#logger.debug("Validator threshold reached → enforcing admin policy");
|
|
130
|
-
|
|
131
|
-
const isAdmin = adminEntry && this.#address === adminEntry.address;
|
|
132
|
-
if (isAdmin) return;
|
|
133
|
-
|
|
134
|
-
// Non-admin nodes: ensure the admin is not kept as a validator connection
|
|
135
|
-
const adminWriter = writers.find((w) => w.address === adminEntry?.address);
|
|
136
|
-
if (adminWriter?.publicKey) {
|
|
137
|
-
this.#network.validatorConnectionManager.remove(adminWriter.publicKey);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
100
|
/**
|
|
142
101
|
* Removes active connections to writers that are no longer valid (e.g., removed/soft-deleted).
|
|
143
102
|
* @param {Uint8Array} publicKey - The public key of the stale connection to be removed.
|
|
@@ -161,40 +120,6 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
161
120
|
return !this.isInterrupted;
|
|
162
121
|
}
|
|
163
122
|
|
|
164
|
-
/**
|
|
165
|
-
* Counts the number of physically active and connected writers in the network.
|
|
166
|
-
* This method bridges the gap between the Autobase state (which may include
|
|
167
|
-
* offline/dormant writers due to its append-only/soft-delete nature) and the
|
|
168
|
-
* actual physical network layer. It ensures the admin threshold policy is
|
|
169
|
-
* evaluated against truly active nodes rather than historical state.
|
|
170
|
-
*
|
|
171
|
-
* @param {Array<Object>} writers - The deduplicated list of writers from the Autobase.
|
|
172
|
-
* @param {Object} adminEntry - The current admin node entry.
|
|
173
|
-
* @returns {number} The count of alive, fully connected writers (including self).
|
|
174
|
-
*/
|
|
175
|
-
#countAliveWriters(writers, adminEntry) {
|
|
176
|
-
let aliveCount = 0;
|
|
177
|
-
const manager = this.#network.validatorConnectionManager;
|
|
178
|
-
|
|
179
|
-
for (const w of writers) {
|
|
180
|
-
// Ignores the admin in the threshold calculation
|
|
181
|
-
if (w.address === adminEntry?.address) continue;
|
|
182
|
-
|
|
183
|
-
// Always counts the current node as alive if it's in the valid writers list
|
|
184
|
-
if (w.address === this.#address) {
|
|
185
|
-
aliveCount++;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Counts only fully connected nodes (ignores pending or offline peers)
|
|
190
|
-
if (manager.connected(w.publicKey)) {
|
|
191
|
-
aliveCount++;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return aliveCount;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
123
|
/**
|
|
199
124
|
* Starts the validator observer loop.
|
|
200
125
|
* Initializes the scheduler and bootstrap state.
|
|
@@ -226,8 +151,8 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
226
151
|
* Main worker loop (executed periodically).
|
|
227
152
|
*
|
|
228
153
|
* Flow:
|
|
229
|
-
* 1.
|
|
230
|
-
* 2.
|
|
154
|
+
* 1. Revalidate connected peers against current roles and the admin threshold.
|
|
155
|
+
* 2. Fetch admin + writers (cached).
|
|
231
156
|
* 3. Skip if the connection pool is full.
|
|
232
157
|
* 4. Select candidates.
|
|
233
158
|
* 5. Attempt connections (with pacing).
|
|
@@ -245,18 +170,23 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
245
170
|
}
|
|
246
171
|
|
|
247
172
|
try {
|
|
173
|
+
// Recheck bounded connection pools on every cycle, even when discovery
|
|
174
|
+
// is cached or the pool is full. Removing a role preserves replication.
|
|
175
|
+
const manager = this.#network.validatorConnectionManager;
|
|
176
|
+
for (const publicKey of manager.connectedPeers()) {
|
|
177
|
+
if (!await this.#state.isValidatorAddress(publicKeyToAddress(publicKey, this.#config))) {
|
|
178
|
+
manager.remove(publicKey);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
248
181
|
const adminEntry = await this.#getAdminEntryCached();
|
|
249
182
|
const writers = await this.#getWritersCached(adminEntry);
|
|
250
183
|
|
|
251
184
|
if (writers.length === 0) return next(interval);
|
|
252
185
|
|
|
253
|
-
this.#enforceAdminPolicy(writers, adminEntry);
|
|
254
|
-
|
|
255
|
-
const manager = this.#network.validatorConnectionManager;
|
|
256
186
|
// CEILING: do not exceed the max connections limit
|
|
257
187
|
if (manager.maxConnectionsReached()) return next(interval);
|
|
258
188
|
|
|
259
|
-
const { candidates, slots} = this.#selectCandidates(writers
|
|
189
|
+
const { candidates, slots} = this.#selectCandidates(writers);
|
|
260
190
|
if (candidates.length > 0) {
|
|
261
191
|
await this.#processCandidates(candidates, slots);
|
|
262
192
|
}
|
|
@@ -389,7 +319,8 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
389
319
|
const addressBuffer = await this.#state.getRegisteredWriterKey(writerKeyHex);
|
|
390
320
|
|
|
391
321
|
if (!addressBuffer || b4a.byteLength(addressBuffer) !== this.#config.addressLength) {
|
|
392
|
-
|
|
322
|
+
// Autobase membership can arrive before the signed address mapping.
|
|
323
|
+
// Retry on the next scan instead of caching a transient absence forever.
|
|
393
324
|
continue;
|
|
394
325
|
}
|
|
395
326
|
|
|
@@ -410,9 +341,7 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
410
341
|
|
|
411
342
|
const { addr, publicKey, publicKeyHex } = decodedIdentity;
|
|
412
343
|
|
|
413
|
-
if (value.isIndexer && addr !== adminEntry?.address)
|
|
414
|
-
|
|
415
|
-
if (value.isRemoved) {
|
|
344
|
+
if (value.isRemoved || (value.isIndexer && addr !== adminEntry?.address)) {
|
|
416
345
|
removedMap.set(addr, publicKey);
|
|
417
346
|
continue;
|
|
418
347
|
}
|
|
@@ -436,7 +365,7 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
436
365
|
/**
|
|
437
366
|
* Attempts to establish a connection with a validator candidate.
|
|
438
367
|
*
|
|
439
|
-
*
|
|
368
|
+
* Revalidates the role because discovery may predate a promotion or removal.
|
|
440
369
|
*
|
|
441
370
|
* Behavior:
|
|
442
371
|
* - Delegates connection attempt to the network layer.
|
|
@@ -445,6 +374,7 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
445
374
|
*/
|
|
446
375
|
async #tryConnect(candidate) {
|
|
447
376
|
try {
|
|
377
|
+
if (!await this.#state.isValidatorAddress(candidate.address)) return CONNECTION_STATUS.IGNORED;
|
|
448
378
|
return await this.#network.tryConnect(candidate.publicKeyHex, "validator");
|
|
449
379
|
} catch (err) {
|
|
450
380
|
this.#logger.error(`Validator connection attempt failed: ${err.message}`);
|
package/src/core/state/State.js
CHANGED
|
@@ -418,13 +418,26 @@ class State extends ReadyResource {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
async isAdminAllowedToValidate() {
|
|
421
|
-
if (!this.
|
|
422
|
-
|
|
423
|
-
const isAdmin = this.writingKey.toString('hex') === this.#config.bootstrap.toString('hex');
|
|
424
|
-
const isIndexer = this.isIndexer();
|
|
421
|
+
if (!this.isWritable() || !this.isIndexer() || !await this.isAdmin()) return false;
|
|
425
422
|
const activeWriters = await this.getActiveWriterCount(true);
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
return activeWriters < this.#config.maxWritersForAdminIndexerConnection;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Checks a remote validator against the current local view, including pending
|
|
428
|
+
* role changes. Cached discovery and established transports do not grant a role.
|
|
429
|
+
*/
|
|
430
|
+
async isValidatorAddress(address) {
|
|
431
|
+
const entry = await this.getNodeEntryUnsigned(address);
|
|
432
|
+
if (!entry?.isWriter) return false;
|
|
433
|
+
if (!entry.isIndexer) {
|
|
434
|
+
const signedEntry = await this.getNodeEntry(address);
|
|
435
|
+
return !!signedEntry?.isWriter && !signedEntry.isIndexer;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const admin = await this.getAdminEntry();
|
|
439
|
+
if (address !== admin?.address || !entry.wk || !admin.wk || !b4a.equals(entry.wk, admin.wk)) return false;
|
|
440
|
+
return await this.getActiveWriterCount(true) < this.#config.maxWritersForAdminIndexerConnection;
|
|
428
441
|
}
|
|
429
442
|
|
|
430
443
|
async isAddressWhitelisted(address) {
|