@optimystic/db-p2p 1.0.0-beta.2 → 1.0.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/src/cluster/block-transfer-service.d.ts +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +172 -17
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +553 -149
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +241 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -7
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +119 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +181 -29
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +259 -76
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +716 -328
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +137 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +273 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +92 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +246 -103
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +3115 -2671
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1253
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +20 -7
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +1296 -1113
- package/src/repo/coordinator-repo.ts +3087 -2687
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +311 -73
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +329 -115
|
@@ -194,7 +194,7 @@ export class Libp2pKeyPeerNetwork {
|
|
|
194
194
|
* and near-zero for a dial-only client; do not quote the headline figure at a profile that
|
|
195
195
|
* never pays it. Every
|
|
196
196
|
* `findCluster` builds a cluster record containing self's addresses, and every commit calls
|
|
197
|
-
* `findCluster` through `
|
|
197
|
+
* `findCluster` through `ClusterCoordinator.resolveCohort` — so a cold `apply schema` paid one NIC sweep per
|
|
198
198
|
* commit. On a solo node with zero peers that was ~13.8 ms per call and **49% of the whole
|
|
199
199
|
* apply** (issue #8); the addresses it recomputed were identical every time.
|
|
200
200
|
*
|
|
@@ -352,7 +352,7 @@ export class Libp2pKeyPeerNetwork {
|
|
|
352
352
|
/**
|
|
353
353
|
* The caller-independent half of eligibility: this peer is neither excluded by the caller
|
|
354
354
|
* nor banned by reputation. Shared by all three places `findCoordinator` narrows a candidate
|
|
355
|
-
* list — the
|
|
355
|
+
* list — the cohort tier, the connected-peer fallback, and the retry-futility input — so the
|
|
356
356
|
* futility test can never disagree with the tiers about who is pickable.
|
|
357
357
|
*
|
|
358
358
|
* NOTE: eligibility here is deliberately blind to self-dialability, so on a relay
|
|
@@ -500,6 +500,14 @@ export class Libp2pKeyPeerNetwork {
|
|
|
500
500
|
recordPeerAddresses(peerId, multiaddrs) {
|
|
501
501
|
mergePeerAddresses(this.libp2p, peerId, multiaddrs, this.addressLog);
|
|
502
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* NOTE: a cached coordinator is trusted without re-deriving the key's cohort. A peer that
|
|
505
|
+
* has since LEFT the cohort (ring growth shifted the nearest set) is corrected on the next
|
|
506
|
+
* hop rather than here: the server-side responsibility check redirects to a current member
|
|
507
|
+
* (`RepoService.checkRedirect`, whose target overwrites this hint), and a peer that has gone
|
|
508
|
+
* away fails its dial and is excluded by the caller. Re-deriving the cohort on every cache
|
|
509
|
+
* hit would cost the same FRET walk and peerStore reads the cache exists to skip.
|
|
510
|
+
*/
|
|
503
511
|
getCachedCoordinator(key) {
|
|
504
512
|
const k = this.toCacheKey(key);
|
|
505
513
|
const hit = this.coordinatorCache.get(k);
|
|
@@ -570,12 +578,6 @@ export class Libp2pKeyPeerNetwork {
|
|
|
570
578
|
throw new Error('FRET service is not registered on this libp2p node');
|
|
571
579
|
return svc;
|
|
572
580
|
}
|
|
573
|
-
async getNeighborIdsForKey(key, wants) {
|
|
574
|
-
const fret = this.getFret();
|
|
575
|
-
const coord = await hashKey(key);
|
|
576
|
-
const both = fret.getNeighbors(coord, 'both', wants);
|
|
577
|
-
return Array.from(new Set(both)).slice(0, wants);
|
|
578
|
-
}
|
|
579
581
|
async findCoordinator(key, _options) {
|
|
580
582
|
const t0 = Date.now();
|
|
581
583
|
const excludedSet = new Set((_options?.excludedPeers ?? []).map(p => p.toString()));
|
|
@@ -583,12 +585,17 @@ export class Libp2pKeyPeerNetwork {
|
|
|
583
585
|
// intent is held to the stricter self-coordination bar.
|
|
584
586
|
const intent = _options?.intent ?? 'write';
|
|
585
587
|
const keyStr = this.toCacheKey(key).substring(0, 12);
|
|
588
|
+
const selfStr = this.libp2p.peerId.toString();
|
|
586
589
|
// Tracks whether the network-membership filter excluded an UNCONFIRMED candidate
|
|
587
590
|
// — `foreign` (another network) OR `unknown` (not yet confirmed to serve this
|
|
588
591
|
// network) — during any attempt. If selection ultimately fails with self
|
|
589
592
|
// unavailable, this lets us surface NO_NETWORK_COORDINATOR (the real cause)
|
|
590
593
|
// instead of the generic NO_COORDINATOR_AVAILABLE.
|
|
591
594
|
let droppedUnconfirmedAnyAttempt = false;
|
|
595
|
+
// The most recent attempt's cohort, kept for the last-resort tier below: self may
|
|
596
|
+
// coordinate a key only when it is among that key's responsible peers, and the
|
|
597
|
+
// verdict is the one the attempt that gave up on every better tier was working from.
|
|
598
|
+
let lastCohort;
|
|
592
599
|
this.log('findCoordinator:start key=%s excluded=%o', keyStr, Array.from(excludedSet).map(s => s.substring(0, 12)));
|
|
593
600
|
// honor cache if not excluded
|
|
594
601
|
const cached = this.getCachedCoordinator(key);
|
|
@@ -604,22 +611,29 @@ export class Libp2pKeyPeerNetwork {
|
|
|
604
611
|
const connected = (this.libp2p.getConnections?.() ?? []).map((c) => c.remotePeer);
|
|
605
612
|
const connectedSet = new Set(connected.map(p => p.toString()));
|
|
606
613
|
this.log('findCoordinator:connected-peers key=%s count=%d peers=%o attempt=%d', keyStr, connected.length, connected.map(p => p.toString().substring(0, 12)), attempt);
|
|
607
|
-
//
|
|
608
|
-
|
|
614
|
+
// The proximity band this attempt's cohort was cut from, for the retry-futility
|
|
615
|
+
// test below. Empty when the assembly threw.
|
|
616
|
+
let band = [];
|
|
617
|
+
// Cohort tier: the key's responsible peers, in proximity order, from the SAME
|
|
618
|
+
// assembly `findCluster` builds the replica set with — so the coordinator is
|
|
619
|
+
// always one of the peers that will hold the block, never a neighbour just
|
|
620
|
+
// outside that set.
|
|
609
621
|
try {
|
|
610
|
-
|
|
611
|
-
|
|
622
|
+
const assembled = await this.assembleServingCohort(key);
|
|
623
|
+
lastCohort = assembled.cohort;
|
|
624
|
+
band = assembled.band;
|
|
625
|
+
this.log('findCoordinator:cohort key=%s size=%d selfInCohort=%s', keyStr, assembled.cohort.length, assembled.cohort.includes(selfStr));
|
|
612
626
|
if (verbose)
|
|
613
|
-
this.log('findCoordinator:
|
|
614
|
-
// Filter to only connected
|
|
627
|
+
this.log('findCoordinator:cohort-candidates key=%s ids=%o connected=%o', keyStr, assembled.cohort, Array.from(connectedSet));
|
|
628
|
+
// Filter to only connected cohort members, excluding banned peers. Self is
|
|
615
629
|
// never "connected" to itself, so it is admitted by the explicit self clause
|
|
616
|
-
// below — but ONLY when the
|
|
617
|
-
//
|
|
618
|
-
// forming network
|
|
619
|
-
// SELF_COORDINATION_BLOCKED would never fire. On
|
|
620
|
-
// from the candidate list, so the connected-peer
|
|
621
|
-
// chance at a good remote peer; only if that also
|
|
622
|
-
// last-resort tier raise the accurate error.
|
|
630
|
+
// below — but ONLY when it is in the cohort (the list being filtered) AND the
|
|
631
|
+
// self-coordination guard allows it, otherwise a node that heads the cohort
|
|
632
|
+
// of nearly every key on a small or forming network would bypass the guard
|
|
633
|
+
// and the last-resort tier's SELF_COORDINATION_BLOCKED would never fire. On
|
|
634
|
+
// refusal self is merely DROPPED from the candidate list, so the connected-peer
|
|
635
|
+
// fallback below still gets its chance at a good remote peer; only if that also
|
|
636
|
+
// comes up empty does the last-resort tier raise the accurate error.
|
|
623
637
|
//
|
|
624
638
|
// An ISOLATED READ is the exception: with no connection left there is no better
|
|
625
639
|
// answer to wait for, and a deferrable denial is not evidence that answering
|
|
@@ -627,13 +641,12 @@ export class Libp2pKeyPeerNetwork {
|
|
|
627
641
|
// immediately instead of paying the ~1s retry loop before the last-resort tier
|
|
628
642
|
// degrades to the same answer. A WRITE keeps dropping self exactly as before,
|
|
629
643
|
// so a peer that lands during the retry window still wins the key.
|
|
630
|
-
const selfStr = this.libp2p.peerId.toString();
|
|
631
644
|
let selfAllowedThisAttempt;
|
|
632
|
-
// Memoized per ATTEMPT, and evaluated lazily so an all-remote
|
|
645
|
+
// Memoized per ATTEMPT, and evaluated lazily so an all-remote cohort never
|
|
633
646
|
// pays detectPartition() / getNetworkSizeEstimate(). Re-evaluated on each attempt
|
|
634
647
|
// because a connection can land during the 500ms inter-attempt sleep and
|
|
635
|
-
// legitimately flip the answer — as
|
|
636
|
-
// NOTE: on a small network self is
|
|
648
|
+
// legitimately flip the answer — as the assembly re-reads the peerStore.
|
|
649
|
+
// NOTE: on a small network self is in the cohort of nearly every key, so this runs
|
|
637
650
|
// per findCoordinator call and self-coordinated keys are never cached to absorb
|
|
638
651
|
// it. Fine while detectPartition()/getNetworkSizeEstimate() stay local FRET
|
|
639
652
|
// table reads; if either ever grows a probe or other network round-trip, cache
|
|
@@ -651,7 +664,7 @@ export class Libp2pKeyPeerNetwork {
|
|
|
651
664
|
// Gated on ISOLATION, not just on the read intent. Self carries no reputation
|
|
652
665
|
// record, so it scores 0 and sorts ahead of every remote candidate in the rank
|
|
653
666
|
// below — admitting it while a connection is live would hand the key to a node
|
|
654
|
-
// its own guard just called partitioned, over a reachable
|
|
667
|
+
// its own guard just called partitioned, over a reachable cohort member. And
|
|
655
668
|
// waiting costs a connected read nothing: the inter-attempt sleep further down
|
|
656
669
|
// only runs when `connected.length === 0`, so with peers present the remaining
|
|
657
670
|
// attempts and the last-resort degrade run back-to-back with no delay.
|
|
@@ -659,28 +672,28 @@ export class Libp2pKeyPeerNetwork {
|
|
|
659
672
|
&& intent === 'read' && connected.length === 0;
|
|
660
673
|
selfAllowedThisAttempt = decision.allow || degradedRead;
|
|
661
674
|
if (degradedRead) {
|
|
662
|
-
this.log('findCoordinator:
|
|
675
|
+
this.log('findCoordinator:cohort-self-degraded key=%s reason=%s intent=read attempt=%d', keyStr, decision.reason, attempt);
|
|
663
676
|
}
|
|
664
677
|
else if (!decision.allow) {
|
|
665
|
-
this.log('findCoordinator:
|
|
678
|
+
this.log('findCoordinator:cohort-self-dropped key=%s reason=%s intent=%s attempt=%d', keyStr, decision.reason, intent, attempt);
|
|
666
679
|
}
|
|
667
680
|
}
|
|
668
681
|
return selfAllowedThisAttempt;
|
|
669
682
|
};
|
|
670
|
-
const
|
|
683
|
+
const reachable = assembled.cohort
|
|
671
684
|
.filter(id => this.isSelectable(id, excludedSet))
|
|
672
685
|
.filter(id => connectedSet.has(id) || (id === selfStr && isSelfAdmissible()))
|
|
686
|
+
// Ranked by reputation, best (lowest) score first. The sort MUST be stable so
|
|
687
|
+
// that equal-score members keep their proximity order — otherwise two writers
|
|
688
|
+
// with the same cohort could name different coordinators for one block.
|
|
689
|
+
// `Array.prototype.sort` is stable in every supported runtime (ES2019+).
|
|
673
690
|
.sort((a, b) => (this.reputation?.getScore(a) ?? 0) - (this.reputation?.getScore(b) ?? 0));
|
|
674
|
-
this.log('findCoordinator:
|
|
675
|
-
// Network-membership scoping (no-op when protocolPrefix is unset)
|
|
676
|
-
// CONFIRMED to serve this network
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
// 3×500ms retry window a genuine same-network peer flips to 'serves' on a
|
|
681
|
-
// re-read of the peerStore and is selected normally on that attempt. Self
|
|
682
|
-
// always classifies as 'serves' and stays eligible.
|
|
683
|
-
const { ranked, droppedUnconfirmed } = await this.filterByMembership(connectedFretIds);
|
|
691
|
+
this.log('findCoordinator:cohort-reachable key=%s count=%d peers=%o', keyStr, reachable.length, reachable.map(s => s.substring(0, 12)));
|
|
692
|
+
// Network-membership scoping (no-op when protocolPrefix is unset). The cohort
|
|
693
|
+
// was already cut to peers CONFIRMED to serve this network by the assembly, so
|
|
694
|
+
// this is the final scope check over the same peerStore records — it costs no
|
|
695
|
+
// second read and cannot disagree with the assembly.
|
|
696
|
+
const { ranked, droppedUnconfirmed } = await this.filterByMembership(reachable, assembled.protocolsByPeer);
|
|
684
697
|
if (droppedUnconfirmed)
|
|
685
698
|
droppedUnconfirmedAnyAttempt = true;
|
|
686
699
|
const pick = ranked[0];
|
|
@@ -689,12 +702,12 @@ export class Libp2pKeyPeerNetwork {
|
|
|
689
702
|
// A self pick is a no-op here — recordCoordinator ignores self-valued
|
|
690
703
|
// writes (see its doc comment), matching the last-resort self tier below.
|
|
691
704
|
this.recordCoordinator(key, pid);
|
|
692
|
-
this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, '
|
|
705
|
+
this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'cohort');
|
|
693
706
|
return pid;
|
|
694
707
|
}
|
|
695
708
|
}
|
|
696
709
|
catch (err) {
|
|
697
|
-
this.log('findCoordinator
|
|
710
|
+
this.log('findCoordinator:cohort-assembly-failed key=%s attempt=%d - %o', keyStr, attempt, err);
|
|
698
711
|
}
|
|
699
712
|
// fallback: prefer any existing connected peer that's not excluded or banned,
|
|
700
713
|
// scoped to this network's serving peers (a `foreign` or not-yet-confirmed
|
|
@@ -702,7 +715,12 @@ export class Libp2pKeyPeerNetwork {
|
|
|
702
715
|
// connected REMOTE peers and never includes self, so when no serving peer is
|
|
703
716
|
// present selection falls through to the last-resort self-coordination block.
|
|
704
717
|
// Being remote-only, this tier needs no self-coordination guard check, unlike the
|
|
705
|
-
//
|
|
718
|
+
// cohort tier above.
|
|
719
|
+
// NOTE: this tier can pick a serving peer OUTSIDE the key's cohort when no cohort
|
|
720
|
+
// member is connected. That is a redirect hop, not a wrong placement: the receiving
|
|
721
|
+
// node's responsibility check redirects the request to a current cohort member (or
|
|
722
|
+
// refuses it). If redirect hops ever show up in profiles, prefer a not-connected
|
|
723
|
+
// cohort member we hold an address for over an out-of-cohort connected peer here.
|
|
706
724
|
const connectedCandidates = connected
|
|
707
725
|
.filter(p => this.isSelectable(p.toString(), excludedSet))
|
|
708
726
|
.sort((a, b) => (this.reputation?.getScore(a.toString()) ?? 0) - (this.reputation?.getScore(b.toString()) ?? 0))
|
|
@@ -720,14 +738,16 @@ export class Libp2pKeyPeerNetwork {
|
|
|
720
738
|
// If no connections and not the last attempt, wait and retry
|
|
721
739
|
if (connected.length === 0 && attempt < maxRetries - 1) {
|
|
722
740
|
// Exclusion/ban filtered — a neighbour we may never pick is not something to
|
|
723
|
-
// wait for.
|
|
724
|
-
//
|
|
725
|
-
// `
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
|
|
741
|
+
// wait for. Fed the whole proximity BAND rather than the cut cohort, and this
|
|
742
|
+
// network's membership filter (peerStore protocols) is deliberately NOT applied:
|
|
743
|
+
// a band member still `unknown` to it is exactly the peer that flips to `serves`
|
|
744
|
+
// inside the retry window, and a connection to ANY serving band member makes the
|
|
745
|
+
// connected fallback above succeed — so each one's presence must keep the window.
|
|
746
|
+
// FRET's own ring membership has already applied a stricter cut upstream —
|
|
747
|
+
// `assembleCohort` returns confirmed live ring members only — so a
|
|
748
|
+
// configured-but-never-reached bootstrap peer is absent from the band entirely,
|
|
749
|
+
// and only the dial-in-flight signal below can keep the window for it.
|
|
750
|
+
const knowable = band.filter(id => this.isSelectable(id, excludedSet));
|
|
731
751
|
if (!this.retryCouldImprove(knowable)) {
|
|
732
752
|
this.log('findCoordinator:retry-futile key=%s neighbors=%d dialsInFlight=%d mode=%s hwm=%d', keyStr, knowable.length, this.dialsInFlight(), this.networkMode, this.networkHighWaterMark);
|
|
733
753
|
break;
|
|
@@ -737,9 +757,21 @@ export class Libp2pKeyPeerNetwork {
|
|
|
737
757
|
continue;
|
|
738
758
|
}
|
|
739
759
|
}
|
|
740
|
-
// last resort:
|
|
760
|
+
// last resort: self, only if not excluded, only if self is among the key's responsible
|
|
761
|
+
// peers, and only if the guard allows. A node that is NOT responsible for the key — one
|
|
762
|
+
// that does not serve storage at all, or one that `clusterSize` nearer serving peers
|
|
763
|
+
// outrank — never coordinates it, however isolated it is: a self-only commit there would
|
|
764
|
+
// leave a copy nobody looks for and a responsible peer without one. `lastCohort` is
|
|
765
|
+
// undefined only when every attempt's assembly threw (FRET unavailable), which is read
|
|
766
|
+
// as "not known to be responsible".
|
|
767
|
+
// NOTE: this also fails an ISOLATED READ of a block this node is not responsible for, even
|
|
768
|
+
// when an older local copy exists (before the cohort rule, such a read degraded to the
|
|
769
|
+
// node's own replica). No effect while the serving peers number at most `clusterSize`,
|
|
770
|
+
// where every node is in every cohort. If offline reads on a wider network ever matter,
|
|
771
|
+
// let a read fall back to a local copy flagged as unverified rather than widening this tier.
|
|
741
772
|
const self = this.libp2p.peerId;
|
|
742
|
-
|
|
773
|
+
const selfInCohort = lastCohort?.includes(selfStr) ?? false;
|
|
774
|
+
if (!excludedSet.has(selfStr) && selfInCohort) {
|
|
743
775
|
const decision = this.shouldAllowSelfCoordination(intent);
|
|
744
776
|
// Only a HARD denial fails the caller. A deferrable one (see
|
|
745
777
|
// SelfCoordinationDecision.deferrable) means self is merely not the preferred
|
|
@@ -751,39 +783,44 @@ export class Libp2pKeyPeerNetwork {
|
|
|
751
783
|
throw new FindCoordinatorError(FIND_COORDINATOR_ERROR_CODES.SELF_COORDINATION_BLOCKED, `Self-coordination blocked: ${decision.reason}. No coordinator available for key.`);
|
|
752
784
|
}
|
|
753
785
|
if (!decision.allow) {
|
|
754
|
-
this.log('findCoordinator:self-selected-degraded key=%s coordinator=%s reason=%s intent=%s', keyStr,
|
|
786
|
+
this.log('findCoordinator:self-selected-degraded key=%s coordinator=%s reason=%s intent=%s', keyStr, selfStr.substring(0, 12), decision.reason, intent);
|
|
755
787
|
this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'self-degraded');
|
|
756
788
|
return self;
|
|
757
789
|
}
|
|
758
790
|
if (decision.warn) {
|
|
759
|
-
this.log('findCoordinator:self-selected-warn key=%s coordinator=%s reason=%s', keyStr,
|
|
791
|
+
this.log('findCoordinator:self-selected-warn key=%s coordinator=%s reason=%s', keyStr, selfStr.substring(0, 12), decision.reason);
|
|
760
792
|
}
|
|
761
793
|
else {
|
|
762
|
-
this.log('findCoordinator:self-selected key=%s coordinator=%s reason=%s', keyStr,
|
|
794
|
+
this.log('findCoordinator:self-selected key=%s coordinator=%s reason=%s', keyStr, selfStr.substring(0, 12), decision.reason);
|
|
763
795
|
}
|
|
764
796
|
this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'self');
|
|
765
797
|
return self;
|
|
766
798
|
}
|
|
767
|
-
// Self is excluded
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
799
|
+
// Self is unavailable (excluded by the caller, or not responsible for the key) and
|
|
800
|
+
// selection found no eligible peer. If the membership filter is the reason the
|
|
801
|
+
// candidate set emptied (the only other peers are `foreign` — serving a DIFFERENT
|
|
802
|
+
// network — or `unknown` — not yet confirmed to serve this network), surface a
|
|
803
|
+
// distinct, accurate cause instead of the generic codes below.
|
|
771
804
|
if (droppedUnconfirmedAnyAttempt) {
|
|
772
|
-
this.log('findCoordinator:no-network-coordinator key=%s prefix=%s self=%s', keyStr, this.protocolPrefix ?? '?',
|
|
805
|
+
this.log('findCoordinator:no-network-coordinator key=%s prefix=%s self=%s selfInCohort=%s', keyStr, this.protocolPrefix ?? '?', selfStr.substring(0, 12), selfInCohort);
|
|
773
806
|
throw new FindCoordinatorError(FIND_COORDINATOR_ERROR_CODES.NO_NETWORK_COORDINATOR, `No coordinator available for key on network ${this.protocolPrefix ?? '?'}: ` +
|
|
774
807
|
`the remaining candidate peer(s) are foreign or not-yet-confirmed to serve this network's cluster/repo protocol.`);
|
|
775
808
|
}
|
|
776
809
|
// Self is excluded. On a solo/bootstrap node (HWM<=1 and no other connected/FRET peers),
|
|
777
810
|
// this means the caller already tried self and the retry has nowhere to go — surface a
|
|
778
811
|
// distinct error so retry logic stops and the original first-attempt cause is preserved.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
this.log('findCoordinator:self-exhausted-solo key=%s self=%s', keyStr, self.toString().substring(0, 12));
|
|
812
|
+
if (excludedSet.has(selfStr) && this.networkHighWaterMark <= 1) {
|
|
813
|
+
this.log('findCoordinator:self-exhausted-solo key=%s self=%s', keyStr, selfStr.substring(0, 12));
|
|
782
814
|
throw new FindCoordinatorError(FIND_COORDINATOR_ERROR_CODES.SELF_COORDINATION_EXHAUSTED, 'Self-coordination exhausted on solo/bootstrap node (self already attempted). ' +
|
|
783
815
|
'The original first-attempt error describes the actual failure cause.');
|
|
784
816
|
}
|
|
785
|
-
|
|
786
|
-
|
|
817
|
+
const why = excludedSet.has(selfStr)
|
|
818
|
+
? 'all candidates excluded'
|
|
819
|
+
: lastCohort === undefined
|
|
820
|
+
? 'the responsible cohort could not be derived and no serving peer is connected'
|
|
821
|
+
: 'this node is not among the responsible peers and none of them is connected';
|
|
822
|
+
this.log('findCoordinator:no-coordinator key=%s self=%s selfInCohort=%s cohort=%d why=%s', keyStr, selfStr.substring(0, 12), selfInCohort, lastCohort?.length ?? -1, why);
|
|
823
|
+
throw new FindCoordinatorError(FIND_COORDINATOR_ERROR_CODES.NO_COORDINATOR_AVAILABLE, `No coordinator available for key (${why})`);
|
|
787
824
|
}
|
|
788
825
|
/**
|
|
789
826
|
* Connection-derived addresses for the cluster record, keyed by peer id.
|
|
@@ -806,74 +843,23 @@ export class Libp2pKeyPeerNetwork {
|
|
|
806
843
|
}
|
|
807
844
|
return byPeer;
|
|
808
845
|
}
|
|
846
|
+
// NOTE: accepted tradeoff — a node with no peers recomputes the self-only cohort on every call
|
|
847
|
+
// (~2 per distinct block read; 144 calls in a 67-object cold apply). Measured 0.009 ms/call on
|
|
848
|
+
// Node with a wildcard TCP listener (test/bench-findcluster.mjs, N=2000, 2026-09-11), so a memo
|
|
849
|
+
// would save ~1.3 ms per apply while adding an invalidation hazard: a node that gains a peer, or
|
|
850
|
+
// finishes identifying one, must stop answering self-only immediately, and read-repair recovery
|
|
851
|
+
// relies on that widening. Revisit if an on-device (React Native) profile shows findCluster as
|
|
852
|
+
// material; then memoize ONLY the solo answer, invalidated on connection:open and peer:identify.
|
|
809
853
|
async findCluster(key) {
|
|
810
854
|
const t0 = Date.now();
|
|
811
|
-
const fret = this.getFret();
|
|
812
|
-
const coord = await hashKey(key);
|
|
813
|
-
// When membership scoping is active, over-fetch a wider proximity band so the
|
|
814
|
-
// nearest peers that SERVE this network are in the candidate pool even if cross-
|
|
815
|
-
// network peers sit nearer the key (see membershipOverfetch).
|
|
816
|
-
const wants = this.protocolPrefix != null ? this.membershipOverfetch() : this.clusterSize;
|
|
817
|
-
const cohort = fret.assembleCohort(coord, wants);
|
|
818
855
|
const keyStr = this.toCacheKey(key).substring(0, 12);
|
|
819
856
|
this.log('findCluster:start key=%s', keyStr);
|
|
820
|
-
//
|
|
857
|
+
// The responsible peers for the key, in proximity order — self among them only when it
|
|
858
|
+
// is one of the nearest `clusterSize` serving peers (see `assembleServingCohort`). On the
|
|
859
|
+
// scoped path the assembly's peerStore reads are handed back so the address backfill
|
|
860
|
+
// below does not read the same records twice.
|
|
861
|
+
const { cohort: ids, band, peerStoreRecords } = await this.assembleServingCohort(key);
|
|
821
862
|
const selfId = this.libp2p.peerId.toString();
|
|
822
|
-
let ids = Array.from(new Set([...cohort, selfId]));
|
|
823
|
-
// Network-membership scoping (no-op when protocolPrefix is unset): a cohort
|
|
824
|
-
// member that serves a DIFFERENT network's protocol can never negotiate THIS
|
|
825
|
-
// network's cluster/repo dial, so it guarantees a super-majority failure rather
|
|
826
|
-
// than contributing a promise. Drop such 'foreign' members; build the cohort from
|
|
827
|
-
// positively-'serves' members only and NEVER admit a not-yet-identified ('unknown')
|
|
828
|
-
// member. A permanently cross-network peer and a freshly-discovered same-network
|
|
829
|
-
// peer mid-identify are indistinguishable while 'unknown' (both have an empty
|
|
830
|
-
// peerStore protocol list), so admitting an 'unknown' on the strength of a viability
|
|
831
|
-
// floor risks pulling a cross-network contaminant into the cohort — its repo dial
|
|
832
|
-
// then negotiates a different network's protocol and the whole write fails. A fresh
|
|
833
|
-
// same-network peer is not starved: it flips to 'serves' once identify completes and
|
|
834
|
-
// is re-included on the caller's retry, and in the meantime a self-only cohort still
|
|
835
|
-
// completes the write under allowClusterDownsize (the default).
|
|
836
|
-
// Scoped path only: one peerStore read per cohort member yields both protocols
|
|
837
|
-
// (for membership classification here) and addresses (reused at backfill below),
|
|
838
|
-
// so a finally-selected member isn't fetched from the peerStore twice. Left
|
|
839
|
-
// undefined on the unscoped path, which never classifies membership.
|
|
840
|
-
let peerStoreRecords;
|
|
841
|
-
if (this.protocolPrefix != null) {
|
|
842
|
-
// `cohort` is the over-fetched nearest-first band. Classify each non-self
|
|
843
|
-
// member, preserving proximity order within each tier.
|
|
844
|
-
const nonSelf = cohort.filter(id => id !== selfId);
|
|
845
|
-
peerStoreRecords = await this.getPeerStoreRecordsByPeer(nonSelf);
|
|
846
|
-
const serves = [];
|
|
847
|
-
const unknown = [];
|
|
848
|
-
let foreignDropped = 0;
|
|
849
|
-
for (const id of nonSelf) {
|
|
850
|
-
const m = this.membershipOf(id, peerStoreRecords[id]?.protocols);
|
|
851
|
-
if (m === 'serves')
|
|
852
|
-
serves.push(id);
|
|
853
|
-
else if (m === 'unknown')
|
|
854
|
-
unknown.push(id);
|
|
855
|
-
else
|
|
856
|
-
foreignDropped++;
|
|
857
|
-
}
|
|
858
|
-
// Take the nearest `clusterSize - 1` SERVING peers. Self is ALWAYS added below and
|
|
859
|
-
// counts toward `clusterSize` (matching the unscoped path, where `assembleCohort`
|
|
860
|
-
// returns the nearest `clusterSize` peers INCLUDING self when self is near the key —
|
|
861
|
-
// the coordinator case), so reserving a slot for self keeps a healthy same-network
|
|
862
|
-
// cohort at exactly `clusterSize` members rather than `clusterSize + 1`. Over-sizing
|
|
863
|
-
// would inflate the super-majority promise count (ceil(peerCount * threshold)) above
|
|
864
|
-
// what the configured `clusterSize` intends and hurt write availability. 'unknown'
|
|
865
|
-
// members are never backfilled: an 'unknown' peer may be a permanently cross-network
|
|
866
|
-
// contaminant whose repo dial cannot negotiate this network's protocol, and a fresh
|
|
867
|
-
// same-network peer mid-identify is indistinguishable from it. We therefore admit
|
|
868
|
-
// only positively-'serves' peers; when self is the sole serving member the cohort is
|
|
869
|
-
// self-only, which completes the write under allowClusterDownsize (the default) and
|
|
870
|
-
// re-includes any legitimate peer as 'serves' on the caller's retry once identify
|
|
871
|
-
// completes. `unknown.length` is still computed above for the diagnostic log line.
|
|
872
|
-
const nonSelfTarget = Math.max(0, this.clusterSize - 1);
|
|
873
|
-
const others = serves.slice(0, nonSelfTarget);
|
|
874
|
-
ids = Array.from(new Set([selfId, ...others]));
|
|
875
|
-
this.log('findCluster:membership key=%s serves=%d unknown=%d foreignDropped=%d kept=%d', keyStr, serves.length, unknown.length, foreignDropped, ids.length);
|
|
876
|
-
}
|
|
877
863
|
const connectedByPeer = this.getConnectedAddrsByPeer();
|
|
878
864
|
const connectedPeerIds = Object.keys(connectedByPeer);
|
|
879
865
|
// Backfill addresses from the peerStore for cohort members we don't have
|
|
@@ -888,7 +874,7 @@ export class Libp2pKeyPeerNetwork {
|
|
|
888
874
|
.map(id => [id, peerStoreRecords[id]?.addrs ?? []])
|
|
889
875
|
.filter(([, addrs]) => addrs.length > 0))
|
|
890
876
|
: await this.getPeerStoreAddrsByPeer(backfillIds);
|
|
891
|
-
this.log('findCluster key=%s
|
|
877
|
+
this.log('findCluster key=%s band=%d cohort=%d connected=%d', keyStr, band.length, ids.length, connectedPeerIds.length);
|
|
892
878
|
if (verbose)
|
|
893
879
|
this.log('findCluster:detail key=%s cohortPeers=%o connectedPeers=%o', keyStr, ids, connectedPeerIds);
|
|
894
880
|
const peers = {};
|
|
@@ -1037,31 +1023,139 @@ export class Libp2pKeyPeerNetwork {
|
|
|
1037
1023
|
return Math.max(this.clusterSize * 4, this.clusterSize + 16);
|
|
1038
1024
|
}
|
|
1039
1025
|
/**
|
|
1040
|
-
*
|
|
1041
|
-
* `
|
|
1042
|
-
|
|
1043
|
-
|
|
1026
|
+
* Does an advertised protocol list serve THIS network's storage — its namespaced `cluster`
|
|
1027
|
+
* or `repo` protocol? The one test behind every membership verdict, self's included.
|
|
1028
|
+
*/
|
|
1029
|
+
servesThisNetwork(protocols) {
|
|
1030
|
+
return protocols.includes(`${this.protocolPrefix}/cluster/1.0.0`)
|
|
1031
|
+
|| protocols.includes(`${this.protocolPrefix}/repo/1.0.0`);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Does THIS node serve storage on this network? True when no `protocolPrefix` is
|
|
1035
|
+
* configured (the unscoped path, where every peer counts as serving), or when libp2p
|
|
1036
|
+
* advertises this network's `cluster` or `repo` protocol — the same test
|
|
1037
|
+
* {@link membershipOf} applies to a remote peer's peerStore protocol list, so self is
|
|
1038
|
+
* classified on exactly the same footing. A libp2p double with no `getProtocols` counts
|
|
1039
|
+
* as serving, the convention `getConnections?.()` already follows for mocks.
|
|
1040
|
+
*
|
|
1041
|
+
* Every production node registers both storage services today (`createLibp2pNode`), so the
|
|
1042
|
+
* `false` branch is defence in depth: it is what keeps a future client-only node out of every
|
|
1043
|
+
* cohort and away from every coordinator pick.
|
|
1044
|
+
*/
|
|
1045
|
+
selfServes() {
|
|
1046
|
+
if (this.protocolPrefix == null)
|
|
1047
|
+
return true;
|
|
1048
|
+
const protocols = this.libp2p.getProtocols?.();
|
|
1049
|
+
if (protocols == null)
|
|
1050
|
+
return true;
|
|
1051
|
+
return this.servesThisNetwork(protocols);
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Classify a peer's network membership from its advertised protocols. Self is classified
|
|
1055
|
+
* from its own registered protocols ({@link selfServes}) rather than assumed to serve.
|
|
1056
|
+
* When no `protocolPrefix` is configured the filter is disabled and EVERY peer is
|
|
1057
|
+
* reported `serves`, so all callers behave exactly as before this scoping was added.
|
|
1044
1058
|
*/
|
|
1045
1059
|
membershipOf(idStr, protocols) {
|
|
1046
1060
|
if (this.protocolPrefix == null)
|
|
1047
1061
|
return 'serves';
|
|
1048
1062
|
if (idStr === this.libp2p.peerId.toString())
|
|
1049
|
-
return 'serves';
|
|
1063
|
+
return this.selfServes() ? 'serves' : 'foreign';
|
|
1050
1064
|
if (protocols == null || protocols.length === 0)
|
|
1051
1065
|
return 'unknown';
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1066
|
+
return this.servesThisNetwork(protocols) ? 'serves' : 'foreign';
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The ordered serving cohort for `key`: the nearest `clusterSize` peers that serve this
|
|
1070
|
+
* network, in proximity order, with THIS node among them only when it genuinely is one of
|
|
1071
|
+
* them. Both `findCluster` (the replica set) and `findCoordinator` (the pick) derive from
|
|
1072
|
+
* this one assembly, so the two can never disagree about who is responsible for a block.
|
|
1073
|
+
*
|
|
1074
|
+
* The rule, stated once:
|
|
1075
|
+
*
|
|
1076
|
+
* 1. `coord = hashKey(key)` — the only hash between a block id and its cohort.
|
|
1077
|
+
* 2. `band = fret.assembleCohort(coord, wants)`: the nearest live ring members, alternating
|
|
1078
|
+
* successor/predecessor outward from the coordinate; `wants` is the over-fetch width on
|
|
1079
|
+
* the scoped path (see {@link membershipOverfetch}) and `clusterSize` otherwise.
|
|
1080
|
+
* 3. FRET's ring store holds this node as a live member, so `band` already contains self
|
|
1081
|
+
* whenever self is among the nearest; absence means self is farther than every band
|
|
1082
|
+
* member. A serving self absent from the band is appended LAST, so the cut below keeps
|
|
1083
|
+
* it only when the band has room; a non-serving self is removed wherever it sits.
|
|
1084
|
+
* 4. Scoped path: keep only members whose membership is `serves`, order preserved.
|
|
1085
|
+
* 5. `cohort` = the first `clusterSize` of what remains.
|
|
1086
|
+
*
|
|
1087
|
+
* Consequences: a node with no live ring members other than itself gets a self-only cohort
|
|
1088
|
+
* (the solo short-circuits downstream are untouched); on a ring no wider than `clusterSize`
|
|
1089
|
+
* every serving node is in every cohort; on a wider ring self is in a block's cohort iff it
|
|
1090
|
+
* is among the nearest `clusterSize` serving members; a client-only node is in no cohort at
|
|
1091
|
+
* any width, and its `findCluster` may legitimately come back empty.
|
|
1092
|
+
*
|
|
1093
|
+
* NOTE: on the scoped path this classifies the WHOLE over-fetch band — one peerStore read per
|
|
1094
|
+
* band member, bounded by the number of live ring members — where the coordinator tier used
|
|
1095
|
+
* to classify only the connected neighbours. On a small network that is at most one read per
|
|
1096
|
+
* known peer. If peerStore reads per lookup ever show in a profile, memoize the per-peer
|
|
1097
|
+
* membership verdict with a short TTL rather than caching cohorts, which would have to be
|
|
1098
|
+
* invalidated on every ring change.
|
|
1099
|
+
*/
|
|
1100
|
+
async assembleServingCohort(key) {
|
|
1101
|
+
const fret = this.getFret();
|
|
1102
|
+
// The only hash between a block id and its cohort: `key` is the id's raw utf8 (`routingKeyForBlock`).
|
|
1103
|
+
const coord = await hashKey(key);
|
|
1104
|
+
const scoped = this.protocolPrefix != null;
|
|
1105
|
+
// When membership scoping is active, over-fetch a wider proximity band so the nearest
|
|
1106
|
+
// peers that SERVE this network are in the candidate pool even if cross-network peers
|
|
1107
|
+
// sit nearer the key (see membershipOverfetch).
|
|
1108
|
+
const wants = scoped ? this.membershipOverfetch() : this.clusterSize;
|
|
1109
|
+
const nearest = fret.assembleCohort(coord, wants);
|
|
1110
|
+
const selfId = this.libp2p.peerId.toString();
|
|
1111
|
+
const band = this.selfServes()
|
|
1112
|
+
? (nearest.includes(selfId) ? nearest : [...nearest, selfId])
|
|
1113
|
+
: nearest.filter(id => id !== selfId);
|
|
1114
|
+
if (!scoped)
|
|
1115
|
+
return { cohort: band.slice(0, this.clusterSize), band };
|
|
1116
|
+
// Network-membership scoping: a band member that serves a DIFFERENT network's protocol
|
|
1117
|
+
// can never negotiate THIS network's cluster/repo dial, so it guarantees a super-majority
|
|
1118
|
+
// failure rather than contributing a promise. Drop such 'foreign' members; build the
|
|
1119
|
+
// cohort from positively-'serves' members only and NEVER admit a not-yet-identified
|
|
1120
|
+
// ('unknown') member. A permanently cross-network peer and a freshly-discovered
|
|
1121
|
+
// same-network peer mid-identify are indistinguishable while 'unknown' (both have an
|
|
1122
|
+
// empty peerStore protocol list), so admitting an 'unknown' on the strength of a
|
|
1123
|
+
// viability floor risks pulling a cross-network contaminant into the cohort — its repo
|
|
1124
|
+
// dial then negotiates a different network's protocol and the whole write fails. A fresh
|
|
1125
|
+
// same-network peer is not starved: it flips to 'serves' once identify completes and is
|
|
1126
|
+
// re-included on the caller's retry, and in the meantime a self-only cohort (when self is
|
|
1127
|
+
// the only serving member known) still completes the write under allowClusterDownsize
|
|
1128
|
+
// (the default). One peerStore read per non-self band member yields both protocols (for
|
|
1129
|
+
// the classification here) and addresses (reused by `findCluster`'s backfill).
|
|
1130
|
+
const peerStoreRecords = await this.getPeerStoreRecordsByPeer(band.filter(id => id !== selfId));
|
|
1131
|
+
const serving = [];
|
|
1132
|
+
let unknown = 0;
|
|
1133
|
+
let foreign = 0;
|
|
1134
|
+
for (const id of band) {
|
|
1135
|
+
const m = this.membershipOf(id, peerStoreRecords[id]?.protocols);
|
|
1136
|
+
if (m === 'serves')
|
|
1137
|
+
serving.push(id);
|
|
1138
|
+
else if (m === 'unknown')
|
|
1139
|
+
unknown++;
|
|
1140
|
+
else
|
|
1141
|
+
foreign++;
|
|
1142
|
+
}
|
|
1143
|
+
const cohort = serving.slice(0, this.clusterSize);
|
|
1144
|
+
this.log('cohort:membership key=%s band=%d serves=%d unknown=%d foreign=%d cohort=%d selfInCohort=%s', this.toCacheKey(key).substring(0, 12), band.length, serving.length, unknown, foreign, cohort.length, cohort.includes(selfId));
|
|
1145
|
+
const protocolsByPeer = Object.fromEntries(Object.entries(peerStoreRecords).map(([id, r]) => [id, r.protocols]));
|
|
1146
|
+
return { cohort, band, peerStoreRecords, protocolsByPeer };
|
|
1056
1147
|
}
|
|
1057
1148
|
/**
|
|
1058
1149
|
* Scope a reputation-ordered candidate id list to this network for COORDINATOR
|
|
1059
|
-
* selection: keep ONLY peers confirmed to serve this network (`serves
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1150
|
+
* selection: keep ONLY peers confirmed to serve this network (`serves`), dropping both
|
|
1151
|
+
* `foreign` peers (serving another network) and `unknown` peers (peerStore protocol
|
|
1152
|
+
* list empty — not yet confirmed). Incoming (reputation) order is preserved among the
|
|
1153
|
+
* surviving `serves` peers. A no-op (returns the input unchanged, no drops) when
|
|
1154
|
+
* `protocolPrefix` is unset or the list is empty — the membership-disabled path is
|
|
1155
|
+
* therefore untouched.
|
|
1156
|
+
*
|
|
1157
|
+
* `protocolsByPeer`, when given, is a peerStore read the caller already made for these
|
|
1158
|
+
* peers (the cohort assembly's); otherwise the protocols are read fresh here.
|
|
1065
1159
|
*
|
|
1066
1160
|
* `droppedUnconfirmed` reports whether any candidate was excluded because it was not
|
|
1067
1161
|
* confirmed to serve this network — `foreign` OR `unknown` under scoping — so the
|
|
@@ -1072,15 +1166,15 @@ export class Libp2pKeyPeerNetwork {
|
|
|
1072
1166
|
* same-network peer that completes `identify` within the retry window flips to `serves`
|
|
1073
1167
|
* and is selected normally on that attempt.
|
|
1074
1168
|
*/
|
|
1075
|
-
async filterByMembership(ids) {
|
|
1169
|
+
async filterByMembership(ids, protocolsByPeer) {
|
|
1076
1170
|
if (this.protocolPrefix == null || ids.length === 0)
|
|
1077
1171
|
return { ranked: ids, droppedUnconfirmed: false };
|
|
1078
1172
|
const selfStr = this.libp2p.peerId.toString();
|
|
1079
|
-
const
|
|
1173
|
+
const protocols = protocolsByPeer ?? await this.getPeerStoreProtocolsByPeer(ids.filter(id => id !== selfStr));
|
|
1080
1174
|
const serves = [];
|
|
1081
1175
|
let droppedUnconfirmed = false;
|
|
1082
1176
|
for (const id of ids) {
|
|
1083
|
-
const m = this.membershipOf(id,
|
|
1177
|
+
const m = this.membershipOf(id, protocols[id]);
|
|
1084
1178
|
if (m === 'serves')
|
|
1085
1179
|
serves.push(id);
|
|
1086
1180
|
else
|