@optimystic/db-p2p 1.0.0-beta.3 → 1.1.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 (234) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +73 -1
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +70 -3
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/block-transfer.d.ts +16 -1
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  7. package/dist/src/cluster/block-transfer.js +73 -72
  8. package/dist/src/cluster/block-transfer.js.map +1 -1
  9. package/dist/src/cluster/client.d.ts +4 -6
  10. package/dist/src/cluster/client.d.ts.map +1 -1
  11. package/dist/src/cluster/client.js +8 -13
  12. package/dist/src/cluster/client.js.map +1 -1
  13. package/dist/src/cluster/cluster-policy.d.ts +2 -1
  14. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  15. package/dist/src/cluster/cluster-policy.js.map +1 -1
  16. package/dist/src/cluster/cluster-repo.d.ts +155 -9
  17. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  18. package/dist/src/cluster/cluster-repo.js +446 -58
  19. package/dist/src/cluster/cluster-repo.js.map +1 -1
  20. package/dist/src/cluster/commit-proof.js +2 -2
  21. package/dist/src/cluster/race-resolution.d.ts +51 -7
  22. package/dist/src/cluster/race-resolution.d.ts.map +1 -1
  23. package/dist/src/cluster/race-resolution.js +71 -7
  24. package/dist/src/cluster/race-resolution.js.map +1 -1
  25. package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
  26. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  27. package/dist/src/cluster/rebalance-monitor.js +120 -26
  28. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  29. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  30. package/dist/src/cluster/spread-on-churn.js +34 -47
  31. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  32. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  33. package/dist/src/cohort-topic/host.js +8 -6
  34. package/dist/src/cohort-topic/host.js.map +1 -1
  35. package/dist/src/cohort-topic/membership-source.js +1 -1
  36. package/dist/src/cohort-topic/membership-source.js.map +1 -1
  37. package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
  38. package/dist/src/cohort-topic/peer-codec.js +5 -2
  39. package/dist/src/cohort-topic/peer-codec.js.map +1 -1
  40. package/dist/src/cohort-topic/stream-util.d.ts +24 -3
  41. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  42. package/dist/src/cohort-topic/stream-util.js +36 -3
  43. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  44. package/dist/src/cohort-topic/topic-router.d.ts +9 -0
  45. package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
  46. package/dist/src/cohort-topic/topic-router.js +12 -2
  47. package/dist/src/cohort-topic/topic-router.js.map +1 -1
  48. package/dist/src/dispute/client.d.ts.map +1 -1
  49. package/dist/src/dispute/client.js +15 -3
  50. package/dist/src/dispute/client.js.map +1 -1
  51. package/dist/src/index.d.ts +5 -0
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +5 -0
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/libp2p-key-network.d.ts +78 -17
  56. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  57. package/dist/src/libp2p-key-network.js +234 -147
  58. package/dist/src/libp2p-key-network.js.map +1 -1
  59. package/dist/src/libp2p-node-base.d.ts +35 -1
  60. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  61. package/dist/src/libp2p-node-base.js +151 -26
  62. package/dist/src/libp2p-node-base.js.map +1 -1
  63. package/dist/src/libp2p-node-rn.d.ts +2 -2
  64. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  65. package/dist/src/libp2p-node-rn.js.map +1 -1
  66. package/dist/src/libp2p-node.d.ts +2 -2
  67. package/dist/src/libp2p-node.d.ts.map +1 -1
  68. package/dist/src/libp2p-node.js.map +1 -1
  69. package/dist/src/logger.d.ts.map +1 -1
  70. package/dist/src/logger.js +6 -1
  71. package/dist/src/logger.js.map +1 -1
  72. package/dist/src/matchmaking/query-transport.d.ts +3 -4
  73. package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
  74. package/dist/src/matchmaking/query-transport.js +9 -9
  75. package/dist/src/matchmaking/query-transport.js.map +1 -1
  76. package/dist/src/network/network-manager-service.d.ts +10 -5
  77. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  78. package/dist/src/network/network-manager-service.js +11 -2
  79. package/dist/src/network/network-manager-service.js.map +1 -1
  80. package/dist/src/network/relay-reservation.d.ts +170 -0
  81. package/dist/src/network/relay-reservation.d.ts.map +1 -0
  82. package/dist/src/network/relay-reservation.js +527 -0
  83. package/dist/src/network/relay-reservation.js.map +1 -0
  84. package/dist/src/pend-validation.d.ts +3 -2
  85. package/dist/src/pend-validation.d.ts.map +1 -1
  86. package/dist/src/pend-validation.js +3 -2
  87. package/dist/src/pend-validation.js.map +1 -1
  88. package/dist/src/protocol-client.d.ts +8 -1
  89. package/dist/src/protocol-client.d.ts.map +1 -1
  90. package/dist/src/protocol-client.js +11 -0
  91. package/dist/src/protocol-client.js.map +1 -1
  92. package/dist/src/reactivity/forwarder-host.d.ts +2 -1
  93. package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
  94. package/dist/src/reactivity/forwarder-host.js +2 -1
  95. package/dist/src/reactivity/forwarder-host.js.map +1 -1
  96. package/dist/src/reactivity/origination-manager.d.ts +3 -3
  97. package/dist/src/reactivity/origination-manager.js +3 -3
  98. package/dist/src/reactivity/recover-transport.d.ts +22 -14
  99. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  100. package/dist/src/reactivity/recover-transport.js +32 -19
  101. package/dist/src/reactivity/recover-transport.js.map +1 -1
  102. package/dist/src/reactivity/subscription-manager.d.ts +2 -2
  103. package/dist/src/reactivity/topic-bytes.d.ts +6 -7
  104. package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
  105. package/dist/src/reactivity/topic-bytes.js +8 -9
  106. package/dist/src/reactivity/topic-bytes.js.map +1 -1
  107. package/dist/src/repo/client.d.ts +5 -2
  108. package/dist/src/repo/client.d.ts.map +1 -1
  109. package/dist/src/repo/client.js +49 -40
  110. package/dist/src/repo/client.js.map +1 -1
  111. package/dist/src/repo/cluster-coordinator.d.ts +96 -11
  112. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  113. package/dist/src/repo/cluster-coordinator.js +164 -25
  114. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  115. package/dist/src/repo/coordinator-repo.d.ts +240 -122
  116. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  117. package/dist/src/repo/coordinator-repo.js +627 -473
  118. package/dist/src/repo/coordinator-repo.js.map +1 -1
  119. package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
  120. package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
  121. package/dist/src/repo/i-under-replication-ledger.js +2 -0
  122. package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
  123. package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
  124. package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
  125. package/dist/src/repo/kv-under-replication-ledger.js +287 -0
  126. package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
  127. package/dist/src/repo/responsibility.d.ts +37 -0
  128. package/dist/src/repo/responsibility.d.ts.map +1 -0
  129. package/dist/src/repo/responsibility.js +37 -0
  130. package/dist/src/repo/responsibility.js.map +1 -0
  131. package/dist/src/repo/service.d.ts +44 -15
  132. package/dist/src/repo/service.d.ts.map +1 -1
  133. package/dist/src/repo/service.js +64 -27
  134. package/dist/src/repo/service.js.map +1 -1
  135. package/dist/src/repo/stuck-reservation.d.ts +127 -0
  136. package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
  137. package/dist/src/repo/stuck-reservation.js +179 -0
  138. package/dist/src/repo/stuck-reservation.js.map +1 -0
  139. package/dist/src/repo/under-replication-drain.d.ts +208 -0
  140. package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
  141. package/dist/src/repo/under-replication-drain.js +461 -0
  142. package/dist/src/repo/under-replication-drain.js.map +1 -0
  143. package/dist/src/rn.d.ts +5 -0
  144. package/dist/src/rn.d.ts.map +1 -1
  145. package/dist/src/rn.js +5 -0
  146. package/dist/src/rn.js.map +1 -1
  147. package/dist/src/storage/block-latch.d.ts +3 -2
  148. package/dist/src/storage/block-latch.d.ts.map +1 -1
  149. package/dist/src/storage/block-latch.js +7 -4
  150. package/dist/src/storage/block-latch.js.map +1 -1
  151. package/dist/src/storage/block-storage.d.ts +45 -3
  152. package/dist/src/storage/block-storage.d.ts.map +1 -1
  153. package/dist/src/storage/block-storage.js +156 -9
  154. package/dist/src/storage/block-storage.js.map +1 -1
  155. package/dist/src/storage/i-block-storage.d.ts +57 -5
  156. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  157. package/dist/src/storage/i-block-storage.js.map +1 -1
  158. package/dist/src/storage/pending-claim.d.ts +167 -0
  159. package/dist/src/storage/pending-claim.d.ts.map +1 -0
  160. package/dist/src/storage/pending-claim.js +156 -0
  161. package/dist/src/storage/pending-claim.js.map +1 -0
  162. package/dist/src/storage/raw-store-codec.d.ts +13 -0
  163. package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +24 -3
  165. package/dist/src/storage/raw-store-codec.js.map +1 -1
  166. package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
  167. package/dist/src/storage/restoration-coordinator.js +2 -1
  168. package/dist/src/storage/restoration-coordinator.js.map +1 -1
  169. package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
  170. package/dist/src/storage/ring-shift-coordinator.js +3 -3
  171. package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
  172. package/dist/src/storage/storage-repo.d.ts +122 -5
  173. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  174. package/dist/src/storage/storage-repo.js +245 -68
  175. package/dist/src/storage/storage-repo.js.map +1 -1
  176. package/dist/src/storage/struct.d.ts +58 -0
  177. package/dist/src/storage/struct.d.ts.map +1 -1
  178. package/dist/src/testing/mesh-harness.d.ts +77 -11
  179. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  180. package/dist/src/testing/mesh-harness.js +226 -100
  181. package/dist/src/testing/mesh-harness.js.map +1 -1
  182. package/package.json +2 -2
  183. package/readme.md +27 -3
  184. package/src/cluster/block-transfer-service.ts +132 -4
  185. package/src/cluster/block-transfer.ts +79 -84
  186. package/src/cluster/client.ts +115 -119
  187. package/src/cluster/cluster-policy.ts +2 -1
  188. package/src/cluster/cluster-repo.ts +493 -65
  189. package/src/cluster/commit-proof.ts +2 -2
  190. package/src/cluster/race-resolution.ts +76 -7
  191. package/src/cluster/rebalance-monitor.ts +677 -539
  192. package/src/cluster/spread-on-churn.ts +34 -54
  193. package/src/cohort-topic/host.ts +8 -6
  194. package/src/cohort-topic/membership-source.ts +1 -1
  195. package/src/cohort-topic/peer-codec.ts +34 -31
  196. package/src/cohort-topic/stream-util.ts +39 -4
  197. package/src/cohort-topic/topic-router.ts +12 -2
  198. package/src/dispute/client.ts +17 -7
  199. package/src/index.ts +64 -59
  200. package/src/libp2p-key-network.ts +1377 -1260
  201. package/src/libp2p-node-base.ts +186 -27
  202. package/src/libp2p-node-rn.ts +31 -30
  203. package/src/libp2p-node.ts +37 -36
  204. package/src/logger.ts +6 -1
  205. package/src/matchmaking/query-transport.ts +9 -9
  206. package/src/network/network-manager-service.ts +16 -5
  207. package/src/network/relay-reservation.ts +636 -0
  208. package/src/pend-validation.ts +3 -2
  209. package/src/protocol-client.ts +13 -1
  210. package/src/reactivity/forwarder-host.ts +2 -1
  211. package/src/reactivity/origination-manager.ts +3 -3
  212. package/src/reactivity/recover-transport.ts +39 -22
  213. package/src/reactivity/subscription-manager.ts +2 -2
  214. package/src/reactivity/topic-bytes.ts +35 -37
  215. package/src/repo/client.ts +172 -173
  216. package/src/repo/cluster-coordinator.ts +183 -25
  217. package/src/repo/coordinator-repo.ts +656 -506
  218. package/src/repo/i-under-replication-ledger.ts +89 -0
  219. package/src/repo/kv-under-replication-ledger.ts +302 -0
  220. package/src/repo/responsibility.ts +48 -0
  221. package/src/repo/service.ts +76 -39
  222. package/src/repo/stuck-reservation.ts +224 -0
  223. package/src/repo/under-replication-drain.ts +582 -0
  224. package/src/rn.ts +74 -69
  225. package/src/storage/block-latch.ts +7 -4
  226. package/src/storage/block-storage.ts +161 -10
  227. package/src/storage/i-block-storage.ts +60 -5
  228. package/src/storage/pending-claim.ts +200 -0
  229. package/src/storage/raw-store-codec.ts +26 -3
  230. package/src/storage/restoration-coordinator.ts +212 -211
  231. package/src/storage/ring-shift-coordinator.ts +271 -271
  232. package/src/storage/storage-repo.ts +280 -72
  233. package/src/storage/struct.ts +58 -0
  234. package/src/testing/mesh-harness.ts +293 -112
