@optimystic/db-p2p 0.24.2 → 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 (249) 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 +13 -4
  43. package/dist/src/cluster/service.d.ts.map +1 -1
  44. package/dist/src/cluster/service.js +17 -18
  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 +14 -10
  77. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  78. package/dist/src/libp2p-key-network.js +28 -58
  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 +148 -63
  83. package/dist/src/libp2p-node-base.js.map +1 -1
  84. package/dist/src/network/open-protocol-stream.d.ts +56 -0
  85. package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
  86. package/dist/src/network/open-protocol-stream.js +60 -0
  87. package/dist/src/network/open-protocol-stream.js.map +1 -0
  88. package/dist/src/network/register-protocol-handler.d.ts +66 -0
  89. package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
  90. package/dist/src/network/register-protocol-handler.js +51 -0
  91. package/dist/src/network/register-protocol-handler.js.map +1 -0
  92. package/dist/src/peer-address-book.d.ts +60 -2
  93. package/dist/src/peer-address-book.d.ts.map +1 -1
  94. package/dist/src/peer-address-book.js +89 -1
  95. package/dist/src/peer-address-book.js.map +1 -1
  96. package/dist/src/pend-validation.d.ts +65 -0
  97. package/dist/src/pend-validation.d.ts.map +1 -0
  98. package/dist/src/pend-validation.js +69 -0
  99. package/dist/src/pend-validation.js.map +1 -0
  100. package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
  101. package/dist/src/reactivity/notify-transport.js +2 -1
  102. package/dist/src/reactivity/notify-transport.js.map +1 -1
  103. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
  104. package/dist/src/reactivity/push-state-gossip.js +2 -1
  105. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  106. package/dist/src/repo/cluster-coordinator.d.ts +32 -6
  107. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  108. package/dist/src/repo/cluster-coordinator.js +44 -10
  109. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  110. package/dist/src/repo/coordinator-repo.d.ts +181 -25
  111. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  112. package/dist/src/repo/coordinator-repo.js +650 -62
  113. package/dist/src/repo/coordinator-repo.js.map +1 -1
  114. package/dist/src/repo/served-repo-proxy.d.ts +30 -0
  115. package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
  116. package/dist/src/repo/served-repo-proxy.js +50 -0
  117. package/dist/src/repo/served-repo-proxy.js.map +1 -0
  118. package/dist/src/repo/service.d.ts +16 -1
  119. package/dist/src/repo/service.d.ts.map +1 -1
  120. package/dist/src/repo/service.js +18 -16
  121. package/dist/src/repo/service.js.map +1 -1
  122. package/dist/src/reputation/types.d.ts +3 -1
  123. package/dist/src/reputation/types.d.ts.map +1 -1
  124. package/dist/src/reputation/types.js +3 -1
  125. package/dist/src/reputation/types.js.map +1 -1
  126. package/dist/src/rn.d.ts +24 -2
  127. package/dist/src/rn.d.ts.map +1 -1
  128. package/dist/src/rn.js +33 -4
  129. package/dist/src/rn.js.map +1 -1
  130. package/dist/src/storage/block-archive.d.ts +153 -0
  131. package/dist/src/storage/block-archive.d.ts.map +1 -0
  132. package/dist/src/storage/block-archive.js +194 -0
  133. package/dist/src/storage/block-archive.js.map +1 -0
  134. package/dist/src/storage/block-latch.d.ts +92 -0
  135. package/dist/src/storage/block-latch.d.ts.map +1 -0
  136. package/dist/src/storage/block-latch.js +132 -0
  137. package/dist/src/storage/block-latch.js.map +1 -0
  138. package/dist/src/storage/block-storage.d.ts +169 -12
  139. package/dist/src/storage/block-storage.d.ts.map +1 -1
  140. package/dist/src/storage/block-storage.js +415 -152
  141. package/dist/src/storage/block-storage.js.map +1 -1
  142. package/dist/src/storage/cached-raw-storage.d.ts +5 -1
  143. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
  144. package/dist/src/storage/cached-raw-storage.js +16 -1
  145. package/dist/src/storage/cached-raw-storage.js.map +1 -1
  146. package/dist/src/storage/cached-store-driver.d.ts +14 -4
  147. package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
  148. package/dist/src/storage/cached-store-driver.js +39 -5
  149. package/dist/src/storage/cached-store-driver.js.map +1 -1
  150. package/dist/src/storage/i-block-storage.d.ts +70 -19
  151. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  152. package/dist/src/storage/i-block-storage.js +21 -1
  153. package/dist/src/storage/i-block-storage.js.map +1 -1
  154. package/dist/src/storage/i-raw-storage.d.ts +38 -0
  155. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  156. package/dist/src/storage/kv-raw-storage.d.ts +14 -8
  157. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
  158. package/dist/src/storage/kv-raw-storage.js +29 -9
  159. package/dist/src/storage/kv-raw-storage.js.map +1 -1
  160. package/dist/src/storage/memory-store-driver.d.ts +4 -1
  161. package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
  162. package/dist/src/storage/memory-store-driver.js +22 -2
  163. package/dist/src/storage/memory-store-driver.js.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +2 -2
  165. package/dist/src/storage/raw-store-driver.d.ts +44 -3
  166. package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
  167. package/dist/src/storage/shared-cache-pool.d.ts +60 -3
  168. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
  169. package/dist/src/storage/shared-cache-pool.js +68 -4
  170. package/dist/src/storage/shared-cache-pool.js.map +1 -1
  171. package/dist/src/storage/storage-repo.d.ts +204 -26
  172. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  173. package/dist/src/storage/storage-repo.js +422 -119
  174. package/dist/src/storage/storage-repo.js.map +1 -1
  175. package/dist/src/storage/store-identity.d.ts +21 -0
  176. package/dist/src/storage/store-identity.d.ts.map +1 -0
  177. package/dist/src/storage/store-identity.js +33 -0
  178. package/dist/src/storage/store-identity.js.map +1 -0
  179. package/dist/src/storage/struct.d.ts +15 -0
  180. package/dist/src/storage/struct.d.ts.map +1 -1
  181. package/dist/src/storage/with-read-cache.d.ts +92 -0
  182. package/dist/src/storage/with-read-cache.d.ts.map +1 -0
  183. package/dist/src/storage/with-read-cache.js +132 -0
  184. package/dist/src/storage/with-read-cache.js.map +1 -0
  185. package/dist/src/sync/service.d.ts +13 -3
  186. package/dist/src/sync/service.d.ts.map +1 -1
  187. package/dist/src/sync/service.js +7 -30
  188. package/dist/src/sync/service.js.map +1 -1
  189. package/dist/src/testing/mesh-harness.d.ts +75 -2
  190. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  191. package/dist/src/testing/mesh-harness.js +193 -81
  192. package/dist/src/testing/mesh-harness.js.map +1 -1
  193. package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
  194. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  195. package/dist/src/testing/raw-storage-conformance.js +97 -2
  196. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  197. package/package.json +5 -6
  198. package/src/cluster/block-transfer-service.ts +528 -312
  199. package/src/cluster/block-transfer.ts +547 -424
  200. package/src/cluster/certified-claims.ts +319 -0
  201. package/src/cluster/client-signature-verifier.ts +34 -0
  202. package/src/cluster/cluster-policy.ts +294 -203
  203. package/src/cluster/cluster-repo.ts +2501 -2034
  204. package/src/cluster/commit-proof.ts +344 -0
  205. package/src/cluster/quorum-restore.ts +348 -223
  206. package/src/cluster/rebalance-monitor.ts +539 -254
  207. package/src/cluster/reconcile-block.ts +404 -203
  208. package/src/cluster/service.ts +23 -20
  209. package/src/cluster/spread-on-churn.ts +352 -346
  210. package/src/cohort-topic/host.ts +8 -7
  211. package/src/cohort-topic/stream-util.ts +14 -43
  212. package/src/dispute/cascade.ts +31 -15
  213. package/src/dispute/dispute-service.ts +706 -696
  214. package/src/dispute/invalidation.ts +162 -52
  215. package/src/dispute/service.ts +126 -125
  216. package/src/index.ts +7 -0
  217. package/src/libp2p-key-network.ts +1204 -1235
  218. package/src/libp2p-node-base.ts +185 -70
  219. package/src/network/open-protocol-stream.ts +87 -0
  220. package/src/network/register-protocol-handler.ts +85 -0
  221. package/src/peer-address-book.ts +96 -2
  222. package/src/pend-validation.ts +89 -0
  223. package/src/reactivity/notify-transport.ts +2 -1
  224. package/src/reactivity/push-state-gossip.ts +2 -1
  225. package/src/repo/cluster-coordinator.ts +65 -11
  226. package/src/repo/coordinator-repo.ts +729 -71
  227. package/src/repo/served-repo-proxy.ts +55 -0
  228. package/src/repo/service.ts +25 -16
  229. package/src/reputation/types.ts +3 -1
  230. package/src/rn.ts +34 -4
  231. package/src/storage/block-archive.ts +242 -0
  232. package/src/storage/block-latch.ts +144 -0
  233. package/src/storage/block-storage.ts +787 -498
  234. package/src/storage/cached-raw-storage.ts +21 -1
  235. package/src/storage/cached-store-driver.ts +898 -859
  236. package/src/storage/i-block-storage.ts +172 -116
  237. package/src/storage/i-raw-storage.ts +40 -0
  238. package/src/storage/kv-raw-storage.ts +34 -9
  239. package/src/storage/memory-store-driver.ts +26 -2
  240. package/src/storage/raw-store-codec.ts +42 -42
  241. package/src/storage/raw-store-driver.ts +45 -3
  242. package/src/storage/shared-cache-pool.ts +516 -452
  243. package/src/storage/storage-repo.ts +1421 -1014
  244. package/src/storage/store-identity.ts +41 -0
  245. package/src/storage/struct.ts +52 -34
  246. package/src/storage/with-read-cache.ts +182 -0
  247. package/src/sync/service.ts +178 -198
  248. package/src/testing/mesh-harness.ts +524 -344
  249. package/src/testing/raw-storage-conformance.ts +114 -2
