@optimystic/db-p2p 0.14.1 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +616 -609
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +4 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +52 -3
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +129 -6
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +3 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +182 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +429 -69
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/peer-key-binding.d.ts +25 -0
- package/dist/src/cluster/peer-key-binding.d.ts.map +1 -0
- package/dist/src/cluster/peer-key-binding.js +39 -0
- package/dist/src/cluster/peer-key-binding.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +72 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -0
- package/dist/src/cluster/quorum-restore.js +0 -0
- package/dist/src/cluster/quorum-restore.js.map +1 -0
- package/dist/src/cluster/rebalance-monitor.d.ts +23 -1
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +17 -2
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +7 -1
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +7 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +4 -1
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cluster/supermajority-coupling.d.ts +22 -0
- package/dist/src/cluster/supermajority-coupling.d.ts.map +1 -0
- package/dist/src/cluster/supermajority-coupling.js +27 -0
- package/dist/src/cluster/supermajority-coupling.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +46 -4
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-driver.js +62 -7
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +7 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.js +9 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts +197 -27
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +643 -87
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/dispute/arbitrator-selection.d.ts +68 -7
- package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -1
- package/dist/src/dispute/arbitrator-selection.js +105 -16
- package/dist/src/dispute/arbitrator-selection.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +1 -1
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +3 -2
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +32 -3
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +82 -9
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +1 -1
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +1 -1
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +25 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +14 -2
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +7 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/it-utility.d.ts +1 -1
- package/dist/src/it-utility.d.ts.map +1 -1
- package/dist/src/it-utility.js +4 -13
- package/dist/src/it-utility.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +98 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +272 -17
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +12 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +441 -89
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +2 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +6 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/matchmaking/traffic-validation.js +1 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +8 -4
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +27 -0
- package/dist/src/owned-block-seed.d.ts.map +1 -0
- package/dist/src/owned-block-seed.js +37 -0
- package/dist/src/owned-block-seed.js.map +1 -0
- package/dist/src/protocol-client.d.ts +1 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +9 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/protocol-limits.d.ts +32 -0
- package/dist/src/protocol-limits.d.ts.map +1 -0
- package/dist/src/protocol-limits.js +32 -0
- package/dist/src/protocol-limits.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +3 -0
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +41 -4
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +128 -80
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +38 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +92 -13
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +7 -1
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/peer-reputation.js +1 -1
- package/dist/src/reputation/peer-reputation.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -0
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +5 -1
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +7 -1
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts +30 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.js +15 -2
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
- package/dist/src/storage/arachnode-partition.d.ts +49 -0
- package/dist/src/storage/arachnode-partition.d.ts.map +1 -0
- package/dist/src/storage/arachnode-partition.js +61 -0
- package/dist/src/storage/arachnode-partition.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +40 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +150 -57
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +8 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +13 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +45 -0
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/kv-raw-storage.js +113 -0
- package/dist/src/storage/kv-raw-storage.js.map +1 -0
- package/dist/src/storage/memory-storage.d.ts +20 -58
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +22 -148
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +39 -0
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -0
- package/dist/src/storage/memory-store-driver.js +129 -0
- package/dist/src/storage/memory-store-driver.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +10 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -0
- package/dist/src/storage/raw-store-codec.js +36 -0
- package/dist/src/storage/raw-store-codec.js.map +1 -0
- package/dist/src/storage/raw-store-driver.d.ts +69 -0
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -0
- package/dist/src/storage/raw-store-driver.js +2 -0
- package/dist/src/storage/raw-store-driver.js.map +1 -0
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts +73 -0
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -0
- package/dist/src/storage/restoration-coordinator.js +181 -0
- package/dist/src/storage/restoration-coordinator.js.map +1 -0
- package/dist/src/storage/ring-selector.d.ts +92 -5
- package/dist/src/storage/ring-selector.d.ts.map +1 -1
- package/dist/src/storage/ring-selector.js +153 -51
- package/dist/src/storage/ring-selector.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts +128 -0
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -0
- package/dist/src/storage/ring-shift-coordinator.js +196 -0
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -0
- package/dist/src/storage/storage-monitor.d.ts +28 -0
- package/dist/src/storage/storage-monitor.d.ts.map +1 -1
- package/dist/src/storage/storage-monitor.js +42 -2
- package/dist/src/storage/storage-monitor.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +19 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +172 -22
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -1
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +2 -1
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +28 -4
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +51 -10
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/index.d.ts +1 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +15 -7
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +24 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -0
- package/dist/src/testing/raw-storage-conformance.js +313 -0
- package/dist/src/testing/raw-storage-conformance.js.map +1 -0
- package/package.json +4 -4
- package/src/cluster/block-transfer-service.ts +4 -3
- package/src/cluster/block-transfer.ts +424 -265
- package/src/cluster/client.ts +3 -1
- package/src/cluster/cluster-repo.ts +481 -71
- package/src/cluster/peer-key-binding.ts +37 -0
- package/src/cluster/quorum-restore.ts +0 -0
- package/src/cluster/rebalance-monitor.ts +34 -3
- package/src/cluster/service.ts +7 -1
- package/src/cluster/spread-on-churn.ts +11 -1
- package/src/cluster/supermajority-coupling.ts +37 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +96 -9
- package/src/cohort-topic/cohort-gossip-transport.ts +10 -0
- package/src/cohort-topic/host.ts +831 -105
- package/src/dispute/arbitrator-selection.ts +138 -28
- package/src/dispute/cascade.ts +8 -1
- package/src/dispute/client.ts +3 -2
- package/src/dispute/dispute-service.ts +94 -9
- package/src/dispute/index.ts +7 -1
- package/src/dispute/invalidation.ts +40 -2
- package/src/dispute/service.ts +7 -1
- package/src/index.ts +46 -43
- package/src/it-utility.ts +5 -13
- package/src/libp2p-key-network.ts +281 -18
- package/src/libp2p-node-base.ts +511 -81
- package/src/matchmaking/query-transport.ts +9 -0
- package/src/network/network-manager-service.ts +8 -2
- package/src/owned-block-seed.ts +40 -0
- package/src/protocol-client.ts +10 -3
- package/src/protocol-limits.ts +33 -0
- package/src/repo/client.ts +3 -0
- package/src/repo/cluster-coordinator.ts +145 -82
- package/src/repo/coordinator-repo.ts +96 -16
- package/src/repo/service.ts +7 -1
- package/src/reputation/types.ts +3 -0
- package/src/rn.ts +7 -1
- package/src/storage/arachnode-fret-adapter.ts +33 -2
- package/src/storage/arachnode-partition.ts +74 -0
- package/src/storage/block-storage.ts +169 -61
- package/src/storage/i-block-storage.ts +9 -0
- package/src/storage/i-raw-storage.ts +14 -0
- package/src/storage/kv-raw-storage.ts +135 -0
- package/src/storage/memory-storage.ts +25 -173
- package/src/storage/memory-store-driver.ts +157 -0
- package/src/storage/raw-store-codec.ts +42 -0
- package/src/storage/raw-store-driver.ts +80 -0
- package/src/storage/{restoration-coordinator-v2.ts → restoration-coordinator.ts} +43 -37
- package/src/storage/ring-selector.ts +317 -176
- package/src/storage/ring-shift-coordinator.ts +271 -0
- package/src/storage/storage-monitor.ts +70 -3
- package/src/storage/storage-repo.ts +710 -558
- package/src/sync/client.ts +5 -1
- package/src/sync/service.ts +2 -1
- package/src/testing/cohort-topic-mesh-harness.ts +61 -11
- package/src/testing/index.ts +1 -0
- package/src/testing/mesh-harness.ts +14 -6
- package/src/testing/raw-storage-conformance.ts +397 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AbortOptions, Libp2p, PeerId, Stream } from "@libp2p/interface";
|
|
1
|
+
import type { AbortOptions, Connection, Libp2p, PeerId, Stream } from "@libp2p/interface";
|
|
2
2
|
import { toString as u8ToString } from 'uint8arrays'
|
|
3
3
|
import type { ClusterPeers, FindCoordinatorOptions, IKeyNetwork, IPeerNetwork } from "@optimystic/db-core";
|
|
4
4
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
@@ -31,12 +31,33 @@ export const FIND_COORDINATOR_ERROR_CODES = {
|
|
|
31
31
|
*/
|
|
32
32
|
SELF_COORDINATION_EXHAUSTED: 'SELF_COORDINATION_EXHAUSTED',
|
|
33
33
|
/** No peer (including self) is an eligible coordinator. */
|
|
34
|
-
NO_COORDINATOR_AVAILABLE: 'NO_COORDINATOR_AVAILABLE'
|
|
34
|
+
NO_COORDINATOR_AVAILABLE: 'NO_COORDINATOR_AVAILABLE',
|
|
35
|
+
/**
|
|
36
|
+
* The candidate set was non-empty but every non-self candidate serves a
|
|
37
|
+
* DIFFERENT network's protocol (or none of this network's). Distinct from
|
|
38
|
+
* NO_COORDINATOR_AVAILABLE so a Sereus-style trace points at the real cause —
|
|
39
|
+
* "peer(s) do not serve this network's protocol" — instead of a generic
|
|
40
|
+
* "all candidates excluded" / super-majority failure.
|
|
41
|
+
*/
|
|
42
|
+
NO_NETWORK_COORDINATOR: 'NO_NETWORK_COORDINATOR'
|
|
35
43
|
} as const;
|
|
36
44
|
|
|
37
45
|
export type FindCoordinatorErrorCode =
|
|
38
46
|
typeof FIND_COORDINATOR_ERROR_CODES[keyof typeof FIND_COORDINATOR_ERROR_CODES];
|
|
39
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Network-membership classification of a peer relative to THIS node's network,
|
|
50
|
+
* derived from the peer's libp2p peerStore protocol list:
|
|
51
|
+
* - `serves` — advertises this network's namespaced `cluster`/`repo` protocol.
|
|
52
|
+
* - `foreign` — has a non-empty protocol list but none for this network → another network.
|
|
53
|
+
* - `unknown` — protocol list empty / peer absent → identify not yet completed. This is
|
|
54
|
+
* both a fresh same-network peer (will flip to `serves`) AND a cross-network
|
|
55
|
+
* peer (whose network-namespaced identify can NEVER complete, so it stays
|
|
56
|
+
* `unknown` forever) — indistinguishable at a single instant, separated over
|
|
57
|
+
* the retry/stabilization window.
|
|
58
|
+
*/
|
|
59
|
+
export type NetworkMembership = 'serves' | 'foreign' | 'unknown';
|
|
60
|
+
|
|
40
61
|
export class FindCoordinatorError extends Error {
|
|
41
62
|
readonly code: FindCoordinatorErrorCode;
|
|
42
63
|
constructor(code: FindCoordinatorErrorCode, message: string) {
|
|
@@ -94,7 +115,16 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
94
115
|
selfCoordinationConfig?: SelfCoordinationConfig,
|
|
95
116
|
networkMode?: NetworkMode,
|
|
96
117
|
persistence?: NetworkStatePersistence,
|
|
97
|
-
private readonly reputation?: IPeerReputation
|
|
118
|
+
private readonly reputation?: IPeerReputation,
|
|
119
|
+
/**
|
|
120
|
+
* Network-namespaced protocol prefix (`/optimystic/<networkName>`). When
|
|
121
|
+
* provided, coordinator/cohort selection is scoped to peers that serve THIS
|
|
122
|
+
* network's `cluster`/`repo` protocol, so a peer that only belongs to another
|
|
123
|
+
* network sharing the same physical nodes/bootstraps is never chosen. When
|
|
124
|
+
* ABSENT, the membership filter is disabled (today's exact behavior) — required
|
|
125
|
+
* for backward compatibility because most call sites don't know the network name.
|
|
126
|
+
*/
|
|
127
|
+
private readonly protocolPrefix?: string
|
|
98
128
|
) {
|
|
99
129
|
this.selfCoordinationConfig = {
|
|
100
130
|
gracePeriodMs: selfCoordinationConfig?.gracePeriodMs ?? 30_000,
|
|
@@ -291,17 +321,39 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
291
321
|
return undefined
|
|
292
322
|
}
|
|
293
323
|
|
|
324
|
+
/**
|
|
325
|
+
* True for a circuit-relay ("limited") connection. libp2p stamps a relayed
|
|
326
|
+
* connection with `limits` (per-circuit data/duration caps); we additionally
|
|
327
|
+
* sniff the multiaddr for `/p2p-circuit` as a fallback for transports/versions
|
|
328
|
+
* that don't populate `limits`.
|
|
329
|
+
*/
|
|
330
|
+
private isLimitedConnection(c: Connection): boolean {
|
|
331
|
+
if ((c as { limits?: unknown }).limits != null) return true
|
|
332
|
+
const addr = c.remoteAddr?.toString?.()
|
|
333
|
+
return addr != null && addr.includes('/p2p-circuit')
|
|
334
|
+
}
|
|
335
|
+
|
|
294
336
|
connect(peerId: PeerId, protocol: string, options?: AbortOptions): Promise<Stream> {
|
|
295
337
|
const conns = this.libp2p.getConnections?.(peerId) ?? []
|
|
296
338
|
// Filter to only-open connections so a closing/closed entry that libp2p
|
|
297
339
|
// hasn't yet evicted from its index doesn't get picked up here.
|
|
298
|
-
const open = conns.
|
|
299
|
-
|
|
340
|
+
const open = conns.filter(c => c?.status === 'open' && typeof c?.newStream === 'function')
|
|
341
|
+
// Prefer a DIRECT connection over a limited (circuit-relay) one for the RPC.
|
|
342
|
+
// A relayed/limited connection can be reset by the relay once a per-circuit
|
|
343
|
+
// cap or reservation lapses (@libp2p/circuit-relay-v2), surfacing to the
|
|
344
|
+
// coordinator as a StreamResetError that fails consensus. After DCUtR upgrades
|
|
345
|
+
// a relayed link to direct, both connections briefly coexist — picking the
|
|
346
|
+
// direct one avoids riding the soon-to-be-reset circuit. We only fall back to
|
|
347
|
+
// the limited connection (with runOnLimitedConnection) when it is the only open
|
|
348
|
+
// path — the steady state for browsers and NATed peers before any upgrade.
|
|
349
|
+
const chosen = open.find(c => !this.isLimitedConnection(c)) ?? open[0]
|
|
350
|
+
if (chosen) {
|
|
300
351
|
// runOnLimitedConnection: true is required to open a stream over a
|
|
301
352
|
// circuit-relay (limited) connection — the steady-state path for
|
|
302
353
|
// browsers and NATed peers. Without it, the warm relay connection
|
|
303
|
-
// from a prior dialProtocol cannot be reused on subsequent RPCs.
|
|
304
|
-
|
|
354
|
+
// from a prior dialProtocol cannot be reused on subsequent RPCs. It is
|
|
355
|
+
// a harmless no-op on the preferred direct connection.
|
|
356
|
+
return chosen.newStream([protocol], {
|
|
305
357
|
signal: options?.signal,
|
|
306
358
|
runOnLimitedConnection: true,
|
|
307
359
|
negotiateFully: false
|
|
@@ -332,6 +384,12 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
332
384
|
const t0 = Date.now();
|
|
333
385
|
const excludedSet = new Set<string>((_options?.excludedPeers ?? []).map(p => p.toString()))
|
|
334
386
|
const keyStr = this.toCacheKey(key).substring(0, 12);
|
|
387
|
+
// Tracks whether the network-membership filter excluded an UNCONFIRMED candidate
|
|
388
|
+
// — `foreign` (another network) OR `unknown` (not yet confirmed to serve this
|
|
389
|
+
// network) — during any attempt. If selection ultimately fails with self
|
|
390
|
+
// unavailable, this lets us surface NO_NETWORK_COORDINATOR (the real cause)
|
|
391
|
+
// instead of the generic NO_COORDINATOR_AVAILABLE.
|
|
392
|
+
let droppedUnconfirmedAnyAttempt = false;
|
|
335
393
|
|
|
336
394
|
this.log('findCoordinator:start key=%s excluded=%o', keyStr, Array.from(excludedSet).map(s => s.substring(0, 12)))
|
|
337
395
|
|
|
@@ -367,7 +425,17 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
367
425
|
.sort((a, b) => (this.reputation?.getScore(a) ?? 0) - (this.reputation?.getScore(b) ?? 0))
|
|
368
426
|
this.log('findCoordinator:fret-connected key=%s count=%d peers=%o', keyStr, connectedFretIds.length, connectedFretIds.map(s => s.substring(0, 12)))
|
|
369
427
|
|
|
370
|
-
|
|
428
|
+
// Network-membership scoping (no-op when protocolPrefix is unset): only a peer
|
|
429
|
+
// CONFIRMED to serve this network ('serves') is eligible — both `foreign`
|
|
430
|
+
// (another network) and `unknown` (not yet identified) peers are excluded
|
|
431
|
+
// from selection. A cross-network peer is permanently 'unknown' (its
|
|
432
|
+
// namespaced identify never completes), so it is never gambled on; over the
|
|
433
|
+
// 3×500ms retry window a genuine same-network peer flips to 'serves' on a
|
|
434
|
+
// re-read of the peerStore and is selected normally on that attempt. Self
|
|
435
|
+
// always classifies as 'serves' and stays eligible.
|
|
436
|
+
const { ranked, droppedUnconfirmed } = await this.filterByMembership(connectedFretIds)
|
|
437
|
+
if (droppedUnconfirmed) droppedUnconfirmedAnyAttempt = true
|
|
438
|
+
const pick = ranked[0]
|
|
371
439
|
if (pick) {
|
|
372
440
|
const pid = peerIdFromString(pick)
|
|
373
441
|
this.recordCoordinator(key, pid)
|
|
@@ -378,15 +446,23 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
378
446
|
this.log('findCoordinator getNeighborIdsForKey failed - %o', err)
|
|
379
447
|
}
|
|
380
448
|
|
|
381
|
-
// fallback: prefer any existing connected peer that's not excluded or banned
|
|
382
|
-
|
|
449
|
+
// fallback: prefer any existing connected peer that's not excluded or banned,
|
|
450
|
+
// scoped to this network's serving peers (a `foreign` or not-yet-confirmed
|
|
451
|
+
// `unknown` peer is never picked). Note this candidate set is built from
|
|
452
|
+
// connected REMOTE peers and never includes self, so when no serving peer is
|
|
453
|
+
// present selection falls through to the last-resort self-coordination block.
|
|
454
|
+
const connectedCandidates = connected
|
|
383
455
|
.filter(p => !excludedSet.has(p.toString()) && !(this.reputation?.isBanned(p.toString())))
|
|
384
456
|
.sort((a, b) => (this.reputation?.getScore(a.toString()) ?? 0) - (this.reputation?.getScore(b.toString()) ?? 0))
|
|
385
|
-
|
|
457
|
+
.map(p => p.toString())
|
|
458
|
+
const { ranked: connRanked, droppedUnconfirmed: connDroppedUnconfirmed } = await this.filterByMembership(connectedCandidates)
|
|
459
|
+
if (connDroppedUnconfirmed) droppedUnconfirmedAnyAttempt = true
|
|
460
|
+
const connectedPick = connRanked[0]
|
|
386
461
|
if (connectedPick) {
|
|
387
|
-
|
|
462
|
+
const pid = peerIdFromString(connectedPick)
|
|
463
|
+
this.recordCoordinator(key, pid)
|
|
388
464
|
this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'connected-fallback')
|
|
389
|
-
return
|
|
465
|
+
return pid
|
|
390
466
|
}
|
|
391
467
|
|
|
392
468
|
// If no connections and not the last attempt, wait and retry
|
|
@@ -424,6 +500,20 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
424
500
|
return self
|
|
425
501
|
}
|
|
426
502
|
|
|
503
|
+
// Self is excluded and selection found no eligible peer. If the membership filter is
|
|
504
|
+
// the reason the candidate set emptied (the only other peers are `foreign` — serving
|
|
505
|
+
// a DIFFERENT network — or `unknown` — not yet confirmed to serve this network),
|
|
506
|
+
// surface a distinct, accurate cause instead of the generic codes below.
|
|
507
|
+
if (droppedUnconfirmedAnyAttempt) {
|
|
508
|
+
this.log('findCoordinator:no-network-coordinator key=%s prefix=%s self=%s',
|
|
509
|
+
keyStr, this.protocolPrefix ?? '?', self.toString().substring(0, 12))
|
|
510
|
+
throw new FindCoordinatorError(
|
|
511
|
+
FIND_COORDINATOR_ERROR_CODES.NO_NETWORK_COORDINATOR,
|
|
512
|
+
`No coordinator available for key on network ${this.protocolPrefix ?? '?'}: ` +
|
|
513
|
+
`the remaining candidate peer(s) are foreign or not-yet-confirmed to serve this network's cluster/repo protocol.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
427
517
|
// Self is excluded. On a solo/bootstrap node (HWM<=1 and no other connected/FRET peers),
|
|
428
518
|
// this means the caller already tried self and the retry has nowhere to go — surface a
|
|
429
519
|
// distinct error so retry logic stops and the original first-attempt cause is preserved.
|
|
@@ -458,7 +548,7 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
458
548
|
private parseMultiaddrs(addrs: string[]): string[] {
|
|
459
549
|
const out: string[] = []
|
|
460
550
|
for (const a of addrs) {
|
|
461
|
-
try { multiaddr(a); out.push(a) } catch (err) {
|
|
551
|
+
try { multiaddr(a); out.push(a) } catch (err) { this.log('WARN: invalid multiaddr from connection %s %o', a, err) }
|
|
462
552
|
}
|
|
463
553
|
return out
|
|
464
554
|
}
|
|
@@ -467,13 +557,70 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
467
557
|
const t0 = Date.now();
|
|
468
558
|
const fret = this.getFret()
|
|
469
559
|
const coord = await hashKey(key)
|
|
470
|
-
|
|
560
|
+
// When membership scoping is active, over-fetch a wider proximity band so the
|
|
561
|
+
// nearest peers that SERVE this network are in the candidate pool even if cross-
|
|
562
|
+
// network peers sit nearer the key (see membershipOverfetch).
|
|
563
|
+
const wants = this.protocolPrefix != null ? this.membershipOverfetch() : this.clusterSize
|
|
564
|
+
const cohort = fret.assembleCohort(coord, wants)
|
|
471
565
|
const keyStr = this.toCacheKey(key).substring(0, 12);
|
|
472
566
|
this.log('findCluster:start key=%s', keyStr);
|
|
473
567
|
|
|
474
568
|
// Include self in the cohort
|
|
475
569
|
const selfId = this.libp2p.peerId.toString()
|
|
476
|
-
|
|
570
|
+
let ids = Array.from(new Set([...cohort, selfId]))
|
|
571
|
+
|
|
572
|
+
// Network-membership scoping (no-op when protocolPrefix is unset): a cohort
|
|
573
|
+
// member that serves a DIFFERENT network's protocol can never negotiate THIS
|
|
574
|
+
// network's cluster/repo dial, so it guarantees a super-majority failure rather
|
|
575
|
+
// than contributing a promise. Drop such 'foreign' members; build the cohort from
|
|
576
|
+
// positively-'serves' members only and NEVER admit a not-yet-identified ('unknown')
|
|
577
|
+
// member. A permanently cross-network peer and a freshly-discovered same-network
|
|
578
|
+
// peer mid-identify are indistinguishable while 'unknown' (both have an empty
|
|
579
|
+
// peerStore protocol list), so admitting an 'unknown' on the strength of a viability
|
|
580
|
+
// floor risks pulling a cross-network contaminant into the cohort — its repo dial
|
|
581
|
+
// then negotiates a different network's protocol and the whole write fails. A fresh
|
|
582
|
+
// same-network peer is not starved: it flips to 'serves' once identify completes and
|
|
583
|
+
// is re-included on the caller's retry, and in the meantime a self-only cohort still
|
|
584
|
+
// completes the write under allowClusterDownsize (the default).
|
|
585
|
+
// Scoped path only: one peerStore read per cohort member yields both protocols
|
|
586
|
+
// (for membership classification here) and addresses (reused at backfill below),
|
|
587
|
+
// so a finally-selected member isn't fetched from the peerStore twice. Left
|
|
588
|
+
// undefined on the unscoped path, which never classifies membership.
|
|
589
|
+
let peerStoreRecords: Record<string, { protocols: string[]; addrs: string[] }> | undefined
|
|
590
|
+
if (this.protocolPrefix != null) {
|
|
591
|
+
// `cohort` is the over-fetched nearest-first band. Classify each non-self
|
|
592
|
+
// member, preserving proximity order within each tier.
|
|
593
|
+
const nonSelf = cohort.filter(id => id !== selfId)
|
|
594
|
+
peerStoreRecords = await this.getPeerStoreRecordsByPeer(nonSelf)
|
|
595
|
+
const serves: string[] = []
|
|
596
|
+
const unknown: string[] = []
|
|
597
|
+
let foreignDropped = 0
|
|
598
|
+
for (const id of nonSelf) {
|
|
599
|
+
const m = this.membershipOf(id, peerStoreRecords[id]?.protocols)
|
|
600
|
+
if (m === 'serves') serves.push(id)
|
|
601
|
+
else if (m === 'unknown') unknown.push(id)
|
|
602
|
+
else foreignDropped++
|
|
603
|
+
}
|
|
604
|
+
// Take the nearest `clusterSize - 1` SERVING peers. Self is ALWAYS added below and
|
|
605
|
+
// counts toward `clusterSize` (matching the unscoped path, where `assembleCohort`
|
|
606
|
+
// returns the nearest `clusterSize` peers INCLUDING self when self is near the key —
|
|
607
|
+
// the coordinator case), so reserving a slot for self keeps a healthy same-network
|
|
608
|
+
// cohort at exactly `clusterSize` members rather than `clusterSize + 1`. Over-sizing
|
|
609
|
+
// would inflate the super-majority promise count (ceil(peerCount * threshold)) above
|
|
610
|
+
// what the configured `clusterSize` intends and hurt write availability. 'unknown'
|
|
611
|
+
// members are never backfilled: an 'unknown' peer may be a permanently cross-network
|
|
612
|
+
// contaminant whose repo dial cannot negotiate this network's protocol, and a fresh
|
|
613
|
+
// same-network peer mid-identify is indistinguishable from it. We therefore admit
|
|
614
|
+
// only positively-'serves' peers; when self is the sole serving member the cohort is
|
|
615
|
+
// self-only, which completes the write under allowClusterDownsize (the default) and
|
|
616
|
+
// re-includes any legitimate peer as 'serves' on the caller's retry once identify
|
|
617
|
+
// completes. `unknown.length` is still computed above for the diagnostic log line.
|
|
618
|
+
const nonSelfTarget = Math.max(0, this.clusterSize - 1)
|
|
619
|
+
const others = serves.slice(0, nonSelfTarget)
|
|
620
|
+
ids = Array.from(new Set([selfId, ...others]))
|
|
621
|
+
this.log('findCluster:membership key=%s serves=%d unknown=%d foreignDropped=%d kept=%d',
|
|
622
|
+
keyStr, serves.length, unknown.length, foreignDropped, ids.length)
|
|
623
|
+
}
|
|
477
624
|
|
|
478
625
|
const connectedByPeer = this.getConnectedAddrsByPeer()
|
|
479
626
|
const connectedPeerIds = Object.keys(connectedByPeer)
|
|
@@ -481,8 +628,17 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
481
628
|
// Backfill addresses from the peerStore for cohort members we don't have
|
|
482
629
|
// a live connection to. The cohort is keyspace-determined and can include
|
|
483
630
|
// peers we know-of but haven't dialed yet; without this backfill those
|
|
484
|
-
// would be silently dropped.
|
|
485
|
-
|
|
631
|
+
// would be silently dropped. On the scoped path reuse the addresses already
|
|
632
|
+
// read into `peerStoreRecords` above (no second store.get per member); on the
|
|
633
|
+
// unscoped path (no record map) do the single peerStore read as before.
|
|
634
|
+
const backfillIds = ids.filter(id => id !== selfId)
|
|
635
|
+
const peerStoreAddrs = peerStoreRecords
|
|
636
|
+
? Object.fromEntries(
|
|
637
|
+
backfillIds
|
|
638
|
+
.map(id => [id, peerStoreRecords![id]?.addrs ?? []] as const)
|
|
639
|
+
.filter(([, addrs]) => addrs.length > 0)
|
|
640
|
+
)
|
|
641
|
+
: await this.getPeerStoreAddrsByPeer(backfillIds)
|
|
486
642
|
|
|
487
643
|
this.log('findCluster key=%s fretCohort=%d connected=%d', keyStr, cohort.length, connectedPeerIds.length)
|
|
488
644
|
if (verbose) this.log('findCluster:detail key=%s cohortPeers=%o connectedPeers=%o', keyStr, ids, connectedPeerIds)
|
|
@@ -541,4 +697,111 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
|
|
|
541
697
|
}))
|
|
542
698
|
return out
|
|
543
699
|
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Single-pass peerStore read returning BOTH protocols and addresses per peer from one
|
|
703
|
+
* `store.get` call. Used on the membership-scoped `findCluster` hot path, where the
|
|
704
|
+
* cohort needs protocols (to classify membership) AND addresses (to backfill dial
|
|
705
|
+
* targets) for the same peers — reading them together avoids a second `store.get` per
|
|
706
|
+
* finally-selected member. Same error handling as {@link getPeerStoreProtocolsByPeer}
|
|
707
|
+
* and {@link getPeerStoreAddrsByPeer}: a missing peer or peerStore failure is left
|
|
708
|
+
* absent from the map (caller treats absent protocols as 'unknown', absent addrs as none).
|
|
709
|
+
*/
|
|
710
|
+
private async getPeerStoreRecordsByPeer(ids: string[]): Promise<Record<string, { protocols: string[]; addrs: string[] }>> {
|
|
711
|
+
const out: Record<string, { protocols: string[]; addrs: string[] }> = {}
|
|
712
|
+
const store = (this.libp2p as { peerStore?: { get?: (id: PeerId) => Promise<{ protocols?: string[]; addresses?: Array<{ multiaddr: { toString(): string } }> }> } }).peerStore
|
|
713
|
+
if (!store?.get) return out
|
|
714
|
+
await Promise.all(ids.map(async (idStr) => {
|
|
715
|
+
try {
|
|
716
|
+
const pid = peerIdFromString(idStr)
|
|
717
|
+
const peer = await store.get!(pid)
|
|
718
|
+
const addrs = (peer?.addresses ?? []).map(a => a.multiaddr.toString())
|
|
719
|
+
out[idStr] = { protocols: peer?.protocols ?? [], addrs }
|
|
720
|
+
} catch {
|
|
721
|
+
// Unknown peer or peerStore failure — leave out of the map.
|
|
722
|
+
}
|
|
723
|
+
}))
|
|
724
|
+
return out
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Prefetch each peer's advertised protocol list from the libp2p peerStore.
|
|
729
|
+
* Returns a map from peer-id string to its protocols (empty array when the peer
|
|
730
|
+
* is absent or has not yet been identified). Mirrors {@link getPeerStoreAddrsByPeer};
|
|
731
|
+
* errors are swallowed so a peerStore hiccup degrades to "unknown" rather than throwing.
|
|
732
|
+
*/
|
|
733
|
+
private async getPeerStoreProtocolsByPeer(ids: string[]): Promise<Record<string, string[]>> {
|
|
734
|
+
const out: Record<string, string[]> = {}
|
|
735
|
+
const store = (this.libp2p as { peerStore?: { get?: (id: PeerId) => Promise<{ protocols?: string[] }> } }).peerStore
|
|
736
|
+
if (!store?.get) return out
|
|
737
|
+
await Promise.all(ids.map(async (idStr) => {
|
|
738
|
+
try {
|
|
739
|
+
const pid = peerIdFromString(idStr)
|
|
740
|
+
const peer = await store.get!(pid)
|
|
741
|
+
out[idStr] = peer?.protocols ?? []
|
|
742
|
+
} catch {
|
|
743
|
+
// Unknown peer or peerStore failure — leave out (treated as 'unknown').
|
|
744
|
+
}
|
|
745
|
+
}))
|
|
746
|
+
return out
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Over-fetch width for network-membership scoping. A cross-network peer can sit
|
|
751
|
+
* NEARER the key than a legitimate same-network peer and displace it from the
|
|
752
|
+
* nearest-`clusterSize` window, so when scoping is active we ask FRET for a wider
|
|
753
|
+
* proximity band and then keep the nearest peers that actually serve this network.
|
|
754
|
+
* (A ring polluted by more cross-network peers than this band is the domain of the
|
|
755
|
+
* separate FRET-side eviction follow-up; this band covers realistic co-location.)
|
|
756
|
+
*/
|
|
757
|
+
private membershipOverfetch(): number {
|
|
758
|
+
return Math.max(this.clusterSize * 4, this.clusterSize + 16)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Classify a peer's network membership from its advertised protocols. Self always
|
|
763
|
+
* `serves` (it trivially serves its own network). When no `protocolPrefix` is
|
|
764
|
+
* configured the filter is disabled and EVERY peer is reported `serves`, so all
|
|
765
|
+
* callers behave exactly as before this scoping was added.
|
|
766
|
+
*/
|
|
767
|
+
private membershipOf(idStr: string, protocols: string[] | undefined): NetworkMembership {
|
|
768
|
+
if (this.protocolPrefix == null) return 'serves'
|
|
769
|
+
if (idStr === this.libp2p.peerId.toString()) return 'serves'
|
|
770
|
+
if (protocols == null || protocols.length === 0) return 'unknown'
|
|
771
|
+
if (protocols.includes(`${this.protocolPrefix}/cluster/1.0.0`)
|
|
772
|
+
|| protocols.includes(`${this.protocolPrefix}/repo/1.0.0`)) return 'serves'
|
|
773
|
+
return 'foreign'
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Scope a reputation-ordered candidate id list to this network for COORDINATOR
|
|
778
|
+
* selection: keep ONLY peers confirmed to serve this network (`serves`, which always
|
|
779
|
+
* includes self), dropping both `foreign` peers (serving another network) and
|
|
780
|
+
* `unknown` peers (peerStore protocol list empty — not yet confirmed). Incoming
|
|
781
|
+
* (reputation) order is preserved among the surviving `serves` peers. A no-op
|
|
782
|
+
* (returns the input unchanged, no drops) when `protocolPrefix` is unset or the list
|
|
783
|
+
* is empty — the membership-disabled path is therefore untouched.
|
|
784
|
+
*
|
|
785
|
+
* `droppedUnconfirmed` reports whether any candidate was excluded because it was not
|
|
786
|
+
* confirmed to serve this network — `foreign` OR `unknown` under scoping — so the
|
|
787
|
+
* caller can surface a distinct "no network coordinator" failure rather than a generic
|
|
788
|
+
* one. An `unknown` peer is not gambled on as coordinator: a permanent cross-network
|
|
789
|
+
* contaminant and a fresh same-network peer mid-identify are indistinguishable at an
|
|
790
|
+
* instant, but the filter re-reads the peerStore on every retry attempt, so a genuine
|
|
791
|
+
* same-network peer that completes `identify` within the retry window flips to `serves`
|
|
792
|
+
* and is selected normally on that attempt.
|
|
793
|
+
*/
|
|
794
|
+
private async filterByMembership(ids: string[]): Promise<{ ranked: string[]; droppedUnconfirmed: boolean }> {
|
|
795
|
+
if (this.protocolPrefix == null || ids.length === 0) return { ranked: ids, droppedUnconfirmed: false }
|
|
796
|
+
const selfStr = this.libp2p.peerId.toString()
|
|
797
|
+
const protocolsByPeer = await this.getPeerStoreProtocolsByPeer(ids.filter(id => id !== selfStr))
|
|
798
|
+
const serves: string[] = []
|
|
799
|
+
let droppedUnconfirmed = false
|
|
800
|
+
for (const id of ids) {
|
|
801
|
+
const m = this.membershipOf(id, protocolsByPeer[id])
|
|
802
|
+
if (m === 'serves') serves.push(id)
|
|
803
|
+
else droppedUnconfirmed = true
|
|
804
|
+
}
|
|
805
|
+
return { ranked: serves, droppedUnconfirmed }
|
|
806
|
+
}
|
|
544
807
|
}
|