@optimystic/db-p2p 0.24.1 → 0.25.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.
Files changed (257) hide show
  1. package/{readme.md → README.md} +688 -668
  2. package/dist/src/cluster/block-transfer-service.d.ts +133 -10
  3. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  4. package/dist/src/cluster/block-transfer-service.js +155 -9
  5. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  6. package/dist/src/cluster/block-transfer.d.ts +67 -7
  7. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  8. package/dist/src/cluster/block-transfer.js +127 -24
  9. package/dist/src/cluster/block-transfer.js.map +1 -1
  10. package/dist/src/cluster/certified-claims.d.ts +170 -0
  11. package/dist/src/cluster/certified-claims.d.ts.map +1 -0
  12. package/dist/src/cluster/certified-claims.js +200 -0
  13. package/dist/src/cluster/certified-claims.js.map +1 -0
  14. package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
  15. package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
  16. package/dist/src/cluster/client-signature-verifier.js +33 -0
  17. package/dist/src/cluster/client-signature-verifier.js.map +1 -0
  18. package/dist/src/cluster/cluster-policy.d.ts +9 -3
  19. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  20. package/dist/src/cluster/cluster-policy.js +111 -27
  21. package/dist/src/cluster/cluster-policy.js.map +1 -1
  22. package/dist/src/cluster/cluster-repo.d.ts +140 -11
  23. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  24. package/dist/src/cluster/cluster-repo.js +485 -29
  25. package/dist/src/cluster/cluster-repo.js.map +1 -1
  26. package/dist/src/cluster/commit-proof.d.ts +134 -0
  27. package/dist/src/cluster/commit-proof.d.ts.map +1 -0
  28. package/dist/src/cluster/commit-proof.js +263 -0
  29. package/dist/src/cluster/commit-proof.js.map +1 -0
  30. package/dist/src/cluster/quorum-restore.d.ts +95 -8
  31. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  32. package/dist/src/cluster/quorum-restore.js +113 -14
  33. package/dist/src/cluster/quorum-restore.js.map +1 -1
  34. package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
  35. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  36. package/dist/src/cluster/rebalance-monitor.js +207 -5
  37. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  38. package/dist/src/cluster/reconcile-block.d.ts +27 -7
  39. package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
  40. package/dist/src/cluster/reconcile-block.js +179 -38
  41. package/dist/src/cluster/reconcile-block.js.map +1 -1
  42. package/dist/src/cluster/service.d.ts +21 -4
  43. package/dist/src/cluster/service.d.ts.map +1 -1
  44. package/dist/src/cluster/service.js +28 -17
  45. package/dist/src/cluster/service.js.map +1 -1
  46. package/dist/src/cluster/spread-on-churn.d.ts +8 -2
  47. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  48. package/dist/src/cluster/spread-on-churn.js +7 -7
  49. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  50. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  51. package/dist/src/cohort-topic/host.js +8 -7
  52. package/dist/src/cohort-topic/host.js.map +1 -1
  53. package/dist/src/cohort-topic/stream-util.d.ts +7 -6
  54. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  55. package/dist/src/cohort-topic/stream-util.js +14 -41
  56. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  57. package/dist/src/dispute/cascade.d.ts +7 -8
  58. package/dist/src/dispute/cascade.d.ts.map +1 -1
  59. package/dist/src/dispute/cascade.js +24 -7
  60. package/dist/src/dispute/cascade.js.map +1 -1
  61. package/dist/src/dispute/dispute-service.d.ts +5 -0
  62. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  63. package/dist/src/dispute/dispute-service.js +13 -3
  64. package/dist/src/dispute/dispute-service.js.map +1 -1
  65. package/dist/src/dispute/invalidation.d.ts +33 -15
  66. package/dist/src/dispute/invalidation.d.ts.map +1 -1
  67. package/dist/src/dispute/invalidation.js +127 -34
  68. package/dist/src/dispute/invalidation.js.map +1 -1
  69. package/dist/src/dispute/service.d.ts.map +1 -1
  70. package/dist/src/dispute/service.js +2 -1
  71. package/dist/src/dispute/service.js.map +1 -1
  72. package/dist/src/index.d.ts +7 -0
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +7 -0
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/libp2p-key-network.d.ts +78 -6
  77. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  78. package/dist/src/libp2p-key-network.js +136 -57
  79. package/dist/src/libp2p-key-network.js.map +1 -1
  80. package/dist/src/libp2p-node-base.d.ts +38 -7
  81. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  82. package/dist/src/libp2p-node-base.js +150 -62
  83. package/dist/src/libp2p-node-base.js.map +1 -1
  84. package/dist/src/logger.d.ts +2 -2
  85. package/dist/src/logger.js +2 -2
  86. package/dist/src/network/open-protocol-stream.d.ts +56 -0
  87. package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
  88. package/dist/src/network/open-protocol-stream.js +60 -0
  89. package/dist/src/network/open-protocol-stream.js.map +1 -0
  90. package/dist/src/network/register-protocol-handler.d.ts +66 -0
  91. package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
  92. package/dist/src/network/register-protocol-handler.js +51 -0
  93. package/dist/src/network/register-protocol-handler.js.map +1 -0
  94. package/dist/src/peer-address-book.d.ts +128 -1
  95. package/dist/src/peer-address-book.d.ts.map +1 -1
  96. package/dist/src/peer-address-book.js +198 -15
  97. package/dist/src/peer-address-book.js.map +1 -1
  98. package/dist/src/pend-validation.d.ts +65 -0
  99. package/dist/src/pend-validation.d.ts.map +1 -0
  100. package/dist/src/pend-validation.js +69 -0
  101. package/dist/src/pend-validation.js.map +1 -0
  102. package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
  103. package/dist/src/reactivity/notify-transport.js +2 -1
  104. package/dist/src/reactivity/notify-transport.js.map +1 -1
  105. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
  106. package/dist/src/reactivity/push-state-gossip.js +2 -1
  107. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  108. package/dist/src/repo/cluster-coordinator.d.ts +32 -6
  109. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  110. package/dist/src/repo/cluster-coordinator.js +44 -10
  111. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  112. package/dist/src/repo/coordinator-repo.d.ts +181 -25
  113. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  114. package/dist/src/repo/coordinator-repo.js +650 -62
  115. package/dist/src/repo/coordinator-repo.js.map +1 -1
  116. package/dist/src/repo/served-repo-proxy.d.ts +30 -0
  117. package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
  118. package/dist/src/repo/served-repo-proxy.js +50 -0
  119. package/dist/src/repo/served-repo-proxy.js.map +1 -0
  120. package/dist/src/repo/service.d.ts +22 -1
  121. package/dist/src/repo/service.d.ts.map +1 -1
  122. package/dist/src/repo/service.js +25 -13
  123. package/dist/src/repo/service.js.map +1 -1
  124. package/dist/src/reputation/types.d.ts +3 -1
  125. package/dist/src/reputation/types.d.ts.map +1 -1
  126. package/dist/src/reputation/types.js +3 -1
  127. package/dist/src/reputation/types.js.map +1 -1
  128. package/dist/src/rn.d.ts +24 -2
  129. package/dist/src/rn.d.ts.map +1 -1
  130. package/dist/src/rn.js +33 -4
  131. package/dist/src/rn.js.map +1 -1
  132. package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
  133. package/dist/src/routing/libp2p-known-peers.js +5 -0
  134. package/dist/src/routing/libp2p-known-peers.js.map +1 -1
  135. package/dist/src/storage/block-archive.d.ts +153 -0
  136. package/dist/src/storage/block-archive.d.ts.map +1 -0
  137. package/dist/src/storage/block-archive.js +194 -0
  138. package/dist/src/storage/block-archive.js.map +1 -0
  139. package/dist/src/storage/block-latch.d.ts +92 -0
  140. package/dist/src/storage/block-latch.d.ts.map +1 -0
  141. package/dist/src/storage/block-latch.js +132 -0
  142. package/dist/src/storage/block-latch.js.map +1 -0
  143. package/dist/src/storage/block-storage.d.ts +169 -12
  144. package/dist/src/storage/block-storage.d.ts.map +1 -1
  145. package/dist/src/storage/block-storage.js +415 -152
  146. package/dist/src/storage/block-storage.js.map +1 -1
  147. package/dist/src/storage/cached-raw-storage.d.ts +5 -1
  148. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
  149. package/dist/src/storage/cached-raw-storage.js +16 -1
  150. package/dist/src/storage/cached-raw-storage.js.map +1 -1
  151. package/dist/src/storage/cached-store-driver.d.ts +14 -4
  152. package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
  153. package/dist/src/storage/cached-store-driver.js +39 -5
  154. package/dist/src/storage/cached-store-driver.js.map +1 -1
  155. package/dist/src/storage/i-block-storage.d.ts +70 -19
  156. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  157. package/dist/src/storage/i-block-storage.js +21 -1
  158. package/dist/src/storage/i-block-storage.js.map +1 -1
  159. package/dist/src/storage/i-raw-storage.d.ts +38 -0
  160. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  161. package/dist/src/storage/kv-raw-storage.d.ts +14 -8
  162. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
  163. package/dist/src/storage/kv-raw-storage.js +29 -9
  164. package/dist/src/storage/kv-raw-storage.js.map +1 -1
  165. package/dist/src/storage/memory-store-driver.d.ts +4 -1
  166. package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
  167. package/dist/src/storage/memory-store-driver.js +22 -2
  168. package/dist/src/storage/memory-store-driver.js.map +1 -1
  169. package/dist/src/storage/raw-store-codec.js +2 -2
  170. package/dist/src/storage/raw-store-driver.d.ts +44 -3
  171. package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
  172. package/dist/src/storage/shared-cache-pool.d.ts +60 -3
  173. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
  174. package/dist/src/storage/shared-cache-pool.js +68 -4
  175. package/dist/src/storage/shared-cache-pool.js.map +1 -1
  176. package/dist/src/storage/storage-repo.d.ts +204 -26
  177. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  178. package/dist/src/storage/storage-repo.js +422 -119
  179. package/dist/src/storage/storage-repo.js.map +1 -1
  180. package/dist/src/storage/store-identity.d.ts +21 -0
  181. package/dist/src/storage/store-identity.d.ts.map +1 -0
  182. package/dist/src/storage/store-identity.js +33 -0
  183. package/dist/src/storage/store-identity.js.map +1 -0
  184. package/dist/src/storage/struct.d.ts +15 -0
  185. package/dist/src/storage/struct.d.ts.map +1 -1
  186. package/dist/src/storage/with-read-cache.d.ts +92 -0
  187. package/dist/src/storage/with-read-cache.d.ts.map +1 -0
  188. package/dist/src/storage/with-read-cache.js +132 -0
  189. package/dist/src/storage/with-read-cache.js.map +1 -0
  190. package/dist/src/sync/service.d.ts +13 -3
  191. package/dist/src/sync/service.d.ts.map +1 -1
  192. package/dist/src/sync/service.js +7 -30
  193. package/dist/src/sync/service.js.map +1 -1
  194. package/dist/src/testing/mesh-harness.d.ts +75 -2
  195. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  196. package/dist/src/testing/mesh-harness.js +193 -81
  197. package/dist/src/testing/mesh-harness.js.map +1 -1
  198. package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
  199. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  200. package/dist/src/testing/raw-storage-conformance.js +97 -2
  201. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  202. package/package.json +5 -6
  203. package/src/cluster/block-transfer-service.ts +528 -312
  204. package/src/cluster/block-transfer.ts +547 -424
  205. package/src/cluster/certified-claims.ts +319 -0
  206. package/src/cluster/client-signature-verifier.ts +34 -0
  207. package/src/cluster/cluster-policy.ts +294 -203
  208. package/src/cluster/cluster-repo.ts +2501 -2034
  209. package/src/cluster/commit-proof.ts +344 -0
  210. package/src/cluster/quorum-restore.ts +348 -223
  211. package/src/cluster/rebalance-monitor.ts +539 -254
  212. package/src/cluster/reconcile-block.ts +404 -203
  213. package/src/cluster/service.ts +308 -293
  214. package/src/cluster/spread-on-churn.ts +352 -346
  215. package/src/cohort-topic/host.ts +8 -7
  216. package/src/cohort-topic/stream-util.ts +14 -43
  217. package/src/dispute/cascade.ts +31 -15
  218. package/src/dispute/dispute-service.ts +706 -696
  219. package/src/dispute/invalidation.ts +162 -52
  220. package/src/dispute/service.ts +126 -125
  221. package/src/index.ts +7 -0
  222. package/src/libp2p-key-network.ts +142 -58
  223. package/src/libp2p-node-base.ts +1793 -1675
  224. package/src/logger.ts +27 -27
  225. package/src/network/open-protocol-stream.ts +87 -0
  226. package/src/network/register-protocol-handler.ts +85 -0
  227. package/src/peer-address-book.ts +360 -149
  228. package/src/pend-validation.ts +89 -0
  229. package/src/reactivity/notify-transport.ts +145 -144
  230. package/src/reactivity/push-state-gossip.ts +292 -291
  231. package/src/repo/cluster-coordinator.ts +65 -11
  232. package/src/repo/coordinator-repo.ts +729 -71
  233. package/src/repo/served-repo-proxy.ts +55 -0
  234. package/src/repo/service.ts +332 -313
  235. package/src/reputation/types.ts +3 -1
  236. package/src/rn.ts +34 -4
  237. package/src/routing/libp2p-known-peers.ts +31 -26
  238. package/src/storage/block-archive.ts +242 -0
  239. package/src/storage/block-latch.ts +144 -0
  240. package/src/storage/block-storage.ts +787 -498
  241. package/src/storage/cached-raw-storage.ts +21 -1
  242. package/src/storage/cached-store-driver.ts +898 -859
  243. package/src/storage/i-block-storage.ts +172 -116
  244. package/src/storage/i-raw-storage.ts +40 -0
  245. package/src/storage/kv-raw-storage.ts +34 -9
  246. package/src/storage/memory-store-driver.ts +26 -2
  247. package/src/storage/raw-store-codec.ts +42 -42
  248. package/src/storage/raw-store-driver.ts +45 -3
  249. package/src/storage/shared-cache-pool.ts +516 -452
  250. package/src/storage/storage-repo.ts +1421 -1014
  251. package/src/storage/store-identity.ts +41 -0
  252. package/src/storage/struct.ts +52 -34
  253. package/src/storage/with-read-cache.ts +182 -0
  254. package/src/sync/service.ts +178 -198
  255. package/src/testing/cohort-topic-mesh-harness.ts +673 -673
  256. package/src/testing/mesh-harness.ts +524 -344
  257. package/src/testing/raw-storage-conformance.ts +114 -2
