@serfab/cadre-core 1.8.0 → 1.9.0
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/dist/cadre-node.d.ts +153 -104
- package/dist/cadre-node.js +227 -199
- package/dist/cadre-node.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +10 -11
- package/dist/index.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +6 -4
- package/dist/node-local-snapshot.js +7 -3
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +0 -9
- package/dist/peer-join-backfill.js +2 -3
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +0 -11
- package/dist/strand-formation-protocol.js +3 -4
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +54 -0
- package/dist/strand-fret-addrs.js +108 -0
- package/dist/strand-fret-addrs.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +9 -38
- package/dist/strand-instance-manager.js +13 -73
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-network-state-file.d.ts +12 -0
- package/dist/strand-network-state-file.js +38 -0
- package/dist/strand-network-state-file.js.map +1 -0
- package/dist/strand-network-state.d.ts +111 -0
- package/dist/strand-network-state.js +191 -0
- package/dist/strand-network-state.js.map +1 -0
- package/dist/types.d.ts +20 -23
- package/package.json +11 -10
- package/src/cadre-node.ts +238 -205
- package/src/index.ts +15 -55
- package/src/node-local-snapshot.ts +12 -7
- package/src/peer-join-backfill.ts +2 -3
- package/src/strand-formation-protocol.ts +3 -4
- package/src/strand-fret-addrs.ts +145 -0
- package/src/strand-instance-manager.ts +20 -96
- package/src/strand-network-state-file.ts +40 -0
- package/src/strand-network-state.ts +243 -0
- package/src/types.ts +20 -23
- package/dist/strand-peer-book-file.d.ts +0 -12
- package/dist/strand-peer-book-file.js +0 -37
- package/dist/strand-peer-book-file.js.map +0 -1
- package/dist/strand-peer-book-protocol.d.ts +0 -168
- package/dist/strand-peer-book-protocol.js +0 -356
- package/dist/strand-peer-book-protocol.js.map +0 -1
- package/dist/strand-peer-book-swap.d.ts +0 -104
- package/dist/strand-peer-book-swap.js +0 -284
- package/dist/strand-peer-book-swap.js.map +0 -1
- package/dist/strand-peer-book.d.ts +0 -159
- package/dist/strand-peer-book.js +0 -361
- package/dist/strand-peer-book.js.map +0 -1
- package/dist/strand-peer-observer.d.ts +0 -88
- package/dist/strand-peer-observer.js +0 -180
- package/dist/strand-peer-observer.js.map +0 -1
- package/src/strand-peer-book-file.ts +0 -39
- package/src/strand-peer-book-protocol.ts +0 -466
- package/src/strand-peer-book-swap.ts +0 -338
- package/src/strand-peer-book.ts +0 -475
- package/src/strand-peer-observer.ts +0 -235
package/dist/cadre-node.js
CHANGED
|
@@ -17,10 +17,11 @@ import { DEFAULT_IDENTITY_KEY_ID } from './key-store.js';
|
|
|
17
17
|
import { loadOrCreateIdentityKey } from './identity-key.js';
|
|
18
18
|
import { MemoryTrustedOwnerStore } from './trusted-owner-store.js';
|
|
19
19
|
import { MemoryBootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
20
|
-
import {
|
|
20
|
+
import { MemoryStrandNetworkStateStore } from './strand-network-state.js';
|
|
21
21
|
import { MemoryEnrolledMachineStore } from './enrolled-machine-store.js';
|
|
22
22
|
import { JoinedStrandSession, KeyStoreJoinedStrandStore, MemoryJoinedStrandStore } from './joined-strand-store.js';
|
|
23
23
|
import { mergePeerAddrs, groupAddrsByPeerId } from './peer-addr-book.js';
|
|
24
|
+
import { strandFretPeerAddrs } from './strand-fret-addrs.js';
|
|
24
25
|
import { verifyCadrePeerVoucher } from './peer-authorization.js';
|
|
25
26
|
import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
|
|
26
27
|
import { signPeerRecord, verifyPeerRecordSignature, isPeerRecordFresh, orderSignalingFirst, isSignalingAddr, trailingPeerId, withTrailingPeerId, currentMemberTrustPolicy, DEFAULT_PEER_RECORD_MAX_AGE_MS, DEFAULT_PEER_RECORD_HEARTBEAT_MS } from './peer-record.js';
|
|
@@ -98,8 +99,8 @@ function siblingAnswered(outcome) {
|
|
|
98
99
|
* `primary` followed by the entries of `extra` it does not already contain, de-duplicated
|
|
99
100
|
* and order-preserving.
|
|
100
101
|
*
|
|
101
|
-
* Used
|
|
102
|
-
* freshly-RPC'd sibling answers and the
|
|
102
|
+
* Used where a strand's discovery seed unions two sources of address strings — the
|
|
103
|
+
* freshly-RPC'd sibling answers and the addresses the strand's formation carried. The
|
|
103
104
|
* caller passes the FRESHER source as `primary`, so a stale entry can only ever be
|
|
104
105
|
* appended, never promoted ahead of a current one, and each source keeps whatever
|
|
105
106
|
* signaling-first ordering it arrived with.
|
|
@@ -438,36 +439,54 @@ export class CadreNode {
|
|
|
438
439
|
*/
|
|
439
440
|
this.strandAddrAskDueAt = new Map();
|
|
440
441
|
/**
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* store as `StartStrandConfig.strandPeerBook`) files the node's own signed entry
|
|
448
|
-
* plus every signed entry a strand peer sends it — the only writer whose entries
|
|
449
|
-
* carry a signature, and the only one that can refresh a peer's addresses without
|
|
450
|
-
* meeting that peer. Read by {@link resolveCohortSeed} (launch + hibernation resume) and
|
|
451
|
-
* re-merged by every {@link refreshStrandPeerAddrs} pass, which is what keeps the
|
|
452
|
-
* entries alive past the peerStore's one-hour address expiry.
|
|
442
|
+
* The strand-network addresses a formation carried back
|
|
443
|
+
* (`FormationResultMessage.strandAddrs`), by strand id: the responder's live strand
|
|
444
|
+
* node addresses, kept so the strand's first attach has something to dial. Written by
|
|
445
|
+
* {@link recordFormationStrandAddrs}, read by {@link resolveCohortSeed} (launch and
|
|
446
|
+
* hibernation resume) and by every refresh pass for the peers the strand node's FRET
|
|
447
|
+
* table holds no record for yet ({@link remergeUnrecordedFormationAddrs}).
|
|
453
448
|
*
|
|
454
|
-
* This is the
|
|
449
|
+
* This is the only cross-party input a FIRST attach has. The strand-addr RPC that
|
|
455
450
|
* resolves a strand's addresses is membership-gated and answers own-party siblings
|
|
456
|
-
* only, so without
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
451
|
+
* only, so without it a joiner's seed for a two-party strand is empty and the mesh
|
|
452
|
+
* never forms. Every later launch has the strand node's saved network state as well
|
|
453
|
+
* ({@link strandNetworkStateStore}), which is what a restart re-finds the other party
|
|
454
|
+
* from (gotchoices/sereus#18).
|
|
455
|
+
*
|
|
456
|
+
* A re-formation replaces the strand's list. An entry survives a {@link stopStrand}
|
|
457
|
+
* and a stop()→start() of this node instance ({@link cleanup} leaves the map alone),
|
|
458
|
+
* and is dropped where the saved network state is: {@link unpublishStrand},
|
|
459
|
+
* {@link forgetJoinedStrand} and self-revocation.
|
|
460
|
+
*
|
|
461
|
+
* NOTE: accepted tradeoff — in memory only, so a process restart between `formStrand`
|
|
462
|
+
* and the strand's first launch loses the carried addresses, and the strand then
|
|
463
|
+
* launches with no cross-party seed until it is re-formed. Persisting the list on the
|
|
464
|
+
* joined-strand record was weighed and declined: it mixes address state into the join
|
|
465
|
+
* record, and needs an "only until the first run" rule to stop dead addresses being
|
|
466
|
+
* dialled on every launch. Every embedder launches straight after forming, and once
|
|
467
|
+
* the strand has run the saved network state covers every later restart. Revisit if an
|
|
468
|
+
* embedder forms and launches in separate sessions.
|
|
469
|
+
*/
|
|
470
|
+
this.formationStrandAddrs = new Map();
|
|
471
|
+
/**
|
|
472
|
+
* Node-local strand network state (see `strand-network-state.ts`): per strand, the
|
|
473
|
+
* state db-p2p saves for the strand's libp2p node — its FRET routing table with each
|
|
474
|
+
* peer's signed address record. Handed to every strand launch as
|
|
475
|
+
* `StartStrandConfig.networkState`; the strand node saves into it on every connection
|
|
476
|
+
* and re-imports it when it is next built, which is what gives a restarted strand
|
|
477
|
+
* node addresses for the peers it was talking to.
|
|
478
|
+
*
|
|
479
|
+
* Constructed (or adopted from `config.strandNetworkState.store`) by
|
|
480
|
+
* {@link initializeStrandNetworkStateStore} during {@link start}, deliberately NOT
|
|
461
481
|
* cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same node
|
|
462
482
|
* instance — same lifecycle as {@link bootstrapPeerStore}.
|
|
463
483
|
*
|
|
464
|
-
*
|
|
465
|
-
* {@link
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* bounded by time and by the strands this node runs rather than by its history.
|
|
484
|
+
* A strand's state survives a {@link stopStrand} and a hibernation quiesce, and is
|
|
485
|
+
* forgotten by {@link unpublishStrand}, {@link forgetJoinedStrand} and
|
|
486
|
+
* self-revocation. A strand detached because the watcher saw its row gone keeps its
|
|
487
|
+
* state (`NOTE:` on `PersistentStrandNetworkStateStore`).
|
|
469
488
|
*/
|
|
470
|
-
this.
|
|
489
|
+
this.strandNetworkStateStore = null;
|
|
471
490
|
/**
|
|
472
491
|
* PENDING strand membership invitations learned at formation, keyed by strandId — the
|
|
473
492
|
* single-use `Strand.Invite` credential a closed-strand formation result carried back
|
|
@@ -495,7 +514,7 @@ export class CadreNode {
|
|
|
495
514
|
* NOTE: entries live for the node's lifetime (one small pair per formed closed
|
|
496
515
|
* strand); the number of keys is bounded only by the strands this node has ever
|
|
497
516
|
* formed, not by time — if a node ever forms strands at scale, evict on
|
|
498
|
-
* `unpublishStrand` / `forgetJoinedStrand` as
|
|
517
|
+
* `unpublishStrand` / `forgetJoinedStrand` as {@link formationStrandAddrs} is.
|
|
499
518
|
*/
|
|
500
519
|
this.pendingMembershipInvites = new Map();
|
|
501
520
|
/**
|
|
@@ -878,8 +897,9 @@ export class CadreNode {
|
|
|
878
897
|
// beside the control rows. Before network bring-up, so a mis-scoped injected store
|
|
879
898
|
// fails closed like the two above.
|
|
880
899
|
this.initializeJoinedStrandStore();
|
|
881
|
-
//
|
|
882
|
-
|
|
900
|
+
// Each strand node's saved network state, which those joins are dialed from after
|
|
901
|
+
// a restart. Same placement, same reason.
|
|
902
|
+
this.initializeStrandNetworkStateStore();
|
|
883
903
|
// Read the party's enrolled-machine count out of its node-local record and
|
|
884
904
|
// capture it for buildControlNodeOptions below. This MUST precede
|
|
885
905
|
// createControlNode: Optimystic freezes the cluster policy when the node is
|
|
@@ -1249,25 +1269,25 @@ export class CadreNode {
|
|
|
1249
1269
|
this.joinedStrands = new JoinedStrandSession(store, this.createPartyJoinedStrandLedger());
|
|
1250
1270
|
}
|
|
1251
1271
|
/**
|
|
1252
|
-
* Construct (or adopt) the node-local strand
|
|
1253
|
-
* {@link
|
|
1272
|
+
* Construct (or adopt) the node-local strand network state (see
|
|
1273
|
+
* {@link strandNetworkStateStore}). Synchronous for the reason
|
|
1254
1274
|
* {@link initializeBootstrapPeerStore} is: an injected store has already loaded its
|
|
1255
1275
|
* persisted entries by the time it is handed in, and the in-memory fallback has
|
|
1256
1276
|
* nothing to load. The instance is kept across stop()→start(). A store scoped to a
|
|
1257
|
-
* different party is a configuration error, fail closed: another party's
|
|
1258
|
-
*
|
|
1277
|
+
* different party is a configuration error, fail closed: another party's routing
|
|
1278
|
+
* tables must never be imported into this node's strands.
|
|
1259
1279
|
*/
|
|
1260
|
-
|
|
1280
|
+
initializeStrandNetworkStateStore() {
|
|
1261
1281
|
const partyId = this.config.controlNetwork.partyId;
|
|
1262
|
-
if (this.
|
|
1282
|
+
if (this.strandNetworkStateStore) {
|
|
1263
1283
|
return;
|
|
1264
1284
|
}
|
|
1265
|
-
const store = this.config.
|
|
1285
|
+
const store = this.config.strandNetworkState?.store ?? new MemoryStrandNetworkStateStore(partyId);
|
|
1266
1286
|
if (store.partyId !== partyId) {
|
|
1267
|
-
throw new Error(`CadreNodeConfig:
|
|
1268
|
-
`but this node serves party ${partyId} — refusing to mix strand
|
|
1287
|
+
throw new Error(`CadreNodeConfig: strandNetworkState.store is scoped to party ${store.partyId}, ` +
|
|
1288
|
+
`but this node serves party ${partyId} — refusing to mix strand network state`);
|
|
1269
1289
|
}
|
|
1270
|
-
this.
|
|
1290
|
+
this.strandNetworkStateStore = store;
|
|
1271
1291
|
}
|
|
1272
1292
|
/**
|
|
1273
1293
|
* The node-local enrolled-machine store (null before {@link start}) — what this
|
|
@@ -1287,13 +1307,13 @@ export class CadreNode {
|
|
|
1287
1307
|
return this.bootstrapPeerStore;
|
|
1288
1308
|
}
|
|
1289
1309
|
/**
|
|
1290
|
-
* The node-local strand
|
|
1291
|
-
*
|
|
1292
|
-
* diagnostics and for a host that wants to show "
|
|
1293
|
-
*
|
|
1310
|
+
* The node-local strand network state (null before {@link start}) — per strand, the
|
|
1311
|
+
* state its strand node last saved and will re-import when next built. Exposed for
|
|
1312
|
+
* diagnostics and for a host that wants to show "what routing table would this
|
|
1313
|
+
* strand restart with?".
|
|
1294
1314
|
*/
|
|
1295
|
-
|
|
1296
|
-
return this.
|
|
1315
|
+
getStrandNetworkStateStore() {
|
|
1316
|
+
return this.strandNetworkStateStore;
|
|
1297
1317
|
}
|
|
1298
1318
|
/**
|
|
1299
1319
|
* The node-local trusted-owner anchor (null before {@link start}). This is
|
|
@@ -2737,10 +2757,10 @@ export class CadreNode {
|
|
|
2737
2757
|
if (!this._running || !this.controlNode || !this.controlDatabase) {
|
|
2738
2758
|
return { dialed: [] };
|
|
2739
2759
|
}
|
|
2740
|
-
// Then re-warm each running strand's own address book from its
|
|
2741
|
-
// from whichever siblings are due an ask. Same reasoning as
|
|
2742
|
-
// below, one layer down: replication runs on the STRAND
|
|
2743
|
-
// layer under cadre-core dials strand peers by bare peer id.
|
|
2760
|
+
// Then re-warm each running strand's own address book from its FRET address
|
|
2761
|
+
// records and from whichever siblings are due an ask. Same reasoning as
|
|
2762
|
+
// warmSiblingAddrBook below, one layer down: replication runs on the STRAND
|
|
2763
|
+
// network, and every layer under cadre-core dials strand peers by bare peer id.
|
|
2744
2764
|
await this.refreshStrandPeerAddrs();
|
|
2745
2765
|
if (!this._running || !this.controlNode || !this.controlDatabase) {
|
|
2746
2766
|
return { dialed: [] };
|
|
@@ -5275,9 +5295,9 @@ export class CadreNode {
|
|
|
5275
5295
|
// `privateKey` — tests, and an embedder that opted out of a stable identity) the
|
|
5276
5296
|
// strand node still runs under a key CADRE-CORE HOLDS: a fresh random Ed25519 key,
|
|
5277
5297
|
// exactly what libp2p would generate internally if handed none, except that the
|
|
5278
|
-
//
|
|
5279
|
-
// the retained launch config keeps
|
|
5280
|
-
//
|
|
5298
|
+
// delegate announcement below can name the strand peer id before the node exists and
|
|
5299
|
+
// the retained launch config keeps that id across a hibernation resume. Stability
|
|
5300
|
+
// across RESTARTS still needs an identity key.
|
|
5281
5301
|
//
|
|
5282
5302
|
// NOTE: derivation requires an Ed25519 identity key, so a node configured
|
|
5283
5303
|
// with some other key type now fails strand launch outright (surfaced as
|
|
@@ -5329,15 +5349,12 @@ export class CadreNode {
|
|
|
5329
5349
|
this.forgetRevokedJoin(revokedStrandId);
|
|
5330
5350
|
this.emit('strand:revoked', { strandId: revokedStrandId });
|
|
5331
5351
|
},
|
|
5332
|
-
//
|
|
5333
|
-
//
|
|
5334
|
-
//
|
|
5335
|
-
|
|
5336
|
-
//
|
|
5337
|
-
|
|
5338
|
-
// the launch config too. `?? undefined`: the field is `null` before start(), and a
|
|
5339
|
-
// strand cannot launch before start(), so this is belt and braces.
|
|
5340
|
-
strandPeerBook: this.strandPeerBookStore ?? undefined,
|
|
5352
|
+
// Where the strand node saves its network state and what it re-imports when it is
|
|
5353
|
+
// built. Retained with the launch config, so a hibernation wake rebuilds the node
|
|
5354
|
+
// over the table the quiesced node last saved. `?? undefined`: the field is `null`
|
|
5355
|
+
// before start(), and a strand cannot launch before start(), so this is belt and
|
|
5356
|
+
// braces.
|
|
5357
|
+
networkState: this.strandNetworkStateStore ?? undefined,
|
|
5341
5358
|
onRejoinBlocked: (blockedStrandId) => this.emit('strand:rejoin-blocked', { strandId: blockedStrandId }),
|
|
5342
5359
|
// The joiner's first-sync write gate (strand-first-sync-gate.ts): a launch that
|
|
5343
5360
|
// comes up `'syncing'` announces the moment its database is published.
|
|
@@ -5457,31 +5474,11 @@ export class CadreNode {
|
|
|
5457
5474
|
*/
|
|
5458
5475
|
async resolveCohortSeed(strandId, delegatePeerId) {
|
|
5459
5476
|
const siblings = await this.resolveSiblingSeed(strandId, delegatePeerId);
|
|
5460
|
-
// Sibling answers FIRST: they were resolved just now, while
|
|
5461
|
-
//
|
|
5477
|
+
// Sibling answers FIRST: they were resolved just now, while the formation-carried
|
|
5478
|
+
// addresses are as old as the formation. `unionAddrs` appends only what the siblings
|
|
5462
5479
|
// did not already name, so a fresher answer is never displaced and each source keeps
|
|
5463
|
-
// its own
|
|
5464
|
-
return unionAddrs(siblings, this.
|
|
5465
|
-
}
|
|
5466
|
-
/**
|
|
5467
|
-
* The strand peer book's contribution to a strand's seed: every live entry's
|
|
5468
|
-
* addresses, freshest peer first, minus this node's own entry when `selfPeerId` is
|
|
5469
|
-
* known (the book swap files one under the strand transport id; a node must never
|
|
5470
|
-
* dial itself). Empty before {@link start} builds the store.
|
|
5471
|
-
*/
|
|
5472
|
-
strandPeerBookAddrs(strandId, selfPeerId) {
|
|
5473
|
-
const store = this.strandPeerBookStore;
|
|
5474
|
-
if (!store) {
|
|
5475
|
-
return [];
|
|
5476
|
-
}
|
|
5477
|
-
const addrs = [];
|
|
5478
|
-
for (const entry of store.entries(strandId)) {
|
|
5479
|
-
if (entry.peerId === selfPeerId) {
|
|
5480
|
-
continue;
|
|
5481
|
-
}
|
|
5482
|
-
addrs.push(...entry.addrs);
|
|
5483
|
-
}
|
|
5484
|
-
return addrs;
|
|
5480
|
+
// its own ordering.
|
|
5481
|
+
return unionAddrs(siblings, this.formationStrandAddrs.get(strandId) ?? []);
|
|
5485
5482
|
}
|
|
5486
5483
|
/**
|
|
5487
5484
|
* The own-party half of {@link resolveCohortSeed}: strand-network addresses resolved
|
|
@@ -5697,18 +5694,19 @@ export class CadreNode {
|
|
|
5697
5694
|
this.recordDelegateAnnounces(due.map((relay) => relay.relayPeerId), strandId, now);
|
|
5698
5695
|
}
|
|
5699
5696
|
/**
|
|
5700
|
-
* Keep each running strand's own libp2p address book warm: re-merge
|
|
5701
|
-
*
|
|
5702
|
-
*
|
|
5697
|
+
* Keep each running strand's own libp2p address book warm: re-merge the address
|
|
5698
|
+
* records its FRET routing table holds on every pass
|
|
5699
|
+
* ({@link remergeStrandFretRecords}), and re-ask each connected SIBLING for its
|
|
5700
|
+
* strand addresses over the control mesh when that (sibling, strand) is due
|
|
5703
5701
|
* ({@link strandAddrAskDueAt}).
|
|
5704
5702
|
*
|
|
5705
|
-
* Without this, a strand's address book
|
|
5703
|
+
* Without this, cadre-core writes a strand's address book exactly once — the
|
|
5706
5704
|
* launch/resume seed — and everything below cadre-core that dials a strand peer
|
|
5707
5705
|
* by bare peer id (Optimystic's cluster and repo clients, FRET ping/announce)
|
|
5708
|
-
* loses its address for any
|
|
5706
|
+
* loses its address for any peer it is not currently connected to: a sibling
|
|
5709
5707
|
* that restarted its strand node or rotated its relay reservation is never
|
|
5710
|
-
* re-resolved, and
|
|
5711
|
-
* one-hour expiry (see `peer-addr-book.ts`).
|
|
5708
|
+
* re-resolved, and every address, another party's included, falls off at the
|
|
5709
|
+
* peerStore's one-hour expiry (see `peer-addr-book.ts`).
|
|
5712
5710
|
*
|
|
5713
5711
|
* Due times are per (sibling, strand) and set from each sibling's own outcome, so
|
|
5714
5712
|
* a sibling that connects late — a phone joining after the party's always-on
|
|
@@ -5789,15 +5787,15 @@ export class CadreNode {
|
|
|
5789
5787
|
}
|
|
5790
5788
|
/**
|
|
5791
5789
|
* One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings that are
|
|
5792
|
-
* due
|
|
5793
|
-
*
|
|
5794
|
-
* strand's failure never costs the others their refresh.
|
|
5790
|
+
* due and merge their answers into the strand's address book, then re-merge the
|
|
5791
|
+
* address records the strand node's FRET table holds. Errors are logged and
|
|
5792
|
+
* swallowed so one strand's failure never costs the others their refresh.
|
|
5795
5793
|
*
|
|
5796
|
-
* The
|
|
5797
|
-
*
|
|
5798
|
-
*
|
|
5799
|
-
*
|
|
5800
|
-
*
|
|
5794
|
+
* The sibling half runs first, so a failure reading the FRET table cannot cost a
|
|
5795
|
+
* sibling answer its merge. The FRET half, and the formation-carried addresses of
|
|
5796
|
+
* the peers it holds no record for, run on EVERY pass, whether or not any sibling is
|
|
5797
|
+
* due or connected — see {@link remergeStrandFretRecords} and
|
|
5798
|
+
* {@link remergeUnrecordedFormationAddrs} for why.
|
|
5801
5799
|
*/
|
|
5802
5800
|
async refreshOneStrandPeerAddrs(strandId, targets, now) {
|
|
5803
5801
|
const controlNode = this.controlNode;
|
|
@@ -5809,33 +5807,93 @@ export class CadreNode {
|
|
|
5809
5807
|
// Inside the try: a strand node torn down mid-pass may throw on any read, and one
|
|
5810
5808
|
// strand's failure must never cost the others their refresh.
|
|
5811
5809
|
const strandPeerId = strandNode.peerId.toString();
|
|
5812
|
-
const contactAddrs = this.strandPeerBookAddrs(strandId, strandPeerId);
|
|
5813
5810
|
const due = targets.filter((target) => now >= (this.strandAddrAskDueAt.get(peerStrandKey(target.peerId, strandId)) ?? 0));
|
|
5814
5811
|
// The running strand node's own peerId is the delegate to announce — see
|
|
5815
5812
|
// the relationship note on refreshStrandPeerAddrs.
|
|
5816
5813
|
const siblingAddrs = due.length === 0
|
|
5817
5814
|
? []
|
|
5818
5815
|
: await this.askSiblingsForStrandAddrs(controlNode, due, strandId, strandPeerId, now);
|
|
5819
|
-
|
|
5820
|
-
// already restarted onto a new node) must never have its store written to.
|
|
5821
|
-
if (!this._running || this.strandManager.getInstance(strandId)?.libp2pNode !== strandNode) {
|
|
5816
|
+
if (!this.isRunningStrandNode(strandId, strandNode)) {
|
|
5822
5817
|
return;
|
|
5823
5818
|
}
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
return;
|
|
5819
|
+
if (siblingAddrs.length > 0) {
|
|
5820
|
+
await this.mergeStrandPeerAddrs(strandNode, siblingAddrs, strandId);
|
|
5827
5821
|
}
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
// unchanged address set writes nothing in libp2p's persistent peer store, so it
|
|
5831
|
-
// is cheap today; if many strands run at once, merge only book entries changed
|
|
5832
|
-
// since the last tick plus those nearing the one-hour expiry.
|
|
5833
|
-
await this.mergeStrandPeerAddrs(strandNode, addrs, strandId);
|
|
5822
|
+
const recorded = await this.remergeStrandFretRecords(strandNode, strandId);
|
|
5823
|
+
await this.remergeUnrecordedFormationAddrs(strandNode, strandId, recorded);
|
|
5834
5824
|
}
|
|
5835
5825
|
catch (error) {
|
|
5836
5826
|
log('refreshStrandPeerAddrs: strand %s refresh failed (continuing): %o', strandId, error);
|
|
5837
5827
|
}
|
|
5838
5828
|
}
|
|
5829
|
+
/**
|
|
5830
|
+
* Is `strandNode` still the node `strandId` runs on? Asked again after every await
|
|
5831
|
+
* in a refresh pass: a strand stopped mid-pass, or one already restarted onto a new
|
|
5832
|
+
* node, must never have its store written to.
|
|
5833
|
+
*/
|
|
5834
|
+
isRunningStrandNode(strandId, strandNode) {
|
|
5835
|
+
return this._running && this.strandManager.getInstance(strandId)?.libp2pNode === strandNode;
|
|
5836
|
+
}
|
|
5837
|
+
/**
|
|
5838
|
+
* Re-merge, into a running strand node's address book, the addresses of every peer
|
|
5839
|
+
* its FRET routing table holds a signed address record for (`strand-fret-addrs.ts`).
|
|
5840
|
+
*
|
|
5841
|
+
* This is what keeps ANOTHER PARTY's strand nodes dialable from a node that stays
|
|
5842
|
+
* up. Nothing re-resolves their addresses — the strand-addr RPC answers own-party
|
|
5843
|
+
* siblings only — and the peerStore hides an address one hour after it was first
|
|
5844
|
+
* observed. FRET keeps each peer's record but hands it to the peerStore only when it
|
|
5845
|
+
* arrives or is imported, so without this pass a connection to another party that
|
|
5846
|
+
* drops after the first hour could not be redialed by either side.
|
|
5847
|
+
*
|
|
5848
|
+
* A record is held while FRET holds the peer's table entry, and at most 14 days after
|
|
5849
|
+
* it was last confirmed, so that is also how long a wrong address can keep being
|
|
5850
|
+
* re-merged. Each record is signed by the peer it names and verified before use, so
|
|
5851
|
+
* the exposure is failed dials to an address that peer itself published.
|
|
5852
|
+
*
|
|
5853
|
+
* NOTE: every pass serialises the whole FRET table (`exportTable()`), verifies one
|
|
5854
|
+
* signature per record and makes one `peerStore.merge` per peer, per running strand
|
|
5855
|
+
* per 15 s tick. Fine at strand table sizes, and an unchanged address set writes
|
|
5856
|
+
* nothing in libp2p's persistent peer store. If many strands or large tables make it
|
|
5857
|
+
* show up, read only the entries whose address stamp is near the one-hour mark.
|
|
5858
|
+
*
|
|
5859
|
+
* @returns the ids of the peers the table holds a usable record for.
|
|
5860
|
+
*/
|
|
5861
|
+
async remergeStrandFretRecords(strandNode, strandId) {
|
|
5862
|
+
const { peers, rejected } = await strandFretPeerAddrs(strandNode);
|
|
5863
|
+
const recorded = new Set(peers.keys());
|
|
5864
|
+
if ((peers.size === 0 && rejected === 0) || !this.isRunningStrandNode(strandId, strandNode)) {
|
|
5865
|
+
return recorded;
|
|
5866
|
+
}
|
|
5867
|
+
const counts = { merged: 0, restamped: 0, skipped: 0, failed: 0 };
|
|
5868
|
+
for (const [peerId, addrs] of peers) {
|
|
5869
|
+
counts[await mergePeerAddrs(strandNode, peerId, addrs)]++;
|
|
5870
|
+
}
|
|
5871
|
+
log('strand %s FRET address records re-merged (peers=%d, merged=%d, restamped=%d, failed=%d, rejected=%d)', strandId, peers.size, counts.merged, counts.restamped, counts.failed, rejected);
|
|
5872
|
+
return recorded;
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* Re-merge the addresses this strand's formation carried ({@link formationStrandAddrs})
|
|
5876
|
+
* for every peer the strand node's FRET table holds no record for (`recorded`).
|
|
5877
|
+
*
|
|
5878
|
+
* Until the first connection to the responder, the carried addresses are the only
|
|
5879
|
+
* thing naming it, and the launch seed writes them once. FRET's `bootstraps` keep only
|
|
5880
|
+
* the peer id, so if the responder stays unreachable past the peerStore's one-hour
|
|
5881
|
+
* expiry, nothing could dial it when it returns. Once FRET holds the peer's own signed
|
|
5882
|
+
* record, that record supersedes what the formation carried and this stops.
|
|
5883
|
+
*/
|
|
5884
|
+
async remergeUnrecordedFormationAddrs(strandNode, strandId, recorded) {
|
|
5885
|
+
const carried = this.formationStrandAddrs.get(strandId);
|
|
5886
|
+
if (!carried) {
|
|
5887
|
+
return;
|
|
5888
|
+
}
|
|
5889
|
+
const unrecorded = [...groupAddrsByPeerId(carried)]
|
|
5890
|
+
.filter(([peerId]) => !recorded.has(peerId))
|
|
5891
|
+
.flatMap(([, addrs]) => addrs.map((addr) => addr.toString()));
|
|
5892
|
+
if (unrecorded.length === 0 || !this.isRunningStrandNode(strandId, strandNode)) {
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
await this.mergeStrandPeerAddrs(strandNode, unrecorded, strandId);
|
|
5896
|
+
}
|
|
5839
5897
|
/**
|
|
5840
5898
|
* RPC `due` siblings for `strandId` and set each one's next due time from its own
|
|
5841
5899
|
* outcome: an answer, even an empty one, waits the full refresh interval; anything
|
|
@@ -5861,11 +5919,11 @@ export class CadreNode {
|
|
|
5861
5919
|
* attributed per peer. Best-effort throughout: an address-book write must never
|
|
5862
5920
|
* fail a launch, a resume, or a reconcile pass.
|
|
5863
5921
|
*
|
|
5864
|
-
* `addrs` is
|
|
5865
|
-
* {@link groupAddrsByPeerId} attributes each
|
|
5866
|
-
* peerId in its final `/p2p/` component — never
|
|
5867
|
-
* which names a different libp2p node entirely.
|
|
5868
|
-
* dropped there and counted here, once per pass.
|
|
5922
|
+
* `addrs` is a peer-agnostic list — the union the strand-addr RPC returns, or the
|
|
5923
|
+
* addresses a formation carried — so {@link groupAddrsByPeerId} attributes each
|
|
5924
|
+
* entry to the **strand transport** peerId in its final `/p2p/` component — never
|
|
5925
|
+
* the sibling's control peerId, which names a different libp2p node entirely.
|
|
5926
|
+
* Entries that name no peer are dropped there and counted here, once per pass.
|
|
5869
5927
|
*
|
|
5870
5928
|
* NOTE: a member could answer with arbitrary multiaddrs bound to arbitrary peer
|
|
5871
5929
|
* ids and poison this address book. That is the same exposure the launch-time
|
|
@@ -5875,14 +5933,13 @@ export class CadreNode {
|
|
|
5875
5933
|
* peerStore's one-hour expiry. No new gating here — cross-party strand trust is
|
|
5876
5934
|
* `backlog/strand-network-nat-relay-reachability`.
|
|
5877
5935
|
*
|
|
5878
|
-
* NOTE: one input
|
|
5879
|
-
* ({@link
|
|
5880
|
-
*
|
|
5881
|
-
*
|
|
5882
|
-
*
|
|
5883
|
-
*
|
|
5884
|
-
*
|
|
5885
|
-
* exposure is a handful of failed dials, not a trust hole.
|
|
5936
|
+
* NOTE: one input outlives that hour — a formation's carried addresses
|
|
5937
|
+
* ({@link formationStrandAddrs}) are in every launch and resume seed for as long as
|
|
5938
|
+
* this process remembers the formation, and re-merged on every refresh pass until the
|
|
5939
|
+
* strand node's FRET table holds the peer's own record, so a junk address from a
|
|
5940
|
+
* responder that is never met keeps being re-merged. Bounded by `sanitizeStrandAddrs`,
|
|
5941
|
+
* replaced by a re-formation, dropped on `unpublishStrand` / `forgetJoinedStrand`, and
|
|
5942
|
+
* still authority-free — so the exposure is a handful of failed dials, not a trust hole.
|
|
5886
5943
|
*/
|
|
5887
5944
|
async mergeStrandPeerAddrs(strandNode, addrs, strandId) {
|
|
5888
5945
|
const counts = { merged: 0, restamped: 0, skipped: 0, failed: 0 };
|
|
@@ -5931,7 +5988,7 @@ export class CadreNode {
|
|
|
5931
5988
|
// is normally a no-op; it is the guard for a bare listen addr and for a relay hop
|
|
5932
5989
|
// whose trailing `/p2p/` names the RELAY. An addr terminating in a DIFFERENT id is
|
|
5933
5990
|
// dropped rather than announced — it does not reach this node, and announcing it
|
|
5934
|
-
// would file our address under someone else's id in the receiver's book.
|
|
5991
|
+
// would file our address under someone else's id in the receiver's address book.
|
|
5935
5992
|
//
|
|
5936
5993
|
// Re-parsed through this package's own `multiaddr` rather than passed straight in:
|
|
5937
5994
|
// libp2p hands back its nested `@multiformats/multiaddr` copy, a structurally
|
|
@@ -5951,7 +6008,8 @@ export class CadreNode {
|
|
|
5951
6008
|
/**
|
|
5952
6009
|
* Leave a strand joined from another party, for the whole party: remove its party-wide
|
|
5953
6010
|
* `JoinedStrand` row (owner-signed, with a `Revocation` tombstone), then this machine's
|
|
5954
|
-
* unpublished record, its
|
|
6011
|
+
* unpublished record, its formation-carried addresses, its saved network state and any
|
|
6012
|
+
* session-kept entry, then
|
|
5955
6013
|
* {@link stopStrand} it here. Every other machine's watcher then sees the row gone and
|
|
5956
6014
|
* detaches the strand, a storage replica included; a machine offline at the time reaps the
|
|
5957
6015
|
* row once the tombstone reaches it. The joiner's counterpart of {@link unpublishStrand},
|
|
@@ -6008,8 +6066,9 @@ export class CadreNode {
|
|
|
6008
6066
|
* session here — the `strand:revoked` contract is that nothing is torn down for the app —
|
|
6009
6067
|
* and its party-wide row is queued for removal by the next connected owner reconcile pass
|
|
6010
6068
|
* (`JoinedStrandSession.forgetAfterThisSession`). A no-op for this party's own strands,
|
|
6011
|
-
* which have neither record; the
|
|
6012
|
-
* removed party must not keep dialing the
|
|
6069
|
+
* which have neither record; the formation-carried addresses and the saved network
|
|
6070
|
+
* state are forgotten either way, since a removed party must not keep dialing the
|
|
6071
|
+
* strand's peers.
|
|
6013
6072
|
*
|
|
6014
6073
|
* NOTE: accepted tradeoff — a sibling machine that has not raised `strand:revoked` itself
|
|
6015
6074
|
* detaches the strand (`strand:stopped`) when the party-wide removal reaches it, instead
|
|
@@ -6033,45 +6092,16 @@ export class CadreNode {
|
|
|
6033
6092
|
this.forgetStrandPeers(strandId, 'self-revocation');
|
|
6034
6093
|
}
|
|
6035
6094
|
/**
|
|
6036
|
-
* Forget a strand's
|
|
6037
|
-
*
|
|
6038
|
-
*
|
|
6095
|
+
* Forget what this node holds about a strand's peers: the addresses its formation
|
|
6096
|
+
* carried (see {@link formationStrandAddrs}) and its saved network state (see
|
|
6097
|
+
* {@link strandNetworkStateStore}). The store write is fire-and-log — the removal is
|
|
6098
|
+
* visible synchronously by the store's contract, and a failed persist only leaves the
|
|
6099
|
+
* entry on disk for the next start.
|
|
6039
6100
|
*/
|
|
6040
6101
|
forgetStrandPeers(strandId, caller) {
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
}
|
|
6045
|
-
/**
|
|
6046
|
-
* The strand peer book's observation writer (`StartStrandConfig.onStrandPeerIdentified`):
|
|
6047
|
-
* a strand node identified a strand peer at `observation.addrs`, so remember it as an
|
|
6048
|
-
* unsigned entry seen just now. The observer never reports self, and the store binds
|
|
6049
|
-
* every address to the peer id before filing it.
|
|
6050
|
-
*/
|
|
6051
|
-
observeStrandPeer(strandId, observation) {
|
|
6052
|
-
this.rememberStrandPeer(strandId, {
|
|
6053
|
-
peerId: observation.peerId,
|
|
6054
|
-
addrs: observation.addrs,
|
|
6055
|
-
issuedAt: 0,
|
|
6056
|
-
lastSeenAt: Date.now()
|
|
6057
|
-
}, 'observeStrandPeer');
|
|
6058
|
-
}
|
|
6059
|
-
/**
|
|
6060
|
-
* Merge one entry into the peer book. Fire-and-log: the entry is visible
|
|
6061
|
-
* synchronously by the store's contract, and the promise tracks durability only. A
|
|
6062
|
-
* persist failure costs restart survival, never this session's seed — the same trade
|
|
6063
|
-
* {@link retainDialTarget} makes.
|
|
6064
|
-
*/
|
|
6065
|
-
rememberStrandPeer(strandId, entry, caller) {
|
|
6066
|
-
const store = this.strandPeerBookStore;
|
|
6067
|
-
if (!store) {
|
|
6068
|
-
// Unreachable in production: start() builds the store before any strand can
|
|
6069
|
-
// launch or form. Logged rather than thrown, for the same reason as retainDialTarget.
|
|
6070
|
-
log('%s: no strand peer book yet — not remembering %s for strand %s', caller, entry.peerId, strandId);
|
|
6071
|
-
return;
|
|
6072
|
-
}
|
|
6073
|
-
void store.merge(strandId, entry).catch((error) => {
|
|
6074
|
-
log('%s: persisting strand %s peer %s failed (continuing): %o', caller, strandId, entry.peerId, error);
|
|
6102
|
+
this.formationStrandAddrs.delete(strandId);
|
|
6103
|
+
void this.strandNetworkStateStore?.forget(strandId).catch((error) => {
|
|
6104
|
+
log('%s: forgetting strand %s network state failed (continuing): %o', caller, strandId, error);
|
|
6075
6105
|
});
|
|
6076
6106
|
}
|
|
6077
6107
|
/**
|
|
@@ -7315,7 +7345,7 @@ export class CadreNode {
|
|
|
7315
7345
|
await this.initializeStrandSolicitation();
|
|
7316
7346
|
}
|
|
7317
7347
|
const result = await this.strandSolicitationService.formStrand(invitation, disclosure, this.controlNode);
|
|
7318
|
-
this.
|
|
7348
|
+
await this.recordFormationStrandAddrs(result.strandId, result.strandAddrs);
|
|
7319
7349
|
// A closed host strand's approval carries the joiner's own membership invitation —
|
|
7320
7350
|
// persist this party's identity key and stage the invitation for bring-up to redeem.
|
|
7321
7351
|
// Runs AFTER the addr recording so a persistence failure (which throws — see the
|
|
@@ -7550,41 +7580,39 @@ export class CadreNode {
|
|
|
7550
7580
|
'runtime did not wake within the formation provisioning budget (still waking)');
|
|
7551
7581
|
}
|
|
7552
7582
|
/**
|
|
7553
|
-
*
|
|
7554
|
-
*
|
|
7555
|
-
*
|
|
7556
|
-
* after every restart.
|
|
7583
|
+
* Keep the responder's strand-network addresses for a strand this node just formed
|
|
7584
|
+
* (see {@link formationStrandAddrs}), so the strand's discovery seed has something to
|
|
7585
|
+
* dial when the app launches it.
|
|
7557
7586
|
*
|
|
7558
7587
|
* The carried list is peer-agnostic (`sanitizeStrandAddrs` bounds and parses it,
|
|
7559
7588
|
* nothing more), so it is attributed per peer here (`groupAddrsByPeerId`, the same
|
|
7560
|
-
* rule the address-book merge applies) and
|
|
7561
|
-
*
|
|
7562
|
-
*
|
|
7563
|
-
*
|
|
7564
|
-
*
|
|
7565
|
-
*
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7589
|
+
* rule the address-book merge applies) and kept grouped by peer; an entry naming no
|
|
7590
|
+
* destination peer is dropped there. Scoped strictly to `strandId`: these addresses
|
|
7591
|
+
* reach ONE strand node of ONE other party and must never seed another strand's mesh
|
|
7592
|
+
* or the control peerStore.
|
|
7593
|
+
*
|
|
7594
|
+
* A list that names a peer REPLACES the strand's earlier one: the responder
|
|
7595
|
+
* disclosed its current addresses, so the older list is stale. An empty list, or one
|
|
7596
|
+
* naming no peer, records nothing, so it cannot wipe an earlier disclosure.
|
|
7597
|
+
*
|
|
7598
|
+
* A strand already running when this lands (a re-formation, which is the recovery
|
|
7599
|
+
* path for a dead address) gets the new addresses merged into its address book now.
|
|
7600
|
+
* Nothing else would deliver them: the seed is read only at launch and resume, and the
|
|
7601
|
+
* refresh pass skips a peer FRET already holds a record for — the stale one, here.
|
|
7602
|
+
*/
|
|
7603
|
+
async recordFormationStrandAddrs(strandId, strandAddrs) {
|
|
7604
|
+
const attributed = [...groupAddrsByPeerId([...strandAddrs]).values()]
|
|
7605
|
+
.flatMap((peerAddrs) => peerAddrs.map((addr) => addr.toString()));
|
|
7606
|
+
if (attributed.length === 0) {
|
|
7607
|
+
log('formStrand: responder disclosed no usable strand addrs for %s (%d carried) — cross-party seed unchanged', strandId, strandAddrs.length);
|
|
7570
7608
|
return;
|
|
7571
7609
|
}
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
issuedAt: 0,
|
|
7579
|
-
lastSeenAt: now
|
|
7580
|
-
}, 'formStrand');
|
|
7581
|
-
peers++;
|
|
7582
|
-
}
|
|
7583
|
-
// A strand already running when this lands (a re-formation) needs nothing more: the
|
|
7584
|
-
// refresh pass merges the book into its address book on every reconcile tick, so the
|
|
7585
|
-
// new addresses land within 15 s — which matters because re-forming is the recovery
|
|
7586
|
-
// path for a dead entry.
|
|
7587
|
-
log('formStrand: recorded %d cross-party strand addr(s) for %s under %d peer(s)', strandAddrs.length, strandId, peers);
|
|
7610
|
+
this.formationStrandAddrs.set(strandId, attributed);
|
|
7611
|
+
log('formStrand: kept %d of %d cross-party strand addr(s) for %s', attributed.length, strandAddrs.length, strandId);
|
|
7612
|
+
const runningNode = this.strandManager.getInstance(strandId)?.libp2pNode;
|
|
7613
|
+
if (runningNode) {
|
|
7614
|
+
await this.mergeStrandPeerAddrs(runningNode, attributed, strandId);
|
|
7615
|
+
}
|
|
7588
7616
|
}
|
|
7589
7617
|
/**
|
|
7590
7618
|
* Encode an open invitation for out-of-band delivery (QR, link, etc.).
|