@@ -1,15 +1,14 @@
1
1
  import type { Startable, Libp2p } from '@libp2p/interface'
2
2
  import type { IPeerNetwork } from '@optimystic/db-core'
3
+ import { routingKeyForBlock } from '@optimystic/db-core'
3
4
  import { hashKey } from 'p2p-fret'
4
5
  import type { FretService } from 'p2p-fret'
5
- import { peerIdFromString } from '@libp2p/peer-id'
6
6
  import type { PartitionDetector } from './partition-detector.js'
7
7
  import type { ProofRetainingRepo } from '../storage/block-archive.js'
8
- import { BlockTransferClient, sourceBlockCertification } from './block-transfer-service.js'
8
+ import { pushBlockToPeers } from './block-transfer-service.js'
9
9
  import { createLogger } from '../logger.js'
10
10
 
11
11
  const log = createLogger('spread-on-churn')
12
- const textEncoder = new TextEncoder()
13
12
 
14
13
  // ── Types ────────────────────────────────────────────────────────────
15
14
 
@@ -208,7 +207,7 @@ export class SpreadOnChurnMonitor implements Startable {
208
207
  const spreadResults: SpreadEvent['spread'] = []
209
208
 
210
209
  for (const blockId of this.trackedBlocks) {
211
- const key = textEncoder.encode(blockId)
210
+ const key = routingKeyForBlock(blockId)
212
211
  const coord = await hashKey(key)
213
212
 
214
213
  // Check eligibility: only middle peers spread
@@ -224,18 +223,33 @@ export class SpreadOnChurnMonitor implements Startable {
224
223
  const targets = expanded.filter(id => !cohortSet.has(id) && id !== selfId)
225
224
  if (targets.length === 0) continue
226
225
 
227
- // Read block data from local storage
228
- const result = await this.deps.repo.get({ blockIds: [blockId] })
229
- const blockResult = result[blockId]
230
- if (blockResult?.unavailable !== undefined) {
226
+ // The shared read-certify-push loop, to every target. Both per-target deadlines are set:
227
+ // a target that connects but never replies would otherwise hang this sequential loop
228
+ // forever during churn (exactly when slow/dying peers are common). A receiver running
229
+ // the default `requirePushCertificate` rejects an uncertified push, so a block this node
230
+ // holds no proof for simply fails to spread (logged by the receiver as
231
+ // `push:reject-uncertified`) rather than being planted.
232
+ //
233
+ // NOTE: no memory of earlier spreads — every debounced departure re-pushes every eligible block
234
+ // to every expansion target, including targets a previous pass already confirmed. Targets are
235
+ // only peers OUTSIDE the cohort, so a deployment no larger than its cohort (a two-member pair)
236
+ // spreads nothing. If churn on a larger network shows the same (block, target) pushed pass
237
+ // after pass, remember confirmed targets per block the way RebalanceMonitor's `cohortPeers` does.
238
+ const outcome = await pushBlockToPeers(this.deps.repo, this.deps.peerNetwork, blockId, targets, {
239
+ reason: 'replication',
240
+ protocolPrefix: this.deps.protocolPrefix,
241
+ dialTimeoutMs: this.config.pushDialTimeoutMs,
242
+ responseTimeoutMs: this.config.pushResponseTimeoutMs,
243
+ })
244
+ if (outcome.status === 'unavailable') {
231
245
  // The repo could not work out whether it still holds this block (unmaterializable
232
246
  // history / failed restore). Untracking on that answer would silently drop the block
233
247
  // from the spread set on a guess, and only a later re-commit would put it back — so
234
248
  // keep it tracked and let the next sweep (or a heal) settle it.
235
- log('unavailable block=%s reason=%s (keeping tracked)', blockId, blockResult.unavailable)
249
+ log('unavailable block=%s reason=%s (keeping tracked)', blockId, outcome.reason)
236
250
  continue
237
251
  }
238
- if (!blockResult?.block) {
252
+ if (outcome.status === 'no-local-data') {
239
253
  // The block has left local storage. No deletion event exists today to evict it
240
254
  // from the tracked set, so prune here. Deleting the current element of a Set mid
241
255
  // for...of is safe - it does not disturb the rest of the iteration. A later
@@ -245,50 +259,16 @@ export class SpreadOnChurnMonitor implements Startable {
245
259
  continue
246
260
  }
247
261
 
248
- const blockData = textEncoder.encode(JSON.stringify(blockResult.block))
249
-
250
- // Carry the source's revision metadata so the replica's `latest` matches the
251
- // source instead of being fabricated as rev 1 on the receiver, plus the cohort commit
252
- // proof for that same revision — a receiver running the default `requirePushCertificate`
253
- // rejects an uncertified push, so a block this node holds no proof for simply fails to
254
- // spread (logged by the receiver as `push:reject-uncertified`) rather than being planted.
255
- const certification = await sourceBlockCertification(this.deps.repo, blockId, blockResult)
256
-
257
- // Push to each target
258
- const succeeded: string[] = []
259
- const failed: string[] = []
260
-
261
- for (const targetId of targets) {
262
- try {
263
- const peerId = peerIdFromString(targetId)
264
- const client = new BlockTransferClient(
265
- peerId,
266
- this.deps.peerNetwork,
267
- this.deps.protocolPrefix
268
- )
269
- // Bound both the dial and the response read. A target that connects but
270
- // never replies would otherwise hang this sequential loop forever during
271
- // churn (exactly when slow/dying peers are common); the deadline makes the
272
- // push throw, which the catch below records as `failed` so the pass advances.
273
- const response = await client.pushBlocks([blockId], [blockData], 'replication', certification, {
274
- dialTimeoutMs: this.config.pushDialTimeoutMs,
275
- responseTimeoutMs: this.config.pushResponseTimeoutMs,
276
- })
277
- // A round-trip that does not throw still does not mean the replica
278
- // landed: handlePush reports a block in `missing` when it could not
279
- // parse or persist it. Treat that as a failed push so the resilience
280
- // mechanism does not falsely count the new owner as holding the block.
281
- if (response.missing.includes(blockId)) {
282
- failed.push(targetId)
283
- log('push:rejected block=%s target=%s (receiver did not persist)', blockId, targetId)
284
- } else {
285
- succeeded.push(targetId)
286
- log('push:ok block=%s target=%s', blockId, targetId)
287
- }
288
- } catch (err) {
289
- failed.push(targetId)
290
- log('push:fail block=%s target=%s err=%s',
291
- blockId, targetId, (err as Error).message)
262
+ const succeeded = outcome.confirmed
263
+ const failed = outcome.refusals.map(refusal => refusal.peerId)
264
+ for (const targetId of succeeded) {
265
+ log('push:ok block=%s target=%s', blockId, targetId)
266
+ }
267
+ for (const refusal of outcome.refusals) {
268
+ if (refusal.reason === 'rejected') {
269
+ log('push:rejected block=%s target=%s (receiver did not persist)', blockId, refusal.peerId)
270
+ } else {
271
+ log('push:fail block=%s target=%s err=%s', blockId, refusal.peerId, refusal.error)
292
272
  }
293
273
  }
294
274
 
@@ -167,7 +167,7 @@ import { createPoWVerifier, createReputationVerifier, type BootstrapReputationVi
167
167
  import { createParentReferenceVerifier, createDefaultParentTopicView, type BootstrapParentTopicView } from "./bootstrap-parent-reference.js";
168
168
  import { createBootstrapEvidenceBuilder } from "./bootstrap-evidence-builder.js";
169
169
  import { DEFAULT_COHORT_TOPIC_PROTOCOLS, cohortTopicProtocolList, type CohortTopicProtocols } from "./protocols.js";
170
- import { requestResponse, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
170
+ import { requestResponse, requireReply, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
171
171
  import { createLogger } from "../logger.js";
172
172
 
173
173
  const log = createLogger("cohort-topic");
@@ -697,10 +697,11 @@ export async function createCohortTopicHost(node: Libp2p, fret: FretService, opt
697
697
  // The real k − x assembly lives in each CoordEngine's own threshold signer (constructed per coord).
698
698
  const verifyingSigner = createCohortSigner(createVerifyOnlyThresholdCrypto(), minSigs);
699
699
 
700
- // Collect one cohort member's `/sign` endorsement over the new fifth protocol.
700
+ // Collect one cohort member's `/sign` endorsement over the new fifth protocol. The `/sign` responder always
701
+ // writes a frame, so a no-result reply is a non-conforming peer: reject, and `collectFrom` counts no signature.
701
702
  const dialSign = async (peerIdStr: string, request: SignRequestV1): Promise<SignReplyV1> => {
702
703
  const reply = await requestResponse(node, peerIdFromString(peerIdStr), protocols.sign, encodeCohortMessage(request, maxBytes), maxBytes);
703
- return validateSignReplyV1(decodeCohortMessage(reply, maxBytes));
704
+ return validateSignReplyV1(decodeCohortMessage(requireReply(reply, "cohort sign"), maxBytes));
704
705
  };
705
706
 
706
707
  /** FRET assembly around `coord`: self prepended + deduped; epoch = H(sorted member join). */
@@ -3175,9 +3176,10 @@ async function registerCohortTopicProtocols(
3175
3176
  *
3176
3177
  * `handle` cannot return `undefined`, and that is the point. The dialer's `readFramed` treats bare
3177
3178
  * end-of-stream as a truncation error rather than an empty reply, so a reply-reading protocol
3178
- * (register, membership, sign) must always put a frame on the wire — an empty one for "no result".
3179
- * Making that a separate constructor from {@link makeOneWayHandler} puts the invariant in the type,
3180
- * where a later edit cannot silently opt out of it. (`stream-util.ts#handleRequestResponse` takes
3179
+ * (register, membership, sign) must always put a frame on the wire — an empty one for "no result",
3180
+ * which the dialer's `requestResponse` resolves as `undefined`. Making that a separate constructor
3181
+ * from {@link makeOneWayHandler} puts the invariant in the type, where a later edit cannot silently
3182
+ * opt out of it. (`stream-util.ts#handleRequestResponse` takes
3181
3183
  * the other route for the same reason: every consumer of it is a reply-reading protocol, so it maps
3182
3184
  * a handler's `undefined` onto an explicit zero-length frame itself.)
3183
3185
  */
@@ -50,7 +50,7 @@ export class FretMembershipSource implements IMembershipSource {
50
50
  for (const peerStr of this.resolver.cohortPeers(coord, this.wants)) {
51
51
  try {
52
52
  const reply = await requestResponse(this.node, peerIdFromString(peerStr), this.membershipProtocol, request, this.maxBytes);
53
- if (reply.length > 0) {
53
+ if (reply !== undefined) {
54
54
  this.cache(coord, reply);
55
55
  return reply;
56
56
  }
@@ -1,31 +1,34 @@
1
- /**
2
- * Cohort-topic peer-id ↔ bytes bridge.
3
- *
4
- * db-core's substrate logic references peers as opaque `Uint8Array` ids ({@link PeerRef}`.id`); FRET +
5
- * libp2p reference them as `PeerId` / peer-id strings. The cohort-member ids the substrate carries on
6
- * the wire (a reply's `primary` / `backups` / `cohortMembers`) must round-trip back to a *dialable*
7
- * libp2p peer, so this module pins one unambiguous encoding for them: the UTF-8 bytes of the canonical
8
- * peer-id string. (The participant's own addressing coordinate `P` is a separate ring-coord value — see
9
- * the host — so this encoding never needs to feed the ring hash.)
10
- */
11
-
12
- import { peerIdFromString } from "@libp2p/peer-id";
13
- import type { PeerId } from "@libp2p/interface";
14
-
15
- const utf8Encoder = new TextEncoder();
16
- const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
17
-
18
- /** A cohort-member id as the substrate's opaque bytes: UTF-8 of the canonical peer-id string. */
19
- export function peerIdToBytes(peerId: PeerId | string): Uint8Array {
20
- return utf8Encoder.encode(typeof peerId === "string" ? peerId : peerId.toString());
21
- }
22
-
23
- /** Reverse of {@link peerIdToBytes}: decode member bytes back to the canonical peer-id string. */
24
- export function bytesToPeerIdString(bytes: Uint8Array): string {
25
- return utf8Decoder.decode(bytes);
26
- }
27
-
28
- /** Reverse of {@link peerIdToBytes}: decode member bytes back to a dialable {@link PeerId}. */
29
- export function bytesToPeerId(bytes: Uint8Array): PeerId {
30
- return peerIdFromString(bytesToPeerIdString(bytes));
31
- }
1
+ /**
2
+ * Cohort-topic peer-id ↔ bytes bridge.
3
+ *
4
+ * db-core's substrate logic references peers as opaque `Uint8Array` ids ({@link PeerRef}`.id`); FRET +
5
+ * libp2p reference them as `PeerId` / peer-id strings. The cohort-member ids the substrate carries on
6
+ * the wire (a reply's `primary` / `backups` / `cohortMembers`) must round-trip back to a *dialable*
7
+ * libp2p peer, so this module pins one unambiguous encoding for them: the UTF-8 bytes of the canonical
8
+ * peer-id string. (The participant's own addressing coordinate `P` is a separate ring-coord value — see
9
+ * the host — so this encoding never needs to feed the ring hash.)
10
+ */
11
+
12
+ import { peerIdFromString } from "@libp2p/peer-id";
13
+ import type { PeerId } from "@libp2p/interface";
14
+
15
+ const utf8Encoder = new TextEncoder();
16
+ // Built on first use, not at module load: Hermes (React Native) has no native `TextDecoder`, so a
17
+ // module-scope construction fails the import itself whenever it runs ahead of the host's polyfill.
18
+ let utf8DecoderInstance: TextDecoder | undefined;
19
+ const utf8Decoder = (): TextDecoder => utf8DecoderInstance ??= new TextDecoder("utf-8", { fatal: true });
20
+
21
+ /** A cohort-member id as the substrate's opaque bytes: UTF-8 of the canonical peer-id string. */
22
+ export function peerIdToBytes(peerId: PeerId | string): Uint8Array {
23
+ return utf8Encoder.encode(typeof peerId === "string" ? peerId : peerId.toString());
24
+ }
25
+
26
+ /** Reverse of {@link peerIdToBytes}: decode member bytes back to the canonical peer-id string. */
27
+ export function bytesToPeerIdString(bytes: Uint8Array): string {
28
+ return utf8Decoder().decode(bytes);
29
+ }
30
+
31
+ /** Reverse of {@link peerIdToBytes}: decode member bytes back to a dialable {@link PeerId}. */
32
+ export function bytesToPeerId(bytes: Uint8Array): PeerId {
33
+ return peerIdFromString(bytesToPeerIdString(bytes));
34
+ }
@@ -34,6 +34,13 @@ export const DEFAULT_STREAM_MAX_BYTES = 512 * 1024;
34
34
  /**
35
35
  * Open `protocol` to `peer`, send `frame`, and read the bounded reply frame.
36
36
  *
37
+ * Resolves `undefined` when the peer replied with a **zero-length frame** — the in-band "I have no
38
+ * result" signal {@link handleRequestResponse} sends for a handler that returned nothing. A returned
39
+ * `Uint8Array` is therefore never empty, and every caller must decide what "no result" means for its
40
+ * protocol: fall through to another peer, map it to a benign default, or {@link requireReply} when the
41
+ * protocol always carries a reply. Genuine failures still reject — a dial failure, a stream the peer
42
+ * aborted (`FrameTruncationError`), an over-ceiling reply (`PayloadTooLargeError`).
43
+ *
37
44
  * NOTE: takes no `AbortSignal`, so a caller cannot set its own deadline. Bounded today anyway —
38
45
  * `readFramed` self-times-out at 5s and `dialProtocol` falls back to libp2p's default dial
39
46
  * timeout (~30s) — so an unresponsive peer is slow, not hung. If a caller ever needs a tighter
@@ -47,14 +54,18 @@ export async function requestResponse(
47
54
  protocol: string,
48
55
  frame: Uint8Array,
49
56
  maxBytes = DEFAULT_STREAM_MAX_BYTES,
50
- ): Promise<Uint8Array> {
57
+ ): Promise<Uint8Array | undefined> {
51
58
  let stream: Stream | undefined;
52
59
  try {
53
60
  // `negotiateFully` deliberately omitted — see the accepted tradeoff in the module docblock.
54
61
  stream = await openProtocolStream(node, peer, protocol);
55
62
  sendFramed(stream, frame);
56
63
  await stream.close();
57
- return await readFramed(stream, maxBytes);
64
+ const reply = await readFramed(stream, maxBytes);
65
+ // NOTE: every zero-length reply maps to "no result". Safe while no protocol has a meaningful empty reply
66
+ // (cohort frames carry an inner length prefix; query/recover replies are encoded objects). If one ever
67
+ // needs to answer "empty" as data, it must send a non-empty envelope rather than bare empty bytes.
68
+ return reply.length === 0 ? undefined : reply;
58
69
  } finally {
59
70
  if (stream != null) {
60
71
  try {
@@ -66,6 +77,25 @@ export async function requestResponse(
66
77
  }
67
78
  }
68
79
 
80
+ /** Thrown when a request/response peer replied with a zero-length frame where the protocol requires a reply. */
81
+ export class NoResultReplyError extends Error {
82
+ constructor(context: string) {
83
+ super(`${context}: peer replied with no result (zero-length reply frame)`);
84
+ this.name = "NoResultReplyError";
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Unwrap a {@link requestResponse} reply for a protocol whose responder always carries a frame; throw
90
+ * {@link NoResultReplyError} (naming `context`) when the peer had no result.
91
+ */
92
+ export function requireReply(reply: Uint8Array | undefined, context: string): Uint8Array {
93
+ if (reply === undefined) {
94
+ throw new NoResultReplyError(context);
95
+ }
96
+ return reply;
97
+ }
98
+
69
99
  /** Open `protocol` to `peer` and send `frame` without awaiting a reply (fire-and-forget gossip). */
70
100
  export async function sendOneWay(node: Libp2p, peer: PeerId, protocol: string, frame: Uint8Array): Promise<void> {
71
101
  let stream: Stream | undefined;
@@ -90,8 +120,13 @@ export async function sendOneWay(node: Libp2p, peer: PeerId, protocol: string, f
90
120
  *
91
121
  * A `handle` that returns `undefined` (a drop, a gate rejection, no serving engine) replies with an
92
122
  * explicit **zero-length frame** rather than silence: `readFramed` treats end-of-stream as a
93
- * truncation error, so "no reply" must travel in-band — the dialer's {@link requestResponse}
94
- * resolves it as empty bytes, exactly what the old read-to-EOF returned for a silent close.
123
+ * truncation error, so "no result" must travel in-band — the dialer's {@link requestResponse}
124
+ * resolves it as `undefined`. Returning `new Uint8Array(0)` puts the same zero-length frame on the
125
+ * wire, so it is the same signal (host.ts's membership responder says "no certificate" that way).
126
+ * A `handle` that throws aborts the stream instead, which the dialer sees as a rejection.
127
+ *
128
+ * Only the reply direction carries "no result": the request frame `handle` receives is always bytes,
129
+ * and an empty request arrives as a zero-length `Uint8Array`.
95
130
  */
96
131
  export function handleRequestResponse(
97
132
  node: Libp2p,
@@ -3,7 +3,7 @@ import { bytesToB64url, b64urlToBytes, encodeCohortMessage } from "@optimystic/d
3
3
  import type { Libp2p } from "libp2p";
4
4
  import type { FretService, RouteAndMaybeActV1, NearAnchorV1 } from "p2p-fret";
5
5
  import { bytesToPeerId } from "./peer-codec.js";
6
- import { requestResponse, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
6
+ import { requestResponse, requireReply, DEFAULT_STREAM_MAX_BYTES, type NoResultReplyError } from "./stream-util.js";
7
7
  import { PROTOCOL_COHORT_REGISTER } from "./protocols.js";
8
8
 
9
9
  /** A FRET `routeAct` result carrying a cohort reply. */
@@ -70,8 +70,18 @@ export class FretTopicRouter implements ITopicRouter {
70
70
  return encodeCohortMessage({ v: 1, result: "no_state" }, this.maxBytes);
71
71
  }
72
72
 
73
+ /**
74
+ * Dial `member`'s `/register` directly and return its encoded reply.
75
+ *
76
+ * The `/register` responder always writes a frame, so a no-result (zero-length) reply only comes from a
77
+ * non-conforming peer; it rejects with {@link NoResultReplyError}, reaching the caller exactly as a dial
78
+ * failure does (renewal counts a failed ping; the walk's direct dial propagates the rejection). db-core's {@link ITopicRouter.dialMember} port deliberately stays
79
+ * `Promise<Uint8Array>` rather than widening to `| undefined`: that would ripple through the walk and
80
+ * renewal decision logic for a state only a misbehaving peer can produce, so the decision is made here.
81
+ */
73
82
  async dialMember(member: PeerRef, activity: Uint8Array): Promise<Uint8Array> {
74
83
  const peer = bytesToPeerId(member.id);
75
- return requestResponse(this.node, peer, this.registerProtocol, activity, this.maxBytes);
84
+ const reply = await requestResponse(this.node, peer, this.registerProtocol, activity, this.maxBytes);
85
+ return requireReply(reply, "cohort-topic register dial");
76
86
  }
77
87
  }
@@ -29,13 +29,23 @@ export class DisputeClient extends ProtocolClient {
29
29
  // arbitrator" behavior. Also apply the default dial cap so a challenge to an
30
30
  // unreachable arbitrator fails the dial fast even when no `timeoutMs` is given;
31
31
  // this does not alter the response semantics (still bounded only by the signal).
32
- const signal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
33
- const response = await this.processMessage<{ type: 'vote'; vote: ArbitrationVote }>(
34
- message,
35
- this.protocol,
36
- { signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS, maxDataLength: MAX_CONTROL_MESSAGE_BYTES }
37
- );
38
- return response.vote;
32
+ //
33
+ // Explicit AbortController + timer rather than `AbortSignal.timeout`, which Hermes
34
+ // (React Native's JS engine) does not provide — same pattern as `dialRelay` in
35
+ // network/relay-reservation.ts. The timer is cleared on every exit path below so a
36
+ // finished call never holds a timer (and the node it references) alive.
37
+ const controller = timeoutMs ? new AbortController() : undefined;
38
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
39
+ try {
40
+ const response = await this.processMessage<{ type: 'vote'; vote: ArbitrationVote }>(
41
+ message,
42
+ this.protocol,
43
+ { signal: controller?.signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS, maxDataLength: MAX_CONTROL_MESSAGE_BYTES }
44
+ );
45
+ return response.vote;
46
+ } finally {
47
+ if (timer) clearTimeout(timer);
48
+ }
39
49
  }
40
50
 
41
51
  /** Send a resolution to a peer (broadcast) */
package/src/index.ts CHANGED
@@ -1,59 +1,64 @@
1
- export * from "./cluster/client.js";
2
- export * from "./cluster/client-signature-verifier.js";
3
- export * from "./cluster/cluster-repo.js";
4
- export * from "./cluster/cluster-policy.js";
5
- export * from "./cluster/commit-cert.js";
6
- export * from "./cluster/commit-proof.js";
7
- export * from "./cluster/service.js";
8
- export * from "./cluster/rebalance-monitor.js";
9
- export * from "./cluster/spread-on-churn.js";
10
- export * from "./cluster/block-transfer.js";
11
- export * from "./cluster/block-transfer-service.js";
12
- export * from "./inbound-authorization.js";
13
- export * from "./protocol-client.js";
14
- export * from "./repo/client.js";
15
- export * from "./repo/cluster-coordinator.js";
16
- export * from "./repo/coordinator-repo.js";
17
- export * from "./repo/served-repo-proxy.js";
18
- export * from "./repo/service.js";
19
- export * from "./storage/block-storage.js";
20
- export * from "./storage/block-latch.js";
21
- export * from "./storage/store-identity.js";
22
- export * from "./storage/raw-store-driver.js";
23
- export * from "./storage/kv-raw-storage.js";
24
- export * from "./storage/shared-cache-pool.js";
25
- export * from "./storage/cached-store-driver.js";
26
- export * from "./storage/cached-raw-storage.js";
27
- export * from "./storage/with-read-cache.js";
28
- export * from "./storage/memory-store-driver.js";
29
- export * from "./storage/memory-storage.js";
30
- export * from "./storage/i-block-storage.js";
31
- export * from "./storage/i-raw-storage.js";
32
- export * from "./storage/struct.js";
33
- export * from "./storage/block-archive.js";
34
- export * from "./storage/storage-repo.js";
35
- export * from "./storage/restoration-coordinator.js";
36
- export * from "./storage/ring-selector.js";
37
- export * from "./storage/storage-monitor.js";
38
- export * from "./storage/arachnode-fret-adapter.js";
39
- export * from "./sync/protocol.js";
40
- export * from "./sync/client.js";
41
- export * from "./sync/service.js";
42
- export * from "./it-utility.js";
43
- export * from "./libp2p-key-network.js";
44
- export * from "./libp2p-node.js";
45
- export * from "./optimystic-node.js";
46
- export * from "./routing/responsibility.js";
47
- export * from "./routing/libp2p-known-peers.js";
48
- export * from "./network/network-manager-service.js";
49
- export * from "./network/get-network-manager.js";
50
- export * from "./reputation/index.js";
51
- export * from "./dispute/index.js";
52
- export * from "./cohort-topic/index.js";
53
- export * from "./matchmaking/index.js";
54
- export * from "./reactivity/index.js";
55
- export * from "./cluster/i-transaction-state-store.js";
56
- export * from "./cluster/memory-transaction-state-store.js";
57
- export * from "./cluster/persistent-transaction-state-store.js";
58
- export * from "./storage/i-kv-store.js";
59
- export * from "./storage/memory-kv-store.js";
1
+ export * from "./cluster/client.js";
2
+ export * from "./cluster/client-signature-verifier.js";
3
+ export * from "./cluster/cluster-repo.js";
4
+ export * from "./cluster/cluster-policy.js";
5
+ export * from "./cluster/commit-cert.js";
6
+ export * from "./cluster/commit-proof.js";
7
+ export * from "./cluster/service.js";
8
+ export * from "./cluster/rebalance-monitor.js";
9
+ export * from "./cluster/spread-on-churn.js";
10
+ export * from "./cluster/block-transfer.js";
11
+ export * from "./cluster/block-transfer-service.js";
12
+ export * from "./inbound-authorization.js";
13
+ export * from "./protocol-client.js";
14
+ export * from "./repo/client.js";
15
+ export * from "./repo/cluster-coordinator.js";
16
+ export * from "./repo/coordinator-repo.js";
17
+ export * from "./repo/i-under-replication-ledger.js";
18
+ export * from "./repo/kv-under-replication-ledger.js";
19
+ export * from "./repo/under-replication-drain.js";
20
+ export * from "./repo/responsibility.js";
21
+ export * from "./repo/served-repo-proxy.js";
22
+ export * from "./repo/service.js";
23
+ export * from "./storage/block-storage.js";
24
+ export * from "./storage/block-latch.js";
25
+ export * from "./storage/store-identity.js";
26
+ export * from "./storage/raw-store-driver.js";
27
+ export * from "./storage/kv-raw-storage.js";
28
+ export * from "./storage/shared-cache-pool.js";
29
+ export * from "./storage/cached-store-driver.js";
30
+ export * from "./storage/cached-raw-storage.js";
31
+ export * from "./storage/with-read-cache.js";
32
+ export * from "./storage/memory-store-driver.js";
33
+ export * from "./storage/memory-storage.js";
34
+ export * from "./storage/i-block-storage.js";
35
+ export * from "./storage/i-raw-storage.js";
36
+ export * from "./storage/struct.js";
37
+ export * from "./storage/pending-claim.js";
38
+ export * from "./storage/block-archive.js";
39
+ export * from "./storage/storage-repo.js";
40
+ export * from "./storage/restoration-coordinator.js";
41
+ export * from "./storage/ring-selector.js";
42
+ export * from "./storage/storage-monitor.js";
43
+ export * from "./storage/arachnode-fret-adapter.js";
44
+ export * from "./sync/protocol.js";
45
+ export * from "./sync/client.js";
46
+ export * from "./sync/service.js";
47
+ export * from "./it-utility.js";
48
+ export * from "./libp2p-key-network.js";
49
+ export * from "./libp2p-node.js";
50
+ export * from "./optimystic-node.js";
51
+ export * from "./routing/responsibility.js";
52
+ export * from "./routing/libp2p-known-peers.js";
53
+ export * from "./network/network-manager-service.js";
54
+ export * from "./network/get-network-manager.js";
55
+ export * from "./reputation/index.js";
56
+ export * from "./dispute/index.js";
57
+ export * from "./cohort-topic/index.js";
58
+ export * from "./matchmaking/index.js";
59
+ export * from "./reactivity/index.js";
60
+ export * from "./cluster/i-transaction-state-store.js";
61
+ export * from "./cluster/memory-transaction-state-store.js";
62
+ export * from "./cluster/persistent-transaction-state-store.js";
63
+ export * from "./storage/i-kv-store.js";
64
+ export * from "./storage/memory-kv-store.js";