@@ -1,12 +1,13 @@
1
- import type { AbortOptions, Connection, Libp2p, PeerId, Stream } from "@libp2p/interface";
1
+ import type { AbortOptions, Libp2p, PeerId, Stream } from "@libp2p/interface";
2
2
  import { toString as u8ToString } from 'uint8arrays'
3
3
  import type { ClusterPeers, CoordinatorIntent, FindCoordinatorOptions, IKeyNetwork, IPeerNetwork } from "@optimystic/db-core";
4
4
  import { peerIdFromString } from '@libp2p/peer-id'
5
5
  import type { FretService, SerializedTable } from 'p2p-fret'
6
6
  import { hashKey } from 'p2p-fret'
7
7
  import { createLogger, verbose } from './logger.js'
8
- import { mergePeerAddresses, validMultiaddrStrings } from './peer-address-book.js'
8
+ import { classifySelfDialability, mergePeerAddresses, publishableConnectionAddr, unionPublishableAddrs, type AddressLog } from './peer-address-book.js'
9
9
  import type { IPeerReputation } from './reputation/types.js'
10
+ import { openProtocolStream } from './network/open-protocol-stream.js'
10
11
 
11
12
  interface WithFretService { services?: { fret?: FretService } }
12
13
 
@@ -70,6 +71,39 @@ export class FindCoordinatorError extends Error {
70
71
  }