@@ -5,7 +5,6 @@ import { identify, identifyPush } from '@libp2p/identify';
5
5
  import { ping } from '@libp2p/ping';
6
6
  import { dcutr } from '@libp2p/dcutr';
7
7
  import { autoNAT } from '@libp2p/autonat';
8
- import { gossipsub } from '@chainsafe/libp2p-gossipsub';
9
8
  import { bootstrap } from '@libp2p/bootstrap';
10
9
  import { circuitRelayServer, type CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
11
10
  import { peerIdFromString } from '@libp2p/peer-id';
@@ -14,10 +13,13 @@ import type { ConnectionGater, PrivateKey } from '@libp2p/interface';
14
13
  import { clusterService } from './cluster/service.js';
15
14
  import { blockTransferService } from './cluster/block-transfer-service.js';
16
15
  import { repoService } from './repo/service.js';
17
- import { StorageRepo, withBlockCommitLatch } from './storage/storage-repo.js';
16
+ import { StorageRepo } from './storage/storage-repo.js';
18
17
  import { BlockStorage } from './storage/block-storage.js';
19
18
  import { MemoryRawStorage } from './storage/memory-storage.js';
19
+ import { withReadCache, type ResolvedReadCache } from './storage/with-read-cache.js';
20
20
  import type { IRawStorage } from './storage/i-raw-storage.js';
21
+ import { latestClaimFromArchive, servableProof, type ArchiveServingRepo } from './storage/block-archive.js';
22
+ import { createServedRepoProxy } from './repo/served-repo-proxy.js';
21
23
  import { seedOwnedBlocksFromStorage } from './owned-block-seed.js';
22
24
  import { clusterMember, type ReconcileBlockCallback, type CommitCertificateSink, type DeriveExpectedClusterCallback } from './cluster/cluster-repo.js';
23
25
  import { createReconcileBlock } from './cluster/reconcile-block.js';
@@ -26,7 +28,7 @@ import { assertClusterSizeCoupling } from './cluster/cluster-size-coupling.js';
26
28
  import { createCommitCertStore, makeClusterCommitCertExtractor, type CommitCertStore } from './cluster/commit-cert.js';
27
29
  import { coordinatorRepo } from './repo/coordinator-repo.js';
28
30
  import { Libp2pKeyPeerNetwork, type NetworkMode, type NetworkStatePersistence } from './libp2p-key-network.js';
29
- import { mergePeerAddresses, publishableConnectionAddr, type AddressLog } from './peer-address-book.js';
31
+ import { mergePeerAddresses, publishableAddrsForPeer, type AddressLog } from './peer-address-book.js';
30
32
  import type { OptimysticNode, OptimysticNodeAttachments } from './optimystic-node.js';
31
33
  import { ClusterClient } from './cluster/client.js';
32
34
  import type { IRepo, ICluster, ITransactionValidator, BlockId, IBlockChangeNotifier } from '@optimystic/db-core';
@@ -135,7 +137,18 @@ const reactivityWiringLog = createLogger('reactivity-node-wiring');
135
137
  */
136
138
  const wiringLog = createLogger('node-wiring');
137
139
 
138
- /** Factory function or instance for creating raw storage */
140
+ /**
141
+ * Factory function or instance for creating raw storage. The node puts the write-through read
142
+ * cache in front of the resolved instance (`withReadCache`, unless it is a `MemoryRawStorage` or
143
+ * already cached) under a lease, and releases THAT LEASE when it stops; the instance you supplied
144
+ * is never disposed. The cache is shared per backing store: two concurrently running nodes handed
145
+ * one uncached instance — or two instances that report the same `getStoreIdentity()`, such as two
146
+ * `FileRawStorage` over one directory — read and write through ONE cache, which is cleared and
147
+ * unregistered only when the last of them stops. Sequential reuse (a restart over the same
148
+ * instance) starts cold once the previous node's lease has released. What remains unguarded is
149
+ * the cross-process case (Invariant 5 in `packages/db-p2p/docs/storage.md`). A host that builds
150
+ * its own `CachedRawStorage` and hands it in keeps owning it; the node never releases it.
151
+ */
139
152
  export type RawStorageProvider = IRawStorage | (() => IRawStorage);
140
153
 
141
154
  /**
@@ -180,7 +193,7 @@ export type NodeOptions = ClusterPolicyOptions & {
180
193
  * `{ reservations: { applyDefaultLimit: false } }` to lift the cap.
181
194
  */
182
195
  relayServerInit?: CircuitRelayServerInit;
183
- /** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. */
196
+ /** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. See {@link RawStorageProvider} for the ownership rule. */
184
197
  storage?: RawStorageProvider;
185
198
  /** Override libp2p listen multiaddrs. */
186
199
  listenAddrs?: string[];
@@ -225,11 +238,15 @@ export type NodeOptions = ClusterPolicyOptions & {
225
238
 
226
239
  /**
227
240
  * Rebalance reaction tuning. Drives the RebalanceMonitor + BlockTransferCoordinator pull-gained/
228
- * push-lost path when arachnode/FRET are available (the only place fretAdapter + restoration
229
- * coordinator exist). Absent -> enabled with defaults (see RebalanceMonitorConfig). Set
230
- * { enabled: false } to disable the rebalance reaction on this node. When arachnode is disabled
231
- * or FRET is absent the rebalance path stays inert regardless of this flag (rebalance is a
232
- * resilience optimization, not a correctness requirement).
241
+ * push-lost/replicate-grown path when arachnode/FRET are available (the only place fretAdapter +
242
+ * restoration coordinator exist). The grown arm is what pushes a block this node keeps to peers
243
+ * that newly became co-responsible for it the founder case: anything committed while the
244
+ * deployment was one node gets its second copy only through this path (bounded per pass by
245
+ * `growthBlockBudget`). Absent -> enabled with defaults (see RebalanceMonitorConfig). Set
246
+ * { enabled: false } to disable the rebalance reaction (including the grown arm) on this node.
247
+ * When arachnode is disabled or FRET is absent the rebalance path stays inert regardless of this
248
+ * flag (rebalance is a resilience optimization — except for singly-held blocks, where the grown
249
+ * arm is currently the only mechanism that ever creates a second copy).
233
250
  */
234
251
  rebalance?: Partial<RebalanceMonitorConfig> & { enabled?: boolean };
235
252
 
@@ -242,6 +259,21 @@ export type NodeOptions = ClusterPolicyOptions & {
242
259
  /** Dispute protocol configuration */
243
260
  dispute?: Partial<DisputeConfig>;
244
261
 
262
+ /**
263
+ * Block-transfer (churn re-replication) receiver tuning.
264
+ *
265
+ * `requirePushCertificate` (default `true`) refuses a pushed block that carries no verifying
266
+ * cohort commit proof — see `BlockTransferServiceInit.requirePushCertificate` for why that is
267
+ * the default. It is exposed here because the migration it exists for is a DEPLOYMENT decision
268
+ * and the escape hatch lives on the RECEIVER: a node still holding pre-proof blocks cannot
269
+ * certify them (the signatures no longer exist), so under the strict default those blocks never
270
+ * gain a new holder — which also means `BlockTransferCoordinator.confirmReplicated` never
271
+ * confirms them, so a rebalance never releases them and a ring shift's confirm phase aborts on
272
+ * them (`docs/arachnode-ring-handoff.md` § Phase B). Clearing the backlog means running the
273
+ * receivers with this `false` until every such block has been rewritten under current code.
274
+ */
275
+ blockTransfer?: { requirePushCertificate?: boolean };
276
+
245
277
  /** Optional persistent store for 2PC transaction state (enables crash recovery) */
246
278
  transactionStateStore?: ITransactionStateStore;
247
279
 
@@ -327,11 +359,30 @@ export type NodeOptions = ClusterPolicyOptions & {
327
359
  connectionGater?: ConnectionGater;
328
360
  };
329
361
 
330
- function resolveStorage(provider: RawStorageProvider | undefined): IRawStorage {
362
+ /**
363
+ * Resolve the node's raw storage and put the write-through read cache in front of it. This is
364
+ * the single place the network node resolves its `IRawStorage`, so it is the single place the
365
+ * cache is wired (`withReadCache` states the exclusions: memory storage and already-cached
366
+ * storage pass through unchanged). The default is a bare `MemoryRawStorage`, deliberately not
367
+ * routed through the helper — nothing to cache.
368
+ *
369
+ * `lease` is this node's claim on the (possibly shared) cache, and the ONLY thing its stop path
370
+ * may release — a host that supplied its own `CachedRawStorage` keeps owning it (see
371
+ * {@link ResolvedReadCache}).
372
+ */
373
+ function resolveStorage(provider: RawStorageProvider | undefined, networkName: string): ResolvedReadCache {
331
374
  if (!provider) {
332
- return new MemoryRawStorage();
375
+ return { storage: new MemoryRawStorage(), lease: undefined };
333
376
  }
334
- return typeof provider === 'function' ? provider() : provider;
377
+ const storage = typeof provider === 'function' ? provider() : provider;
378
+ // NOTE: the label is the network name, so N nodes on one network in one process over N DISTINCT
379
+ // stores produce N identically-labelled rows in `SharedCachePool.stats()` (the pool keys on a
380
+ // monotonic store id, not the label); N nodes over ONE store share a single row, labelled by
381
+ // whichever node wrapped first. Harmless while the label is only read by a human eyeballing
382
+ // occupancy; if pool stats ever need to attribute bytes to a SPECIFIC node, fold the peer id
383
+ // in — it is not known here, so that would mean labelling after node construction rather than
384
+ // at resolve time.
385
+ return withReadCache(storage, `node:${networkName}`);
335
386
  }
336
387
 
337
388
  /**
@@ -374,7 +425,7 @@ export async function createLibp2pNodeBase(
374
425
  transports: Libp2pTransports;
375
426
  }
376
427
  ): Promise<OptimysticNode> {
377
- const rawStorage = resolveStorage(options.storage);
428
+ const { storage: rawStorage, lease } = resolveStorage(options.storage, options.networkName);
378
429
 
379
430
  // Create placeholder restore callback (will be replaced after node starts)
380
431
  let restoreCallback: RestoreCallback = async (_blockId, _rev?) => {
@@ -386,15 +437,6 @@ export async function createLibp2pNodeBase(
386
437
  new BlockStorage(blockId, rawStorage, restoreCallback)
387
438
  );
388
439
 
389
- // Per-block commit-latch runner, ready to thread into the invalidation-apply sink (`onInvalidate`)
390
- // passed to `clusterMember(...)` and into each cascade `CollectionEnv`, the instant either is wired
391
- // here. Sharing the `StorageRepo.commit:<blockId>` latch makes a compensating saveReplica/saveDeletion
392
- // RMW of `meta.latest` mutually exclusive with a concurrent commit on the same block. It is unused
393
- // today only because no `onInvalidate`/cascade driver is wired in the live node (see review handoff);
394
- // it is bound here so that wiring is a one-liner and cannot reach for a divergent latch key.
395
- const blockCommitLatch = withBlockCommitLatch;
396
- void blockCommitLatch;
397
-
398
440
  let clusterImpl: ICluster | undefined;
399
441
  let coordinatedRepo: IRepo | undefined;
400
442
  // The running node, bound immediately after `createLibp2p` below. Service factories that need
@@ -415,24 +457,11 @@ export async function createLibp2pNodeBase(
415
457
  }
416
458
  };
417
459
 
418
- const repoProxy: IRepo = {
419
- async get(blockGets, options) {
420
- const target = coordinatedRepo ?? storageRepo;
421
- return await target.get(blockGets, options);
422
- },
423
- async pend(request, options) {
424
- const target = coordinatedRepo ?? storageRepo;
425
- return await target.pend(request, options);
426
- },
427
- async cancel(trxRef, options) {
428
- const target = coordinatedRepo ?? storageRepo;
429
- return await target.cancel(trxRef, options);
430
- },
431
- async commit(request, options) {
432
- const target = coordinatedRepo ?? storageRepo;
433
- return await target.commit(request, options);
434
- }
435
- };
460
+ // Built by a named factory, not an inline literal: see `createServedRepoProxy` for why (an
461
+ // inline object here is unreachable from every test that does not boot a libp2p node, which is
462
+ // how it served every repair archive without its commit proof). `coordinatedRepo` is read per
463
+ // call because it is assigned further down, after the cluster is assembled.
464
+ const repoProxy: ArchiveServingRepo = createServedRepoProxy(storageRepo, () => coordinatedRepo);
436
465
 
437
466
  // The ONE authorization slice, spread verbatim into all four database-protocol service inits
438
467
  // below. Building it once (rather than repeating two option reads per service) is what makes
@@ -547,10 +576,17 @@ export async function createLibp2pNodeBase(
547
576
  // is acceptable — they neither throw nor break the build in that case.
548
577
  dcutr: dcutr(),
549
578
  autoNAT: autoNAT(),
550
- pubsub: gossipsub({
551
- allowPublishToZeroTopicPeers: true,
552
- heartbeatInterval: 7000
553
- }),
579
+ // NOTE: no `pubsub` service. `@chainsafe/libp2p-gossipsub` (currently at 14.1.2, the newest
580
+ // published version) is built against `@libp2p/interface@^2` — its `Stream` still expects
581
+ // `.sink`/`.source`. Under libp2p 3 `Stream extends MessageStream` (event target +
582
+ // AsyncIterable), which has neither, so `it-pipe`'s `isDuplex` check on the outbound stream
583
+ // fails and `OutboundStream`'s constructor throws `fns.shift(...) is not a function`
584
+ // synchronously — before gossipsub's own `.catch` can attach. Gossipsub logs and swallows
585
+ // it, so the service reports as running while never sending a subscription to any peer:
586
+ // `publish` always resolves with zero recipients. Verified end-to-end against this repo's
587
+ // installed dependency tree (gotchoices/Optimystic#9). Nothing in this repo uses `pubsub`
588
+ // today — re-add only against a gossipsub release built for libp2p 3.
589
+
554
590
  // Circuit relay server - enables this node to relay connections for other peers
555
591
  ...(options.relay ? { relay: circuitRelayServer(options.relayServerInit) } : {}),
556
592
 
@@ -571,17 +607,13 @@ export async function createLibp2pNodeBase(
571
607
  peerId: components.peerId,
572
608
  // Fallback addr resolver for redirect targets whose multiaddrs are not
573
609
  // already embedded in record.peers. A redirect payload is handed to a THIRD
574
- // party, so it obeys the same rule the cluster record does — see
575
- // `publishableConnectionAddr`: only an outbound connection's remoteAddr is an
576
- // address anyone else can reach.
577
- getConnectionAddrs: (peerId: any) => {
578
- const conns = liveNode?.getConnections?.(peerId) ?? [];
579
- const addrs: string[] = [];
580
- for (const c of conns) {
581
- const addr = publishableConnectionAddr(c, addressLog);
582
- if (addr !== undefined) addrs.push(addr);
583
- }
584
- return addrs;
610
+ // party, so it answers with `publishableAddrsForPeer` the same rule, and the
611
+ // same function, `findCluster` uses to fill a cluster record: the publishable
612
+ // half of our live connections plus the peer's own advertised addresses.
613
+ getConnectionAddrs: async (peerId: any) => {
614
+ if (!liveNode) return [];
615
+ const conns = liveNode.getConnections?.(peerId) ?? [];
616
+ return await publishableAddrsForPeer(liveNode, conns, peerId, addressLog);
585
617
  },
586
618
  // Inbound cluster records carry each cohort member's multiaddrs. libp2p only
587
619
  // propagates addresses between directly-connected peers, so for a cohort chosen
@@ -635,11 +667,20 @@ export async function createLibp2pNodeBase(
635
667
  blockTransfer: (components: any) => {
636
668
  const serviceFactory = blockTransferService({
637
669
  protocolPrefix: `/optimystic/${options.networkName}`,
670
+ // Absent → the strict default inside the service; see `blockTransfer` on the options.
671
+ ...(options.blockTransfer?.requirePushCertificate !== undefined
672
+ ? { requirePushCertificate: options.blockTransfer.requirePushCertificate }
673
+ : {}),
638
674
  ...inboundAuthorization
639
675
  });
640
676
  return serviceFactory({
641
677
  registrar: components.registrar,
642
678
  repo: storageRepo,
679
+ // Verifying a pushed block's commit proof needs the cohort's super-majority fraction.
680
+ // Read from the SAME resolved `consensusConfig` the member and coordinator read (whose
681
+ // coupling `assertSuperMajorityCoupling` below already asserts) — a third copy resolving
682
+ // its own default would defeat that.
683
+ superMajorityThreshold: consensusConfig.superMajorityThreshold,
643
684
  // So this service's authorization denials reach the same error sink as the other three.
644
685
  logger: components.logger
645
686
  });
@@ -694,6 +735,26 @@ export async function createLibp2pNodeBase(
694
735
  // soon as the protocol handler goes live in start().
695
736
  liveNode = node;
696
737
 
738
+ // Release this node's lease on the raw-storage read cache when the node stops. Installed FIRST
739
+ // — before start() and before every other stop wrapper — so it runs LAST in the wrapper chain,
740
+ // after every monitor and service that may still read storage during its own stop. Only wired
741
+ // when the seam handed out a lease: a MemoryRawStorage passes through unwrapped, and a
742
+ // host-supplied `CachedRawStorage` stays the host's to dispose. Releasing a lease is safe even
743
+ // when other nodes share the cache — it is cleared and unregistered only when the last lease
744
+ // goes, so stopping one node never blinds the others. A skipped release leaks only cold
745
+ // entries the pool evicts under pressure; the point of the polite release is honest pool
746
+ // occupancy on a long-lived provider node.
747
+ if (lease) {
748
+ const previousStop = node.stop.bind(node);
749
+ node.stop = async () => {
750
+ try {
751
+ await previousStop();
752
+ } finally {
753
+ await lease.release();
754
+ }
755
+ };
756
+ }
757
+
697
758
  // Inject the REAL libp2p node into the services that need it, before start(). These are
698
759
  // load-bearing and the node has NOT started yet, so any throw fails fast and rejects node
699
760
  // creation (nothing started leaks) — far better than the service silently falling back to the
@@ -777,10 +838,21 @@ export async function createLibp2pNodeBase(
777
838
  const reconcileBlock: ReconcileBlockCallback = createReconcileBlock({
778
839
  selfPeerId: node.peerId.toString(),
779
840
  fetchArchive: fetchArchiveFromPeer,
780
- saveReplicatedBlock: (blockId, block, source) => storageRepo.saveReplicatedBlock(blockId, block, source),
841
+ // The 4th parameter is a proof reconcile verified against these exact bytes; it is
842
+ // persisted so the repaired replica serves it onward (see StorageRepo.saveReplicatedBlock).
843
+ saveReplicatedBlock: (blockId, block, source, verifiedProof) =>
844
+ storageRepo.saveReplicatedBlock(blockId, block, source, verifiedProof),
781
845
  simpleMajorityThreshold: consensusConfig.simpleMajorityThreshold,
846
+ superMajorityThreshold: consensusConfig.superMajorityThreshold,
782
847
  repairCorroborationClusterSize: consensusConfig.repairCorroborationClusterSize,
783
848
  reputation
849
+ // `anchoring` (proof layer-2, `ProofAnchoring`) is intentionally NOT wired here yet — nor
850
+ // is the coordinator's `proofAnchoring` below: a real implementation re-derives the
851
+ // block's cohort from `keyNetwork.findCluster` (the same source `deriveExpectedCluster`
852
+ // uses) and needs a churn-tolerance window so historic cohort rotation does not read as an
853
+ // anomaly flood. Until then certification runs on layer-1 cryptography and LOGS the
854
+ // unanchored residual. See `feat-cluster-membership-threshold-cert-anchoring`, and the
855
+ // matching `recomputeArbitratorSet` note at the clusterMember construction below.
784
856
  });
785
857
 
786
858
  // Member-side membership derivation for the admission gate: independently re-derive this block's
@@ -855,12 +927,18 @@ export async function createLibp2pNodeBase(
855
927
  );
856
928
 
857
929
  // Create callback for querying cluster peers for their latest block revision. Three-way
858
- // contract (see ClusterLatestCallback): an ActionRev is the peer's claim, a resolved
930
+ // contract (see ClusterLatestCallback): a CertifiedActionRev is the peer's claim, a resolved
859
931
  // `undefined` is the peer answering "I hold nothing", and a REJECTION is silence — the
860
932
  // coordinator counts it as "did not answer" and refuses to report an authoritative absent
861
933
  // over it. Transport errors must therefore propagate, not collapse into `undefined` (that
862
934
  // collapse let a slow two-node cohort report a missing block as authoritatively absent —
863
935
  // ticket cluster-read-consult-cannot-report-unreachable).
936
+ //
937
+ // The claim carries the cohort's commit proof for the claimed revision when the answering
938
+ // peer retained one. It is attached UNVERIFIED — a peer chooses what to send. Both repair
939
+ // paths verify it before weighing it (the shared layer in `cluster/certified-claims.ts`):
940
+ // the coordinator's certifyClaim pass in `queryClusterForLatest`, and reconcile's
941
+ // certification pass in `cluster/reconcile-block.ts`.
864
942
  const clusterLatestCallback: ClusterLatestCallback = async (peerId, blockId, context?) => {
865
943
  // Self-read short-circuit: dialling self via SyncClient is a round trip
866
944
  // with no remote on the other end, and on nodes without listen addresses
@@ -871,7 +949,13 @@ export async function createLibp2pNodeBase(
871
949
  if (peerId.equals(node.peerId)) {
872
950
  try {
873
951
  const result = await storageRepo.get({ blockIds: [blockId], context });
874
- return result[blockId]?.state?.latest;
952
+ const latest = result[blockId]?.state?.latest;
953
+ if (!latest) return undefined;
954
+ // The SAME lookup a peer serving an archive uses, so a self answer and a remote
955
+ // answer attach proofs by one rule (including the mis-pairing guard). It never
956
+ // throws — a proof fault degrades to "no proof", never to a lost claim.
957
+ const proof = await servableProof(storageRepo, blockId, latest);
958
+ return proof ? { ...latest, proof } : latest;
875
959
  } catch {
876
960
  return undefined;
877
961
  }
@@ -881,14 +965,11 @@ export async function createLibp2pNodeBase(
881
965
  // per-peer deadline also bounds a hung request — slowness needs no race here.
882
966
  const response = await syncClient.requestBlock({ blockId, rev: undefined });
883
967
  if (response.success && response.archive) {
884
- const revisions = Object.keys(response.archive.revisions).map(Number);
885
- if (revisions.length > 0) {
886
- const maxRev = Math.max(...revisions);
887
- const revisionData = response.archive.revisions[maxRev];
888
- if (revisionData?.action) {
889
- return { actionId: revisionData.action.actionId, rev: maxRev };
890
- }
891
- }
968
+ // Projection lives with the archive shape (`latestClaimFromArchive`) rather than
969
+ // re-read inline here — it reads the claim and its proof out of ONE revision entry,
970
+ // so a serving peer cannot pair a proof with a revision it does not certify.
971
+ const claim = latestClaimFromArchive(response.archive);
972
+ if (claim) return claim;
892
973
  }
893
974
  // The peer DID answer, without data: `success:false` is the sync service's "Block not
894
975
  // found in local storage", and an archive with no usable revisions holds nothing either
@@ -1065,7 +1146,11 @@ export async function createLibp2pNodeBase(
1065
1146
  // Setup restoration coordinator with FRET adapter
1066
1147
  const restorationCoordinatorV2 = new RestorationCoordinator(
1067
1148
  fretAdapter,
1068
- { connect: (pid, protocol) => node.dialProtocol(pid as Parameters<typeof node.dialProtocol>[0], [protocol]) },
1149
+ // The node's own IPeerNetwork, not an inline `dialProtocol` lambda: the lambda opened
1150
+ // streams without `runOnLimitedConnection`, so a block holder reachable only through a
1151
+ // relay looked like a peer that simply did not have the block, and it dropped the
1152
+ // caller's AbortSignal, so `SyncClient`'s per-peer dial deadline never bounded a dial.
1153
+ keyNetwork,
1069
1154
  `/optimystic/${options.networkName}`,
1070
1155
  node.peerId.toString()
1071
1156
  );
@@ -1135,6 +1220,18 @@ export async function createLibp2pNodeBase(
1135
1220
  // async loop. Adding/deleting a Set entry during iteration does not throw in JS — entries
1136
1221
  // are visited best-effort — which is acceptable for a resilience mechanism, so we
1137
1222
  // document it here rather than add locking.
1223
+ //
1224
+ // The event's `grown` arm (blocks this node KEEPS whose cohort acquired new peers — the
1225
+ // founder/cohort-growth case): handleRebalanceEvent pushes each grown block to the newly
1226
+ // co-responsible peers and returns a per-block GrowthOutcome in `result.growth`, which is
1227
+ // fed back into the monitor here. The monitor records a peer as seen ONLY off that
1228
+ // feedback (a confirmed replica, or a block that otherwise reached its floor), so a failed
1229
+ // push is re-detected on the next check instead of being silently dropped — bounded by
1230
+ // growthMaxAttempts, after which the monitor abandons the peer for that block (visible in
1231
+ // getGrowthDiagnostics). Nothing is released or untracked off the grown arm — the node
1232
+ // keeps serving the block either way. On the catch path NOTHING is recorded: the monitor's
1233
+ // state stays un-advanced and the next check retries, the correct outcome for a reaction
1234
+ // that threw.
1138
1235
  rebalanceMonitor.onRebalance((event) => {
1139
1236
  for (const blockId of event.gained) ownedBlocks.add(blockId);
1140
1237
  coordinator.handleRebalanceEvent(event).then((result) => {
@@ -1142,7 +1239,25 @@ export async function createLibp2pNodeBase(
1142
1239
  rebalanceMonitor.untrackBlock(blockId); // also evicts from the shared ownedBlocks set
1143
1240
  gcEligible.add(blockId); // confirmed replicated → safe to sweep
1144
1241
  }
1242
+ for (const [blockId, outcome] of result.growth) {
1243
+ rebalanceMonitor.recordGrowthOutcome(blockId, outcome);
1244
+ }
1245
+ if (result.underReplicated.length > 0) {
1246
+ const growthDiag = rebalanceMonitor.getGrowthDiagnostics();
1247
+ log?.('cohort-growth: %d of %d grown blocks not confirmed on new peers this pass ' +
1248
+ '(awaiting-confirmation=%d given-up-pairs=%d)',
1249
+ result.underReplicated.length, event.grown.size,
1250
+ growthDiag.blocksAwaitingConfirmation, growthDiag.abandonedPairs);
1251
+ }
1145
1252
  }).catch((err) => {
1253
+ // NOTE: accepted tradeoff — recording nothing here means growthMaxAttempts (which
1254
+ // only counts RECORDED incomplete outcomes) never bounds a reaction that throws
1255
+ // every time, so the re-check timer retries it at growthRecheckIntervalMs forever,
1256
+ // logging each failure. Kept deliberately: per-peer errors are already caught
1257
+ // inside the coordinator, so a throw out of handleRebalanceEvent is a coding bug,
1258
+ // and a loud unbounded retry is the right way to surface one — silently abandoning
1259
+ // the block would hide it and leave the block singly held. Revisit if a legitimate
1260
+ // recoverable condition is ever allowed to throw out of the reaction.
1146
1261
  log?.('rebalance reaction failed: %o', err);
1147
1262
  });
1148
1263
  });
@@ -1503,13 +1618,13 @@ export async function createLibp2pNodeBase(
1503
1618
 
1504
1619
  // Inbound notify frames → forwarder host (subscriber role delivers in-process; forwarder role fans out).
1505
1620
  // NOTE: the four `register*Handler` helpers below (notify / pushStateGossip / recover /
1506
- // matchmaking query) all call `node.handle(...)` fire-and-forget (`void`), so a rejected
1621
+ // matchmaking query) all call `registerProtocolHandler(...)` fire-and-forget (`void`), so a rejected
1507
1622
  // registration escapes the post-start rollback `catch` as an UNHANDLED rejection instead of
1508
1623
  // failing node creation. Harmless today — every protocol id here is a fixed constant registered
1509
1624
  // exactly once, so the only realistic rejection is a duplicate, and that needs a caller to pass
1510
1625
  // overlapping custom `cohortTopic.host.protocols`. If any of these ids ever becomes
1511
1626
  // caller-configurable, or a helper grows a registration that can genuinely fail, make them await
1512
- // their `node.handle` so the failure reaches the rollback.
1627
+ // their `registerProtocolHandler` so the failure reaches the rollback.
1513
1628
  registerNotifyHandler(node, reactivityProtocols.notify, notify);
1514
1629
  offInboundNotify = notify.onNotification((from, n): void => { void forwarderHost.onInbound(from, n); });
1515
1630
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The single place in `db-p2p` that opens a libp2p protocol stream.
3
+ *
4
+ * libp2p refuses to open a protocol stream over a *limited* (circuit-relay) connection unless the
5
+ * caller opts in with `runOnLimitedConnection: true`. Omitting it produces no compile error and no
6
+ * obvious failure — only "that peer never answers" — so every peer reachable only through a relay
7
+ * (the steady state for browsers, phones, and NATed peers) silently drops out. The flag is
8
+ * deliberately NOT an option here: a new call site gets relay support without its author needing to
9
+ * know the flag exists, and `test/dial-options-single-site.spec.ts` fails the build if a second
10
+ * source file calls `dialProtocol` / `newStream` directly.
11
+ *
12
+ * NOTE: accepted tradeoff — this duplicates FRET's `rpc/protocols.ts#openRpcStream` (exported since
13
+ * `p2p-fret@1.0.0-beta.3`) rather than delegating to it. FRET's helper pins `negotiateFully: false`
14
+ * with no way to opt out, and `cohort-topic/stream-util.ts` deliberately does not set that option
15
+ * (see the NOTE at its call sites); adopting FRET's would silently reverse a decision a human
16
+ * already made. It also has no hook for the pre-dial check `libp2p-key-network.ts#connect` runs.
17
+ * Revisit — and delete this module in favour of `openRpcStream` — if FRET ever parameterizes
18
+ * `negotiateFully`.
19
+ *
20
+ * Imports here are type-only on purpose: this module is reachable from the react-native entry
21
+ * (`src/rn.ts`) via `libp2p-key-network.ts`, which must not pull node-specific code.
22
+ */
23
+
24
+ import type { Libp2p } from "libp2p";
25
+ import type { Connection, PeerId, Stream } from "@libp2p/interface";
26
+
27
+ export interface OpenProtocolStreamOptions {
28
+ /** Forwarded to libp2p so a caller's deadline cancels both the connection reuse and the dial. */
29
+ signal?: AbortSignal;
30
+ /**
31
+ * Omit for libp2p's default (full multistream-select negotiation at stream-open).
32
+ * Pass `false` to save the round trip, accepting that an unsupported-protocol failure is
33
+ * deferred to the first read — only safe when the caller always reads a reply.
34
+ */
35
+ negotiateFully?: boolean;
36
+ /**
37
+ * Runs immediately before a FRESH dial, and never on the connection-reuse path.
38
+ * Throwing aborts the open. This is the seam for checks that are only meaningful when no
39
+ * connection exists yet (see `libp2p-key-network.ts#assertNotSelfRelayOnly`).
40
+ */
41
+ beforeDial?: () => Promise<void> | void;
42
+ }
43
+
44
+ /**
45
+ * True for a circuit-relay ("limited") connection: libp2p stamps one with per-circuit `limits`
46
+ * (data/duration caps); sniffing `/p2p-circuit` in the remote multiaddr covers transports and
47
+ * versions that leave `limits` unpopulated.
48
+ */
49
+ export function isLimitedConnection(c: Connection): boolean {
50
+ if (c.limits != null) return true;
51
+ return c.remoteAddr?.toString?.().includes("/p2p-circuit") ?? false;
52
+ }
53
+
54
+ /**
55
+ * Open `protocol` to `peer`, reusing a healthy existing connection when there is one.
56
+ *
57
+ * Skips connections libp2p has not yet evicted from its index but that are no longer open, and
58
+ * prefers a direct connection over a relayed one — a relayed connection can be reset once the
59
+ * relay's per-circuit cap or reservation lapses, and after DCUtR upgrades a link to direct both
60
+ * briefly coexist. Falls back to the relayed connection when it is the only open path.
61
+ */
62
+ export async function openProtocolStream(
63
+ node: Libp2p,
64
+ peer: PeerId,
65
+ protocol: string,
66
+ options?: OpenProtocolStreamOptions,
67
+ ): Promise<Stream> {
68
+ // Before touching connections: a caller that has already given up is owed its own reason,
69
+ // not whatever libp2p would report several layers down.
70
+ options?.signal?.throwIfAborted();
71
+
72
+ const conns = node.getConnections?.(peer) ?? [];
73
+ const open = conns.filter(c => c?.status === "open" && typeof c?.newStream === "function");
74
+ const chosen = open.find(c => !isLimitedConnection(c)) ?? open[0];
75
+
76
+ // Keys are OMITTED rather than set to `undefined` when the caller did not supply them, so
77
+ // libp2p applies its own defaults instead of seeing an explicit `undefined`.
78
+ const streamOptions = {
79
+ runOnLimitedConnection: true,
80
+ ...(options?.negotiateFully !== undefined ? { negotiateFully: options.negotiateFully } : {}),
81
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
82
+ };
83
+
84
+ if (chosen) return await chosen.newStream([protocol], streamOptions);
85
+ await options?.beforeDial?.();
86
+ return await node.dialProtocol(peer, [protocol], streamOptions);
87
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The single place in `db-p2p` that registers a libp2p protocol handler.
3
+ *
4
+ * The mirror image of `./open-protocol-stream.ts`. Opening a stream over a *limited* (circuit-relay)
5
+ * connection needs BOTH sides to opt in, and libp2p checks each side against its own options:
6
+ *
7
+ * - the dialer's `newStream` / `dialProtocol` options (`libp2p/src/connection.ts`, outbound path), and
8
+ * - the options the *answering* peer passed when it registered the handler
9
+ * (`libp2p/src/connection.ts`, incoming path — it reads back
10
+ * `registrar.getHandler(protocol).options.runOnLimitedConnection`).
11
+ *
12
+ * Miss either one and libp2p throws `LimitedConnectionError`. On the answering side it does so
13
+ * *after* multistream-select has already acknowledged the protocol, so the dialer's stream open
14
+ * appears to succeed and the stream is then reset with no reply — indistinguishable, from the
15
+ * dialer's seat, from a peer that simply does not hold the data. Every peer reachable only through
16
+ * a capped relay (phones, browsers, machines behind a home router) silently drops out.
17
+ *
18
+ * `openProtocolStream` fixed the dialling half; this fixes the answering half. As there, the flag is
19
+ * deliberately NOT an option: a new protocol gets relay support without its author needing to know
20
+ * the flag exists, and `test/dial-options-single-site.spec.ts` fails the build if a second source
21
+ * file calls `.handle(...)` directly.
22
+ *
23
+ * Accepting the stream is necessary but not sufficient. A stock relay also caps the *whole* relayed
24
+ * connection — `applyDefaultLimit: true` stamps `Limit { data: 128 KiB, duration: 2 min }` on every
25
+ * reservation — so a relay-only peer now answers, but the circuit is reset once either cap is hit,
26
+ * which one cohort frame (`DEFAULT_STREAM_MAX_BYTES`, 512 KiB) can do on its own. Lifting that is
27
+ * the relay operator's knob, not this one: see `NodeOptions.relayServerInit` in `libp2p-node-base.ts`.
28
+ *
29
+ * Imports here are type-only on purpose: this module is reachable from the react-native entry
30
+ * (`src/rn.ts`), which must not pull node-specific code.
31
+ */
32
+
33
+ import type { StreamHandler, StreamHandlerOptions, StreamMiddleware } from "@libp2p/interface";
34
+
35
+ /**
36
+ * Anything that can register a libp2p protocol handler.
37
+ *
38
+ * Both shapes in this package satisfy it: a `Libp2p` node (`node.handle(...)`) and the `registrar`
39
+ * component the service classes are constructed with (`components.registrar.handle(...)`).
40
+ */
41
+ export interface ProtocolRegistrar {
42
+ handle(protocol: string, handler: StreamHandler, options?: StreamHandlerOptions): Promise<void>;
43
+ }
44
+
45
+ /**
46
+ * The per-protocol settings that legitimately vary. `runOnLimitedConnection` is absent on purpose —
47
+ * it is a constant of this helper, not a caller's choice.
48
+ */
49
+ export interface RegisterProtocolHandlerOptions {
50
+ /** Concurrent inbound streams allowed per connection. Omit for libp2p's default (32). */
51
+ maxInboundStreams?: number;
52
+ /** Concurrent outbound streams allowed per connection. Omit for libp2p's default (64). */
53
+ maxOutboundStreams?: number;
54
+ /** Stream middleware, run around the handler. Omit for none. */
55
+ middleware?: StreamMiddleware[];
56
+ /** Replace an existing registration for this protocol instead of rejecting. */
57
+ force?: true;
58
+ /** Forwarded to libp2p so a caller's deadline cancels the registration. */
59
+ signal?: AbortSignal;
60
+ }
61
+
62
+ /**
63
+ * Register `handler` for `protocol`, accepting streams over limited (relay) connections.
64
+ *
65
+ * Rejects if the registrar rejects — a duplicate protocol id, or an aborted signal. Callers that
66
+ * register fire-and-forget (`void registerProtocolHandler(...)`) keep that behaviour; see the NOTE
67
+ * in `libp2p-node-base.ts` about the reactivity handlers.
68
+ */
69
+ export async function registerProtocolHandler(
70
+ target: ProtocolRegistrar,
71
+ protocol: string,
72
+ handler: StreamHandler,
73
+ options?: RegisterProtocolHandlerOptions,
74
+ ): Promise<void> {
75
+ // Keys are OMITTED rather than set to `undefined` when the caller did not supply them, so
76
+ // libp2p applies its own defaults instead of seeing an explicit `undefined`.
77
+ await target.handle(protocol, handler, {
78
+ runOnLimitedConnection: true,
79
+ ...(options?.maxInboundStreams !== undefined ? { maxInboundStreams: options.maxInboundStreams } : {}),
80
+ ...(options?.maxOutboundStreams !== undefined ? { maxOutboundStreams: options.maxOutboundStreams } : {}),
81
+ ...(options?.middleware !== undefined ? { middleware: options.middleware } : {}),
82
+ ...(options?.force !== undefined ? { force: options.force } : {}),
83
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
84
+ });
85
+ }