trac-msb 0.2.19 → 0.2.21

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 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.19 | :white_check_mark: | Pear v2 and v3 |
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.17 | :x: | N/A |
11
+ | < 0.2.21 | :x: | N/A |
14
12
 
15
- Versions older than 0.2.17 are deprecated.
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.19",
4
+ "version": "0.2.21",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -39,6 +39,7 @@ class Network extends ReadyResource {
39
39
  #wallet;
40
40
  #validatorHealthCheckService;
41
41
  #logger;
42
+ #closing = false;
42
43
 
43
44
  /**
44
45
  * @param {State} state
@@ -82,6 +83,7 @@ class Network extends ReadyResource {
82
83
 
83
84
  async _open() {
84
85
  this.#logger.info('Network initialization...');
86
+ this.#closing = false;
85
87
 
86
88
  this.setupNetworkListeners();
87
89
 
@@ -91,6 +93,7 @@ class Network extends ReadyResource {
91
93
 
92
94
  async _close() {
93
95
  this.#logger.info('Network: closing gracefully...');
96
+ this.#closing = true;
94
97
  await this.transactionPoolService.stopPool();
95
98
  await sleep(100);
96
99
  await this.#validatorObserverService.stopValidatorObserver();
@@ -104,8 +107,15 @@ class Network extends ReadyResource {
104
107
  this.#pendingRequestsService.close();
105
108
  this.#transactionCommitService.close();
106
109
 
107
- if (this.#swarm !== null) {
108
- this.#swarm.destroy();
110
+ const swarm = this.#swarm;
111
+ if (swarm !== null) {
112
+ if (typeof swarm.removeAllListeners === 'function') {
113
+ swarm.removeAllListeners('connection');
114
+ }
115
+ await swarm.destroy();
116
+ if (this.#swarm === swarm) {
117
+ this.#swarm = null;
118
+ }
109
119
  }
110
120
  }
111
121
 
@@ -167,8 +177,12 @@ class Network extends ReadyResource {
167
177
  store,
168
178
  wallet,
169
179
  ) {
180
+ this.#assertCanReplicate();
181
+
170
182
  if (!this.#swarm) {
171
183
  const { wallet: wrappedWallet, keyPair } = await this.#getOrGenerateWallet(store, wallet);
184
+ this.#assertCanReplicate();
185
+
172
186
  this.#wallet = wrappedWallet
173
187
  this.#validatorMessageOrchestrator.setWallet(this.#wallet);
174
188
 
@@ -193,19 +207,49 @@ class Network extends ReadyResource {
193
207
  );
194
208
  this.#validatorHealthCheckService = new ValidatorHealthCheckService(this.#config);
195
209
  await this.#validatorHealthCheckService.ready();
210
+ this.#assertCanReplicate();
211
+
196
212
  this.#validatorConnectionManager.subscribeToHealthChecks(this.#validatorHealthCheckService);
197
213
 
198
214
  this.#logger.info(`Channel: ${b4a.toString(this.#config.channel)}`);
199
215
 
200
216
  this.#swarm.on('connection', async (connection) => {
201
- // Per-peer connection initialization:
202
- // - attach Protomux (legacy + v1 channels/messages)
203
- // - attach connection.protocolSession (used later by tryConnect / orchestrators to send messages)
204
- await this.#networkMessages.setupProtomuxMessages(connection);
217
+ if (this.#closing) {
218
+ this.#destroyConnection(connection);
219
+ return;
220
+ }
205
221
 
206
- // ATTENTION: Must be called AFTER the protomux init above
207
- const stream = store.replicate(connection);
208
- wakeup.addStream(stream);
222
+ try {
223
+ // Per-peer connection initialization:
224
+ // - attach Protomux (legacy + v1 channels/messages)
225
+ // - attach connection.protocolSession (used later by tryConnect / orchestrators to send messages)
226
+ await this.#networkMessages.setupProtomuxMessages(connection);
227
+
228
+ // Pear v3 can deliver a late swarm connection while shutdown is
229
+ // already closing the Corestore. Do not replicate a connection
230
+ // after close has begun; store.replicate would otherwise throw.
231
+ if (this.#closing || this.#swarm === null) {
232
+ this.#destroyConnection(connection);
233
+ return;
234
+ }
235
+
236
+ // ATTENTION: Must be called AFTER the protomux init above
237
+ const stream = store.replicate(connection);
238
+ wakeup.addStream(stream);
239
+ } catch (error) {
240
+ const publicKey = connection.remotePublicKey
241
+ ? b4a.toString(connection.remotePublicKey, 'hex')
242
+ : 'unknown';
243
+ this.#pendingRequestsService.rejectPendingRequestsForPeer(
244
+ publicKey,
245
+ error ?? new Error('Connection setup failed')
246
+ );
247
+ this.#destroyConnection(connection);
248
+ if (!this.#closing) {
249
+ this.#logger.error(error?.message ?? 'Unknown network connection setup error');
250
+ }
251
+ return;
252
+ }
209
253
 
210
254
  const publicKey = b4a.toString(connection.remotePublicKey, 'hex');
211
255
  if (this.#pendingConnections.has(publicKey)) {
@@ -218,9 +262,13 @@ class Network extends ReadyResource {
218
262
  publicKey,
219
263
  new Error('Connection closed before response')
220
264
  );
221
- this.#swarm.leavePeer(connection.remotePublicKey);
265
+ this.#swarm?.leavePeer(connection.remotePublicKey);
222
266
  this.#validatorConnectionManager.remove(publicKey);
223
- connection.protocolSession.close();
267
+ if (connection.protocolSession) {
268
+ try {
269
+ connection.protocolSession.close();
270
+ } catch {}
271
+ }
224
272
  });
225
273
 
226
274
  connection.on('error', (error) => {
@@ -344,6 +392,26 @@ class Network extends ReadyResource {
344
392
  this.#pendingConnections.delete(publicKeyHex);
345
393
  return true;
346
394
  }
395
+
396
+ #assertCanReplicate() {
397
+ if (this.#closing || this.closed) {
398
+ throw new Error('Network is closing or already closed');
399
+ }
400
+ }
401
+
402
+ #destroyConnection(connection) {
403
+ if (!connection) return;
404
+ if (connection.protocolSession) {
405
+ try {
406
+ connection.protocolSession.close();
407
+ } catch {}
408
+ }
409
+ if (typeof connection.destroy === 'function') {
410
+ connection.destroy();
411
+ } else if (typeof connection.end === 'function') {
412
+ connection.end();
413
+ }
414
+ }
347
415
  }
348
416
 
349
417
  export default Network;
@@ -373,8 +373,8 @@ class ValidatorObserverService {
373
373
  * Rules:
374
374
  * - Skips invalid/malformed entries.
375
375
  * - Skips non-admin indexers.
376
- * - Deduplicates nodes by mapping their latest state to a unique address.
377
- * - Removes stale connections for nodes flagged with 'isRemoved'.
376
+ * - Deduplicates active writers by wallet address.
377
+ * - Removes stale connections only when no active writer remains for an address.
378
378
  * - Includes only valid and active writer nodes.
379
379
  * @param {Object} adminEntry - The current admin node entry.
380
380
  * @returns {Promise<Array<Object>>} A promise resolving to the deduplicated array of active writers.
@@ -385,13 +385,13 @@ class ValidatorObserverService {
385
385
  this.#keyDecodeCache.clear();
386
386
  }
387
387
 
388
- // Map is used for deduplication, ensuring that each node physical address
389
- // occupies only a single slot despite the append-only nature of Autobase
388
+ // Historical writer keys can share an address with an active replacement.
390
389
  const activeMap = new Map();
390
+ const removedMap = new Map();
391
391
 
392
392
  try {
393
393
  for await (const { key, value } of this.#state.base.system.list()) {
394
- if (!this.#shouldRun()) break;
394
+ if (!this.#shouldRun()) return Array.from(activeMap.values());
395
395
 
396
396
  if (!key || b4a.byteLength(key) !== WRITER_BYTE_LENGTH) continue;
397
397
  if (!value) continue;
@@ -426,16 +426,20 @@ class ValidatorObserverService {
426
426
 
427
427
  if (value.isIndexer && addr !== adminEntry?.address) continue;
428
428
 
429
- // Handles soft-delete: drops stale connections and removes the node from the pool
430
429
  if (value.isRemoved) {
431
- this.#enforceNoStaleConnections(publicKey);
432
- activeMap.delete(addr);
430
+ removedMap.set(addr, publicKey);
433
431
  continue;
434
432
  }
435
433
 
436
- // Registers or overwrites to maintain a single source of truth per machine
437
434
  activeMap.set(addr, { key, address: addr, publicKey, publicKeyHex });
438
435
  }
436
+
437
+ // Writer iteration order does not represent key age. Only a complete scan
438
+ // can establish that a removed writer has no active replacement.
439
+ for (const [addr, publicKey] of removedMap) {
440
+ if (!this.#shouldRun()) break;
441
+ if (!activeMap.has(addr)) this.#enforceNoStaleConnections(publicKey);
442
+ }
439
443
  } catch (err) {
440
444
  this.#logger.error(`Autobase writer scan error: ${err.message}`);
441
445
  }
@@ -462,4 +466,4 @@ class ValidatorObserverService {
462
466
  }
463
467
  }
464
468
 
465
- export default ValidatorObserverService;
469
+ export default ValidatorObserverService;