71
72
  }
72
73
 
74
+ /**
75
+ * `.code` on {@link SelfRelayOnlyAddressesError}. A stable string so it survives the
76
+ * `ClusterErrorEnvelope` round trip (`toClusterErrorEnvelope` carries `name` and `code`) and shows
77
+ * up as `code=SELF_RELAY_ONLY_ADDRESSES` on `ProtocolClient`'s `dial:fail` line instead of `none`.
78
+ */
79
+ export const SELF_RELAY_ONLY_ERROR_CODE = 'SELF_RELAY_ONLY_ADDRESSES';
80
+
81
+ /**
82
+ * Thrown by {@link Libp2pKeyPeerNetwork.connect} when we hold addresses for a peer but EVERY one
83
+ * of them reaches it by relaying through this node.
84
+ *
85
+ * This is the steady state a relay reaches for its own reservation holders: the address such a
86
+ * client advertises is `/<our transport addr>/p2p/<our peer id>/p2p-circuit`, which is correct and
87
+ * useful to every node except us. Dialing it asks us to relay to the client through ourselves, so
88
+ * it can only fail — with an error text (`NoValidAddressesError`, or an `AggregateError` of
89
+ * `Can not dial self`, depending on whether the circuit transport is registered as a dialer here)
90
+ * that is indistinguishable from "nobody ever taught us an address". Retrying cannot help: once the
91
+ * client's connection drops, only the client can re-initiate. So we fail fast and distinctly,
92
+ * letting the caller's existing exclude-and-continue logic move to another cohort member instead of
93
+ * burning a dial timeout.
94
+ */
95
+ export class SelfRelayOnlyAddressesError extends Error {
96
+ readonly code = SELF_RELAY_ONLY_ERROR_CODE;
97
+ constructor(peer: string, protocol: string, addrCount: number) {
98
+ super(
99
+ `Peer ${peer} is reachable only through a circuit on THIS node: all ${addrCount} address(es) ` +
100
+ `we hold route back through us, so no dial for ${protocol} can succeed. ` +
101
+ `Only that peer can re-establish the connection.`
102
+ );
103
+ this.name = 'SelfRelayOnlyAddressesError';
104
+ }
105
+ }
106
+
73
107
  /** Snapshot format this build writes and is willing to read. Bump on any incompatible change. */
74
108
  export const PERSISTED_STATE_VERSION = 2 as const;
75
109
 
@@ -248,6 +282,12 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
248
282
  private readonly coordinatorCache = new Map<string, { id: PeerId, expires: number }>()
249
283
  private static readonly MAX_CACHE_ENTRIES = 1000
250
284
  private readonly log: ReturnType<typeof createLogger>
285
+ /**
286
+ * This instance's logger, in the shape `peer-address-book.ts` accepts. Declared once so the
287
+ * five address predicates that take a sink all report under the same peer-id-suffixed
288
+ * namespace, rather than five separately-written adapters drifting apart.
289
+ */
290
+ private readonly addressLog: AddressLog = (fmt, ...args) => this.log(fmt, ...args)
251
291
 
252
292
  private toCacheKey(key: Uint8Array): string { return u8ToString(key, 'base64url') }
253
293
 
@@ -390,6 +430,15 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
390
430
  * nor banned by reputation. Shared by all three places `findCoordinator` narrows a candidate
391
431
  * list — the FRET tier, the connected-peer fallback, and the retry-futility input — so the
392
432
  * futility test can never disagree with the tiers about who is pickable.
433
+ *
434
+ * NOTE: eligibility here is deliberately blind to self-dialability, so on a relay
435
+ * `findCoordinator` can still pick one of our own reservation holders and only then fail fast
436
+ * with `SelfRelayOnlyAddressesError`. The caller's exclude-and-continue walk
437
+ * (`db-core/utility/batch-coordinator.ts`) absorbs it, and each such pick now costs an
438
+ * instant refusal rather than a burned dial timeout, so it is a selection round-trip, not a
439
+ * stall — and the verdict is a live peerStore read that a stale eligibility filter would have
440
+ * to guess at. If a relay serving many reservation holders is ever measured spending real time
441
+ * walking through them, feed the verdict into the tiers instead of discovering it at dial.
393
442
  */
394
443
  private isSelectable(id: string, excluded: Set<string>): boolean {
395
444
  return !excluded.has(id) && !(this.reputation?.isBanned(id));
@@ -532,7 +581,7 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
532
581
  * protocol client dials through.
533
582
  */
534
583
  public recordPeerAddresses(peerId: PeerId, multiaddrs: string[]): void {
535
- mergePeerAddresses(this.libp2p, peerId, multiaddrs, (fmt, ...args) => this.log(fmt, ...args))
584
+ mergePeerAddresses(this.libp2p, peerId, multiaddrs, this.addressLog)
536
585
  }
537
586
 
538
587
  private getCachedCoordinator(key: Uint8Array): PeerId | undefined {
@@ -544,49 +593,60 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
544
593
  }
545
594
 
546
595
  /**
547
- * True for a circuit-relay ("limited") connection. libp2p stamps a relayed
548
- * connection with `limits` (per-circuit data/duration caps); we additionally
549
- * sniff the multiaddr for `/p2p-circuit` as a fallback for transports/versions
550
- * that don't populate `limits`.
596
+ * Open a stream to `peerId` on `protocol` — reusing a live connection when we hold one, and
597
+ * otherwise dialing.
598
+ *
599
+ * Connection selection (prefer a direct connection over a resettable circuit-relay one, skip
600
+ * entries libp2p has not yet evicted, opt in to limited connections) lives in
601
+ * {@link openProtocolStream}, the single place in this package that opens a protocol stream.
602
+ *
603
+ * `negotiateFully: false` is safe here and saves a round trip: this is request/response and the
604
+ * caller always reads a reply, so an unsupported-protocol failure deferred to the first read is
605
+ * still observed. The caller's `AbortSignal` is forwarded so a per-peer dial deadline (enforced
606
+ * upstream by `ProtocolClient.processMessage`) can actually cancel a stuck dial — without it,
607
+ * libp2p falls back to its built-in connection-manager `dialTimeout` and the caller's tighter
608
+ * deadline is decorative.
609
+ *
610
+ * The cold path pays one `peerStore.get` before dialing, to separate two failures libp2p
611
+ * reports identically: "nobody ever taught us an address" and "every address we hold routes
612
+ * back through us" (see {@link SelfRelayOnlyAddressesError}). Only the second is diagnosed
613
+ * here; the first still dials, so an unknown peer produces libp2p's own `NoValidAddressesError`
614
+ * exactly as before. It rides `beforeDial`, which never runs on the reuse path — the warm path
615
+ * is deliberately kept clear of that read, since a live connection is the case this method
616
+ * exists to make cheap.
551
617
  */
552
- private isLimitedConnection(c: Connection): boolean {
553
- if ((c as { limits?: unknown }).limits != null) return true
554
- const addr = c.remoteAddr?.toString?.()
555
- return addr != null && addr.includes('/p2p-circuit')
618
+ async connect(peerId: PeerId, protocol: string, options?: AbortOptions): Promise<Stream> {
619
+ return await openProtocolStream(this.libp2p, peerId, protocol, {
620
+ signal: options?.signal,
621
+ negotiateFully: false,
622
+ beforeDial: () => this.assertNotSelfRelayOnly(peerId, protocol, options)
623
+ })
556
624
  }
557
625
 
558
- connect(peerId: PeerId, protocol: string, options?: AbortOptions): Promise<Stream> {
559
- const conns = this.libp2p.getConnections?.(peerId) ?? []
560
- // Filter to only-open connections so a closing/closed entry that libp2p
561
- // hasn't yet evicted from its index doesn't get picked up here.
562
- const open = conns.filter(c => c?.status === 'open' && typeof c?.newStream === 'function')
563
- // Prefer a DIRECT connection over a limited (circuit-relay) one for the RPC.
564
- // A relayed/limited connection can be reset by the relay once a per-circuit
565
- // cap or reservation lapses (@libp2p/circuit-relay-v2), surfacing to the
566
- // coordinator as a StreamResetError that fails consensus. After DCUtR upgrades
567
- // a relayed link to direct, both connections briefly coexist — picking the
568
- // direct one avoids riding the soon-to-be-reset circuit. We only fall back to
569
- // the limited connection (with runOnLimitedConnection) when it is the only open
570
- // path the steady state for browsers and NATed peers before any upgrade.
571
- const chosen = open.find(c => !this.isLimitedConnection(c)) ?? open[0]
572
- if (chosen) {
573
- // runOnLimitedConnection: true is required to open a stream over a
574
- // circuit-relay (limited) connection — the steady-state path for
575
- // browsers and NATed peers. Without it, the warm relay connection
576
- // from a prior dialProtocol cannot be reused on subsequent RPCs. It is
577
- // a harmless no-op on the preferred direct connection.
578
- return chosen.newStream([protocol], {
579
- signal: options?.signal,
580
- runOnLimitedConnection: true,
581
- negotiateFully: false
582
- })
583
- }
584
- // Forward the caller's AbortSignal so a per-peer dial deadline (enforced
585
- // upstream by ProtocolClient.processMessage) can actually cancel a stuck
586
- // dial — without this, libp2p falls back to its built-in dial timeout
587
- // (default ~30s) and the caller's tighter deadline is decorative.
588
- const dialOptions = { runOnLimitedConnection: true, negotiateFully: false, signal: options?.signal } as const
589
- return this.libp2p.dialProtocol(peerId, [protocol], dialOptions)
626
+ /**
627
+ * Throw {@link SelfRelayOnlyAddressesError} when every address we hold for `peerId` routes
628
+ * through us, so the caller skips a dial that cannot succeed.
629
+ *
630
+ * Holding NOTHING is left alone on purpose that dial still happens and still fails with
631
+ * libp2p's `NoValidAddressesError`, because "we were never told an address" is a genuinely
632
+ * different condition with a genuinely different remedy (someone teaches us one; see
633
+ * `recordPeerAddresses`).
634
+ */
635
+ private async assertNotSelfRelayOnly(peerId: PeerId, protocol: string, options?: AbortOptions): Promise<void> {
636
+ // NOTE: this costs one `peerStore.get` on EVERY cold dial, not only on relays, and libp2p's
637
+ // own dial queue reads the same record moments later so a cold dial pays the peerStore
638
+ // twice. Unmeasured, and negligible against a dial's own cost; if cold-dial latency or
639
+ // peerStore contention ever shows up in a profile, hoist the verdict into the dial path
640
+ // rather than reading ahead of it.
641
+ const idStr = peerId.toString()
642
+ const held = (await this.getPeerStoreAddrsByPeer([idStr]))[idStr] ?? []
643
+ // A caller that cancelled while we were reading the peerStore is owed ITS reason, not a
644
+ // verdict we computed from a snapshot it no longer cares about.
645
+ options?.signal?.throwIfAborted()
646
+ if (classifySelfDialability(held, this.libp2p.peerId.toString(), this.addressLog) !== 'self-relay-only') return
647
+ this.log('connect:self-relay-only peer=%s protocol=%s addrs=%d',
648
+ idStr.substring(0, 12), protocol, held.length)
649
+ throw new SelfRelayOnlyAddressesError(idStr, protocol, held.length)
590
650
  }
591
651
 
592
652
  private getFret(): FretService {
@@ -836,21 +896,27 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
836
896
  );
837
897
  }
838
898
 
899
+ /**
900
+ * Connection-derived addresses for the cluster record, keyed by peer id.
901
+ *
902
+ * Only OUTBOUND connections contribute — see {@link publishableConnectionAddr} for why an
903
+ * inbound connection's `remoteAddr` is an ephemeral source socket that no third party can
904
+ * reach. This is the only place `findCluster` derives addresses from connections, so the
905
+ * rule has exactly one site.
906
+ */
839
907
  private getConnectedAddrsByPeer(): Record<string, string[]> {
840
908
  const conns = this.libp2p.getConnections()
841
909
  const byPeer: Record<string, string[]> = {}
842
910
  for (const c of conns) {
911
+ const addr = publishableConnectionAddr(c, this.addressLog)
912
+ if (addr === undefined) continue
843
913
  const id = c.remotePeer.toString()
844
- const addr = c.remoteAddr?.toString?.()
845
- if (addr) (byPeer[id] ??= []).push(addr)
914
+ const forPeer = byPeer[id] ??= []
915
+ forPeer.push(addr)
846
916
  }
847
917
  return byPeer
848
918
  }
849
919
 
850
- private parseMultiaddrs(addrs: string[]): string[] {
851
- return validMultiaddrStrings(addrs, (fmt, ...args) => this.log(fmt, ...args))
852
- }
853
-
854
920
  async findCluster(key: Uint8Array): Promise<ClusterPeers> {
855
921
  const t0 = Date.now();
856
922
  const fret = this.getFret()
@@ -947,6 +1013,13 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
947
1013
  // reported symptom: clean membership logs on every node while every dial to the
948
1014
  // addressless member died instantly and the write never completed.
949
1015
  const addressless: string[] = []
1016
+ // The OTHER way a member can be undialable by us: we hold addresses, but every one of
1017
+ // them is a circuit through this node — the steady state for our own reservation holders.
1018
+ // Counted separately because the two have different remedies (be taught an address, vs.
1019
+ // wait for the client to re-dial us) and libp2p's dial error cannot tell them apart.
1020
+ // These addresses are still PUBLISHED: a cohort sibling reaching the member through our
1021
+ // relay is the working path, and dropping them would break it.
1022
+ const selfRelayOnly: string[] = []
950
1023
 
951
1024
  for (const idStr of ids) {
952
1025
  if (idStr === selfId) {
@@ -956,12 +1029,13 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
956
1029
  }
957
1030
  const connectedStrings = connectedByPeer[idStr] ?? []
958
1031
  const peerStoreStrings = peerStoreAddrs[idStr] ?? []
959
- // De-duplicate while preserving connected-first ordering. The
960
- // connected multiaddr is the one libp2p just used to reach this peer
961
- // and is the most reliable; peerStore addrs are the fallback for
962
- // cohort members we know-of but aren't currently connected to.
963
- const merged = Array.from(new Set([...connectedStrings, ...peerStoreStrings]))
964
- const parsed = this.parseMultiaddrs(merged)
1032
+ // The union rule connected-first, de-duplicated, validated — lives in
1033
+ // `unionPublishableAddrs` so a cluster record and a redirect payload cannot describe
1034
+ // the same peer differently. The sync form is used here (rather than
1035
+ // `publishableAddrsForPeer`) because both halves are already in hand: the peerStore
1036
+ // read above is batched across the whole cohort, and on the membership-scoped path it
1037
+ // is the same `store.get` that fetched protocols.
1038
+ const parsed = unionPublishableAddrs(connectedStrings, peerStoreStrings, this.addressLog)
965
1039
  const remotePeerId = peerIdFromString(idStr)
966
1040
  const raw = remotePeerId.publicKey?.raw ?? new Uint8Array()
967
1041
  // Note: parsed may be empty for a cohort member we have neither a
@@ -970,7 +1044,9 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
970
1044
  // retry/exclude logic takes over — we intentionally do NOT drop
971
1045
  // addressless members here, because shrinking the cohort below
972
1046
  // `clusterSize` puts consensus supermajority out of reach.
973
- if (parsed.length === 0) addressless.push(idStr.substring(0, 12))
1047
+ const dialability = classifySelfDialability(parsed, selfId, this.addressLog)
1048
+ if (dialability === 'none') addressless.push(idStr.substring(0, 12))
1049
+ else if (dialability === 'self-relay-only') selfRelayOnly.push(idStr.substring(0, 12))
974
1050
  peers[idStr] = { multiaddrs: parsed, publicKey: u8ToString(raw, 'base64url') }
975
1051
  }
976
1052
 
@@ -983,8 +1059,16 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
983
1059
  keyStr, addressless.length, Object.keys(peers).length, addressless)
984
1060
  }
985
1061
 
986
- this.log('findCluster:done key=%s ms=%d peers=%d addressless=%d',
987
- keyStr, Date.now() - t0, Object.keys(peers).length, addressless.length)
1062
+ // Same reasoning, second condition: a non-zero count here means we DO hold addresses for
1063
+ // these members and still cannot dial them, because the only route we know runs through
1064
+ // our own relay. No amount of retrying changes that (see `SelfRelayOnlyAddressesError`).
1065
+ if (selfRelayOnly.length > 0) {
1066
+ this.log('findCluster:self-relay-only-members key=%s count=%d of=%d peers=%o',
1067
+ keyStr, selfRelayOnly.length, Object.keys(peers).length, selfRelayOnly)
1068
+ }
1069
+
1070
+ this.log('findCluster:done key=%s ms=%d peers=%d addressless=%d selfRelayOnly=%d',
1071
+ keyStr, Date.now() - t0, Object.keys(peers).length, addressless.length, selfRelayOnly.length)
988
1072
  return peers
989
1073
  }
990
1074