@optimystic/db-p2p 0.13.5 → 0.14.1

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 (342) hide show
  1. package/README.md +6 -6
  2. package/dist/src/cluster/block-transfer-service.d.ts +67 -8
  3. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  4. package/dist/src/cluster/block-transfer-service.js +101 -62
  5. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  6. package/dist/src/cluster/client.d.ts +18 -1
  7. package/dist/src/cluster/client.d.ts.map +1 -1
  8. package/dist/src/cluster/client.js +55 -39
  9. package/dist/src/cluster/client.js.map +1 -1
  10. package/dist/src/cluster/cluster-error.d.ts +38 -0
  11. package/dist/src/cluster/cluster-error.d.ts.map +1 -0
  12. package/dist/src/cluster/cluster-error.js +51 -0
  13. package/dist/src/cluster/cluster-error.js.map +1 -0
  14. package/dist/src/cluster/cluster-repo.d.ts +135 -3
  15. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  16. package/dist/src/cluster/cluster-repo.js +333 -58
  17. package/dist/src/cluster/cluster-repo.js.map +1 -1
  18. package/dist/src/cluster/commit-cert.d.ts +63 -0
  19. package/dist/src/cluster/commit-cert.d.ts.map +1 -0
  20. package/dist/src/cluster/commit-cert.js +108 -0
  21. package/dist/src/cluster/commit-cert.js.map +1 -0
  22. package/dist/src/cluster/service.d.ts +46 -11
  23. package/dist/src/cluster/service.d.ts.map +1 -1
  24. package/dist/src/cluster/service.js +97 -33
  25. package/dist/src/cluster/service.js.map +1 -1
  26. package/dist/src/cluster/spread-on-churn.d.ts +13 -0
  27. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  28. package/dist/src/cluster/spread-on-churn.js +34 -4
  29. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  30. package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts +59 -0
  31. package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts.map +1 -0
  32. package/dist/src/cohort-topic/bootstrap-evidence-builder.js +79 -0
  33. package/dist/src/cohort-topic/bootstrap-evidence-builder.js.map +1 -0
  34. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts +81 -0
  35. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts.map +1 -0
  36. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js +93 -0
  37. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js.map +1 -0
  38. package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts +108 -0
  39. package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts.map +1 -0
  40. package/dist/src/cohort-topic/bootstrap-parent-reference.js +108 -0
  41. package/dist/src/cohort-topic/bootstrap-parent-reference.js.map +1 -0
  42. package/dist/src/cohort-topic/change-bridge.d.ts +65 -0
  43. package/dist/src/cohort-topic/change-bridge.d.ts.map +1 -0
  44. package/dist/src/cohort-topic/change-bridge.js +76 -0
  45. package/dist/src/cohort-topic/change-bridge.js.map +1 -0
  46. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +74 -0
  47. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -0
  48. package/dist/src/cohort-topic/cohort-gossip-driver.js +87 -0
  49. package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -0
  50. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +42 -0
  51. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -0
  52. package/dist/src/cohort-topic/cohort-gossip-transport.js +57 -0
  53. package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -0
  54. package/dist/src/cohort-topic/fret-trust-anchor.d.ts +100 -0
  55. package/dist/src/cohort-topic/fret-trust-anchor.d.ts.map +1 -0
  56. package/dist/src/cohort-topic/fret-trust-anchor.js +115 -0
  57. package/dist/src/cohort-topic/fret-trust-anchor.js.map +1 -0
  58. package/dist/src/cohort-topic/host.d.ts +573 -0
  59. package/dist/src/cohort-topic/host.d.ts.map +1 -0
  60. package/dist/src/cohort-topic/host.js +1460 -0
  61. package/dist/src/cohort-topic/host.js.map +1 -0
  62. package/dist/src/cohort-topic/index.d.ts +14 -0
  63. package/dist/src/cohort-topic/index.d.ts.map +1 -0
  64. package/dist/src/cohort-topic/index.js +14 -0
  65. package/dist/src/cohort-topic/index.js.map +1 -0
  66. package/dist/src/cohort-topic/membership-publish-sink.d.ts +14 -0
  67. package/dist/src/cohort-topic/membership-publish-sink.d.ts.map +1 -0
  68. package/dist/src/cohort-topic/membership-publish-sink.js +17 -0
  69. package/dist/src/cohort-topic/membership-publish-sink.js.map +1 -0
  70. package/dist/src/cohort-topic/membership-source.d.ts +36 -0
  71. package/dist/src/cohort-topic/membership-source.d.ts.map +1 -0
  72. package/dist/src/cohort-topic/membership-source.js +58 -0
  73. package/dist/src/cohort-topic/membership-source.js.map +1 -0
  74. package/dist/src/cohort-topic/peer-codec.d.ts +18 -0
  75. package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -0
  76. package/dist/src/cohort-topic/peer-codec.js +26 -0
  77. package/dist/src/cohort-topic/peer-codec.js.map +1 -0
  78. package/dist/src/cohort-topic/peer-sig.d.ts +52 -0
  79. package/dist/src/cohort-topic/peer-sig.d.ts.map +1 -0
  80. package/dist/src/cohort-topic/peer-sig.js +83 -0
  81. package/dist/src/cohort-topic/peer-sig.js.map +1 -0
  82. package/dist/src/cohort-topic/protocols.d.ts +49 -0
  83. package/dist/src/cohort-topic/protocols.d.ts.map +1 -0
  84. package/dist/src/cohort-topic/protocols.js +58 -0
  85. package/dist/src/cohort-topic/protocols.js.map +1 -0
  86. package/dist/src/cohort-topic/reactivity-membership-gate.d.ts +49 -0
  87. package/dist/src/cohort-topic/reactivity-membership-gate.d.ts.map +1 -0
  88. package/dist/src/cohort-topic/reactivity-membership-gate.js +42 -0
  89. package/dist/src/cohort-topic/reactivity-membership-gate.js.map +1 -0
  90. package/dist/src/cohort-topic/size-estimator.d.ts +16 -0
  91. package/dist/src/cohort-topic/size-estimator.d.ts.map +1 -0
  92. package/dist/src/cohort-topic/size-estimator.js +16 -0
  93. package/dist/src/cohort-topic/size-estimator.js.map +1 -0
  94. package/dist/src/cohort-topic/stream-util.d.ts +19 -0
  95. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -0
  96. package/dist/src/cohort-topic/stream-util.js +76 -0
  97. package/dist/src/cohort-topic/stream-util.js.map +1 -0
  98. package/dist/src/cohort-topic/threshold-crypto.d.ts +94 -0
  99. package/dist/src/cohort-topic/threshold-crypto.d.ts.map +1 -0
  100. package/dist/src/cohort-topic/threshold-crypto.js +178 -0
  101. package/dist/src/cohort-topic/threshold-crypto.js.map +1 -0
  102. package/dist/src/cohort-topic/topic-router.d.ts +41 -0
  103. package/dist/src/cohort-topic/topic-router.d.ts.map +1 -0
  104. package/dist/src/cohort-topic/topic-router.js +63 -0
  105. package/dist/src/cohort-topic/topic-router.js.map +1 -0
  106. package/dist/src/dispute/cascade.d.ts +179 -0
  107. package/dist/src/dispute/cascade.d.ts.map +1 -0
  108. package/dist/src/dispute/cascade.js +315 -0
  109. package/dist/src/dispute/cascade.js.map +1 -0
  110. package/dist/src/dispute/client.d.ts +2 -1
  111. package/dist/src/dispute/client.d.ts.map +1 -1
  112. package/dist/src/dispute/client.js +12 -3
  113. package/dist/src/dispute/client.js.map +1 -1
  114. package/dist/src/dispute/dispute-service.d.ts +41 -1
  115. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  116. package/dist/src/dispute/dispute-service.js +147 -21
  117. package/dist/src/dispute/dispute-service.js.map +1 -1
  118. package/dist/src/dispute/index.d.ts +2 -0
  119. package/dist/src/dispute/index.d.ts.map +1 -1
  120. package/dist/src/dispute/index.js +2 -0
  121. package/dist/src/dispute/index.js.map +1 -1
  122. package/dist/src/dispute/invalidation.d.ts +266 -0
  123. package/dist/src/dispute/invalidation.d.ts.map +1 -0
  124. package/dist/src/dispute/invalidation.js +405 -0
  125. package/dist/src/dispute/invalidation.js.map +1 -0
  126. package/dist/src/dispute/types.d.ts +14 -0
  127. package/dist/src/dispute/types.d.ts.map +1 -1
  128. package/dist/src/dispute/types.js.map +1 -1
  129. package/dist/src/index.d.ts +4 -0
  130. package/dist/src/index.d.ts.map +1 -1
  131. package/dist/src/index.js +4 -0
  132. package/dist/src/index.js.map +1 -1
  133. package/dist/src/libp2p-key-network.d.ts +7 -0
  134. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  135. package/dist/src/libp2p-key-network.js +49 -5
  136. package/dist/src/libp2p-key-network.js.map +1 -1
  137. package/dist/src/libp2p-node-base.d.ts +45 -3
  138. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  139. package/dist/src/libp2p-node-base.js +522 -5
  140. package/dist/src/libp2p-node-base.js.map +1 -1
  141. package/dist/src/matchmaking/aggregate-counts.d.ts +67 -0
  142. package/dist/src/matchmaking/aggregate-counts.d.ts.map +1 -0
  143. package/dist/src/matchmaking/aggregate-counts.js +60 -0
  144. package/dist/src/matchmaking/aggregate-counts.js.map +1 -0
  145. package/dist/src/matchmaking/index.d.ts +20 -0
  146. package/dist/src/matchmaking/index.d.ts.map +1 -0
  147. package/dist/src/matchmaking/index.js +20 -0
  148. package/dist/src/matchmaking/index.js.map +1 -0
  149. package/dist/src/matchmaking/module.d.ts +181 -0
  150. package/dist/src/matchmaking/module.d.ts.map +1 -0
  151. package/dist/src/matchmaking/module.js +221 -0
  152. package/dist/src/matchmaking/module.js.map +1 -0
  153. package/dist/src/matchmaking/protocols.d.ts +37 -0
  154. package/dist/src/matchmaking/protocols.d.ts.map +1 -0
  155. package/dist/src/matchmaking/protocols.js +42 -0
  156. package/dist/src/matchmaking/protocols.js.map +1 -0
  157. package/dist/src/matchmaking/provider-manager.d.ts +52 -0
  158. package/dist/src/matchmaking/provider-manager.d.ts.map +1 -0
  159. package/dist/src/matchmaking/provider-manager.js +74 -0
  160. package/dist/src/matchmaking/provider-manager.js.map +1 -0
  161. package/dist/src/matchmaking/query-handler.d.ts +34 -0
  162. package/dist/src/matchmaking/query-handler.d.ts.map +1 -0
  163. package/dist/src/matchmaking/query-handler.js +62 -0
  164. package/dist/src/matchmaking/query-handler.js.map +1 -0
  165. package/dist/src/matchmaking/query-transport.d.ts +180 -0
  166. package/dist/src/matchmaking/query-transport.d.ts.map +1 -0
  167. package/dist/src/matchmaking/query-transport.js +315 -0
  168. package/dist/src/matchmaking/query-transport.js.map +1 -0
  169. package/dist/src/matchmaking/seeker-manager.d.ts +39 -0
  170. package/dist/src/matchmaking/seeker-manager.d.ts.map +1 -0
  171. package/dist/src/matchmaking/seeker-manager.js +49 -0
  172. package/dist/src/matchmaking/seeker-manager.js.map +1 -0
  173. package/dist/src/matchmaking/seeker-walk-client.d.ts +127 -0
  174. package/dist/src/matchmaking/seeker-walk-client.d.ts.map +1 -0
  175. package/dist/src/matchmaking/seeker-walk-client.js +189 -0
  176. package/dist/src/matchmaking/seeker-walk-client.js.map +1 -0
  177. package/dist/src/matchmaking/traffic-validation.d.ts +128 -0
  178. package/dist/src/matchmaking/traffic-validation.d.ts.map +1 -0
  179. package/dist/src/matchmaking/traffic-validation.js +105 -0
  180. package/dist/src/matchmaking/traffic-validation.js.map +1 -0
  181. package/dist/src/network/network-manager-service.d.ts +4 -2
  182. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  183. package/dist/src/network/network-manager-service.js +9 -3
  184. package/dist/src/network/network-manager-service.js.map +1 -1
  185. package/dist/src/protocol-client.d.ts +13 -0
  186. package/dist/src/protocol-client.d.ts.map +1 -1
  187. package/dist/src/protocol-client.js +74 -2
  188. package/dist/src/protocol-client.js.map +1 -1
  189. package/dist/src/reactivity/forwarder-host.d.ts +198 -0
  190. package/dist/src/reactivity/forwarder-host.d.ts.map +1 -0
  191. package/dist/src/reactivity/forwarder-host.js +355 -0
  192. package/dist/src/reactivity/forwarder-host.js.map +1 -0
  193. package/dist/src/reactivity/index.d.ts +19 -0
  194. package/dist/src/reactivity/index.d.ts.map +1 -0
  195. package/dist/src/reactivity/index.js +19 -0
  196. package/dist/src/reactivity/index.js.map +1 -0
  197. package/dist/src/reactivity/notify-transport.d.ts +70 -0
  198. package/dist/src/reactivity/notify-transport.d.ts.map +1 -0
  199. package/dist/src/reactivity/notify-transport.js +111 -0
  200. package/dist/src/reactivity/notify-transport.js.map +1 -0
  201. package/dist/src/reactivity/origination-manager.d.ts +110 -0
  202. package/dist/src/reactivity/origination-manager.d.ts.map +1 -0
  203. package/dist/src/reactivity/origination-manager.js +129 -0
  204. package/dist/src/reactivity/origination-manager.js.map +1 -0
  205. package/dist/src/reactivity/protocols.d.ts +43 -0
  206. package/dist/src/reactivity/protocols.d.ts.map +1 -0
  207. package/dist/src/reactivity/protocols.js +50 -0
  208. package/dist/src/reactivity/protocols.js.map +1 -0
  209. package/dist/src/reactivity/push-state-gossip.d.ts +132 -0
  210. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -0
  211. package/dist/src/reactivity/push-state-gossip.js +231 -0
  212. package/dist/src/reactivity/push-state-gossip.js.map +1 -0
  213. package/dist/src/reactivity/recover-transport.d.ts +167 -0
  214. package/dist/src/reactivity/recover-transport.d.ts.map +1 -0
  215. package/dist/src/reactivity/recover-transport.js +308 -0
  216. package/dist/src/reactivity/recover-transport.js.map +1 -0
  217. package/dist/src/reactivity/rotation-rereg-scheduler.d.ts +142 -0
  218. package/dist/src/reactivity/rotation-rereg-scheduler.d.ts.map +1 -0
  219. package/dist/src/reactivity/rotation-rereg-scheduler.js +223 -0
  220. package/dist/src/reactivity/rotation-rereg-scheduler.js.map +1 -0
  221. package/dist/src/reactivity/subscriber-registry.d.ts +47 -0
  222. package/dist/src/reactivity/subscriber-registry.d.ts.map +1 -0
  223. package/dist/src/reactivity/subscriber-registry.js +87 -0
  224. package/dist/src/reactivity/subscriber-registry.js.map +1 -0
  225. package/dist/src/reactivity/subscription-manager.d.ts +221 -0
  226. package/dist/src/reactivity/subscription-manager.d.ts.map +1 -0
  227. package/dist/src/reactivity/subscription-manager.js +302 -0
  228. package/dist/src/reactivity/subscription-manager.js.map +1 -0
  229. package/dist/src/reactivity/topic-bytes.d.ts +32 -0
  230. package/dist/src/reactivity/topic-bytes.d.ts.map +1 -0
  231. package/dist/src/reactivity/topic-bytes.js +34 -0
  232. package/dist/src/reactivity/topic-bytes.js.map +1 -0
  233. package/dist/src/repo/client.d.ts.map +1 -1
  234. package/dist/src/repo/client.js +51 -23
  235. package/dist/src/repo/client.js.map +1 -1
  236. package/dist/src/repo/service.d.ts +46 -1
  237. package/dist/src/repo/service.d.ts.map +1 -1
  238. package/dist/src/repo/service.js +79 -45
  239. package/dist/src/repo/service.js.map +1 -1
  240. package/dist/src/rpc-deadline.d.ts +37 -0
  241. package/dist/src/rpc-deadline.d.ts.map +1 -0
  242. package/dist/src/rpc-deadline.js +31 -0
  243. package/dist/src/rpc-deadline.js.map +1 -0
  244. package/dist/src/storage/block-storage.d.ts +2 -0
  245. package/dist/src/storage/block-storage.d.ts.map +1 -1
  246. package/dist/src/storage/block-storage.js +95 -2
  247. package/dist/src/storage/block-storage.js.map +1 -1
  248. package/dist/src/storage/i-block-storage.d.ts +25 -0
  249. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  250. package/dist/src/storage/storage-repo.d.ts +53 -2
  251. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  252. package/dist/src/storage/storage-repo.js +193 -9
  253. package/dist/src/storage/storage-repo.js.map +1 -1
  254. package/dist/src/sync/client.d.ts +4 -1
  255. package/dist/src/sync/client.d.ts.map +1 -1
  256. package/dist/src/sync/client.js +5 -2
  257. package/dist/src/sync/client.js.map +1 -1
  258. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
  259. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
  260. package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
  261. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
  262. package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
  263. package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
  264. package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
  265. package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
  266. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  267. package/dist/src/testing/mesh-harness.js +23 -1
  268. package/dist/src/testing/mesh-harness.js.map +1 -1
  269. package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
  270. package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
  271. package/dist/src/testing/reactivity-mesh-harness.js +663 -0
  272. package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
  273. package/package.json +7 -4
  274. package/src/cluster/block-transfer-service.ts +133 -68
  275. package/src/cluster/client.ts +64 -45
  276. package/src/cluster/cluster-error.ts +64 -0
  277. package/src/cluster/cluster-repo.ts +405 -55
  278. package/src/cluster/commit-cert.ts +139 -0
  279. package/src/cluster/service.ts +116 -44
  280. package/src/cluster/spread-on-churn.ts +328 -285
  281. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
  282. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
  283. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
  284. package/src/cohort-topic/change-bridge.ts +109 -0
  285. package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
  286. package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
  287. package/src/cohort-topic/fret-trust-anchor.ts +153 -0
  288. package/src/cohort-topic/host.ts +2175 -0
  289. package/src/cohort-topic/index.ts +13 -0
  290. package/src/cohort-topic/membership-publish-sink.ts +20 -0
  291. package/src/cohort-topic/membership-source.ts +68 -0
  292. package/src/cohort-topic/peer-codec.ts +31 -0
  293. package/src/cohort-topic/peer-sig.ts +86 -0
  294. package/src/cohort-topic/protocols.ts +71 -0
  295. package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
  296. package/src/cohort-topic/size-estimator.ts +16 -0
  297. package/src/cohort-topic/stream-util.ts +87 -0
  298. package/src/cohort-topic/threshold-crypto.ts +239 -0
  299. package/src/cohort-topic/topic-router.ts +77 -0
  300. package/src/dispute/cascade.ts +517 -0
  301. package/src/dispute/client.ts +12 -2
  302. package/src/dispute/dispute-service.ts +171 -24
  303. package/src/dispute/index.ts +42 -0
  304. package/src/dispute/invalidation.ts +587 -0
  305. package/src/dispute/types.ts +14 -0
  306. package/src/index.ts +4 -0
  307. package/src/libp2p-key-network.ts +49 -6
  308. package/src/libp2p-node-base.ts +620 -12
  309. package/src/matchmaking/aggregate-counts.ts +104 -0
  310. package/src/matchmaking/index.ts +20 -0
  311. package/src/matchmaking/module.ts +363 -0
  312. package/src/matchmaking/protocols.ts +51 -0
  313. package/src/matchmaking/provider-manager.ts +95 -0
  314. package/src/matchmaking/query-handler.ts +88 -0
  315. package/src/matchmaking/query-transport.ts +483 -0
  316. package/src/matchmaking/seeker-manager.ts +64 -0
  317. package/src/matchmaking/seeker-walk-client.ts +293 -0
  318. package/src/matchmaking/traffic-validation.ts +195 -0
  319. package/src/network/network-manager-service.ts +9 -2
  320. package/src/protocol-client.ts +64 -3
  321. package/src/reactivity/forwarder-host.ts +438 -0
  322. package/src/reactivity/index.ts +19 -0
  323. package/src/reactivity/notify-transport.ts +144 -0
  324. package/src/reactivity/origination-manager.ts +192 -0
  325. package/src/reactivity/protocols.ts +61 -0
  326. package/src/reactivity/push-state-gossip.ts +291 -0
  327. package/src/reactivity/recover-transport.ts +408 -0
  328. package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
  329. package/src/reactivity/subscriber-registry.ts +96 -0
  330. package/src/reactivity/subscription-manager.ts +450 -0
  331. package/src/reactivity/topic-bytes.ts +37 -0
  332. package/src/repo/client.ts +54 -23
  333. package/src/repo/service.ts +90 -42
  334. package/src/rpc-deadline.ts +45 -0
  335. package/src/storage/block-storage.ts +106 -3
  336. package/src/storage/i-block-storage.ts +27 -0
  337. package/src/storage/storage-repo.ts +209 -13
  338. package/src/sync/client.ts +5 -2
  339. package/src/testing/cohort-topic-mesh-harness.ts +613 -0
  340. package/src/testing/matchmaking-mesh-harness.ts +475 -0
  341. package/src/testing/mesh-harness.ts +22 -2
  342. package/src/testing/reactivity-mesh-harness.ts +922 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Matchmaking — seeker walk client (db-p2p, drives the hang-out-vs-continue walk over the substrate).
3
+ *
4
+ * `docs/matchmaking.md` §Hang-out vs. continue. This is the RPC orchestration on top of the pure
5
+ * db-core {@link decide} engine: starting at `d_max`, it registers the seeker, issues `QueryV1`,
6
+ * re-validates and dedupes returned providers, and on each `Accepted` reply asks {@link decide} whether
7
+ * to finish, hang out (renew + requery on the `requery_interval_ms` poll cadence), or escalate (withdraw
8
+ * + re-register one tier toward the root). At `d = 0` there is nowhere left to walk, so it hangs out for
9
+ * the remaining patience and returns whatever matched (possibly `< wantCount`).
10
+ *
11
+ * Patience is a single budget that **drains across hops** (`docs/matchmaking.md` §Patience budgeting):
12
+ * a wall-clock deadline is fixed at the start, so every walked tier and every hang-out poll consumes
13
+ * the same `patienceMs` — escalation makes the next tier's hang-out progressively less attractive, which
14
+ * is correct since the root is terminal.
15
+ *
16
+ * The transport, clock, sleep, and per-entry verifier are all injected, so the walk unit-tests without a
17
+ * live libp2p stack (mock-tier e2e is a documented follow-on). This client implements the **poll path**
18
+ * only; the arrival-push path (`pushOnArrival`) is a separate slice.
19
+ *
20
+ * Edge cases encoded here (`docs/matchmaking.md` §Edge cases — the rest live in {@link decide}):
21
+ * - **`topicTraffic` absent (case 1):** the cohort is treated as zero-rate; the seeker issues one query
22
+ * (immediate-match) and walks one tier toward the root **without** hanging out.
23
+ * - **Stale `arrivalsPerMin = 0` after an epoch change (case 2):** never withdraw on a single zero
24
+ * reading — the immediate `QueryV1` runs first; a cohort that actually holds `>= wantCount` providers
25
+ * resolves to `done`, and only a query that also yields below threshold escalates.
26
+ * - **`UnwillingCohort`/`UnwillingMember` (case 3):** the hang-out decision is never entered (no
27
+ * `Accepted`); the walk terminates with whatever matched and standard substrate back-off applies.
28
+ */
29
+
30
+ import {
31
+ decide,
32
+ filterAcceptRatio,
33
+ matchesFilter,
34
+ newFilterAcceptRatioState,
35
+ observeYield,
36
+ verifyProviderEntry,
37
+ DEFAULT_HANG_OUT_CONFIG,
38
+ DEFAULT_MEAN_WANT_COUNT,
39
+ FILTER_ACCEPT_RATIO_INITIAL,
40
+ type CapabilityFilter,
41
+ type EntrySigVerifier,
42
+ type FilterAcceptRatioState,
43
+ type HangOutConfig,
44
+ type ProviderEntryV1,
45
+ type QueryReplyV1,
46
+ } from "@optimystic/db-core";
47
+
48
+ /** A seeker register/probe reply at a tree tier (the matchmaking-relevant subset of `RegisterReplyV1`). */
49
+ export interface SeekerProbeReply {
50
+ readonly result: "accepted" | "no_state" | "promoted" | "unwilling_member" | "unwilling_cohort";
51
+ /** Present on `accepted` and `promoted` (the substrate's barometer); absent triggers edge case 1. */
52
+ readonly topicTraffic?: QueryReplyV1["topicTraffic"];
53
+ /** Present on `promoted` — the tier to descend to. */
54
+ readonly targetTier?: number;
55
+ }
56
+
57
+ /** The substrate seam the walk drives: register/query/renew/withdraw against a tree tier. */
58
+ export interface SeekerWalkTransport {
59
+ /** Register (or re-register) the seeker at tree tier `treeTier`; resolves the probe reply. */
60
+ register(treeTier: number): Promise<SeekerProbeReply>;
61
+ /** Issue a `QueryV1` against the cohort the seeker is currently registered with. */
62
+ query(treeTier: number): Promise<QueryReplyV1>;
63
+ /** Renew the live seeker registration (hang-out keep-alive via TTL renewal). */
64
+ renew(): Promise<void>;
65
+ /** Withdraw the seeker registration before escalating (polite `RenewV1` TTL = 0; optional). */
66
+ withdraw(): Promise<void>;
67
+ }
68
+
69
+ /** Construction inputs for {@link SeekerWalkClient}. */
70
+ export interface SeekerWalkClientDeps {
71
+ readonly transport: SeekerWalkTransport;
72
+ /** The matchmaking topic id (used to re-validate each forwarded entry's `registrationSig`). */
73
+ readonly topicId: Uint8Array;
74
+ /** Providers the seeker needs. */
75
+ readonly wantCount: number;
76
+ /** The starting tree tier `d_max`. */
77
+ readonly dMax: number;
78
+ /** Total patience budget (ms); drains across hops + hang-out. */
79
+ readonly patienceMs: number;
80
+ /** Optional capability filter (re-applied seeker-side over the returned set). */
81
+ readonly filter?: CapabilityFilter;
82
+ /** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
83
+ readonly verifyEntry: EntrySigVerifier;
84
+ /** Hang-out decision config. Default {@link DEFAULT_HANG_OUT_CONFIG}. */
85
+ readonly config?: HangOutConfig;
86
+ /** Assumed competing-seeker mean `wantCount`. Default {@link DEFAULT_MEAN_WANT_COUNT}. */
87
+ readonly meanWantCount?: number;
88
+ /** Starting `filterAcceptRatio` (refined per walk). Default {@link FILTER_ACCEPT_RATIO_INITIAL}. */
89
+ readonly filterAcceptRatioInitial?: number;
90
+ /** Wall clock (unix ms); injectable for tests. Default `Date.now`. */
91
+ readonly clock?: () => number;
92
+ /** Sleep for the requery cadence; injectable for tests. Default a real timer. */
93
+ readonly sleep?: (ms: number) => Promise<void>;
94
+ }
95
+
96
+ /** The result of a completed walk. */
97
+ export interface SeekerWalkResult {
98
+ /** Matched, re-validated, deduped providers (up to whatever accumulated; may be `< wantCount`). */
99
+ readonly providers: ProviderEntryV1[];
100
+ /** Whether `wantCount` was met. */
101
+ readonly metWantCount: boolean;
102
+ /** The tier the walk terminated at. */
103
+ readonly terminalTier: number;
104
+ /** Total register hops issued (probes + escalations + descends). */
105
+ readonly hops: number;
106
+ /**
107
+ * Max `topicTraffic.childCohortCount` observed across every `Accepted` reply this walk saw. `> 0`
108
+ * means the topic has promoted (it is hot), so the single-cohort sample is unrepresentative — the
109
+ * public seeker session / voting `QuorumDiscovery` binding uses this to decide whether to escalate to
110
+ * the multi-cohort sweep (`docs/matchmaking.md` §Multi-cohort sweep).
111
+ */
112
+ readonly maxChildCohortCount: number;
113
+ }
114
+
115
+ /** Outcome of evaluating one `Accepted` tier. */
116
+ type AcceptedOutcome = "done" | "escalate" | "terminal";
117
+
118
+ /** Drives the seeker hang-out-vs-continue walk for one matchmaking topic. See the module header. */
119
+ export class SeekerWalkClient {
120
+ private readonly transport: SeekerWalkTransport;
121
+ private readonly topicId: Uint8Array;
122
+ private readonly wantCount: number;
123
+ private readonly dMax: number;
124
+ private readonly patienceMs: number;
125
+ private readonly filter?: CapabilityFilter;
126
+ private readonly verifyEntry: EntrySigVerifier;
127
+ private readonly config: HangOutConfig;
128
+ private readonly meanWantCount: number;
129
+ private readonly filterAcceptRatioInitial: number;
130
+ private readonly clock: () => number;
131
+ private readonly sleep: (ms: number) => Promise<void>;
132
+
133
+ /** Matched providers, deduped by `participantId` (a provider seen via two queries counts once). */
134
+ private readonly matched = new Map<string, ProviderEntryV1>();
135
+ private ratioState: FilterAcceptRatioState = newFilterAcceptRatioState();
136
+ private deadline = 0;
137
+ private hops = 0;
138
+ /** Max `childCohortCount` seen across `Accepted` replies — the hot-topic / sweep-escalation signal. */
139
+ private maxChildCohortCount = 0;
140
+
141
+ constructor(deps: SeekerWalkClientDeps) {
142
+ this.transport = deps.transport;
143
+ this.topicId = deps.topicId;
144
+ this.wantCount = deps.wantCount;
145
+ this.dMax = deps.dMax;
146
+ this.patienceMs = deps.patienceMs;
147
+ this.filter = deps.filter;
148
+ this.verifyEntry = deps.verifyEntry;
149
+ this.config = deps.config ?? DEFAULT_HANG_OUT_CONFIG;
150
+ this.meanWantCount = deps.meanWantCount ?? DEFAULT_MEAN_WANT_COUNT;
151
+ this.filterAcceptRatioInitial = deps.filterAcceptRatioInitial ?? FILTER_ACCEPT_RATIO_INITIAL;
152
+ this.clock = deps.clock ?? ((): number => Date.now());
153
+ this.sleep = deps.sleep ?? ((ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)));
154
+ }
155
+
156
+ /** Run the walk from `d_max` toward the root; resolves the matched providers + termination info. */
157
+ async run(): Promise<SeekerWalkResult> {
158
+ this.deadline = this.clock() + this.patienceMs;
159
+ let d = this.dMax;
160
+
161
+ while (true) {
162
+ this.hops++;
163
+ const reply = await this.transport.register(d);
164
+ switch (reply.result) {
165
+ case "no_state": {
166
+ if (d <= 0) {
167
+ return this.finish(d);
168
+ }
169
+ d -= 1;
170
+ continue;
171
+ }
172
+ case "unwilling_member":
173
+ case "unwilling_cohort": {
174
+ // Substrate-level refusal — never received Accepted, so hang-out is not entered (case 3).
175
+ return this.finish(d);
176
+ }
177
+ case "promoted": {
178
+ const target = reply.targetTier ?? d + 1;
179
+ if (target <= d) {
180
+ return this.finish(d);
181
+ }
182
+ d = target;
183
+ continue;
184
+ }
185
+ case "accepted": {
186
+ const outcome = await this.handleAccepted(d, reply.topicTraffic);
187
+ if (outcome === "done" || outcome === "terminal") {
188
+ return this.finish(d);
189
+ }
190
+ await this.transport.withdraw();
191
+ d -= 1;
192
+ continue;
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ /** Remaining patience (ms); the wall-clock deadline drains uniformly across hops + hang-out. */
199
+ private remaining(): number {
200
+ return Math.max(0, this.deadline - this.clock());
201
+ }
202
+
203
+ /** Filter, re-validate (`registrationSig`), and dedupe a query reply's providers into {@link matched}. */
204
+ private collect(reply: QueryReplyV1): void {
205
+ const returned = reply.providers ?? [];
206
+ let matchedThisQuery = 0;
207
+ for (const entry of returned) {
208
+ if (!matchesFilter(entry, this.filter)) {
209
+ continue;
210
+ }
211
+ if (!verifyProviderEntry(this.topicId, entry, this.verifyEntry)) {
212
+ continue;
213
+ }
214
+ matchedThisQuery++;
215
+ this.matched.set(entry.participantId, entry);
216
+ }
217
+ this.ratioState = observeYield(this.ratioState, matchedThisQuery, returned.length);
218
+ }
219
+
220
+ /** Evaluate one `Accepted` tier: immediate query, then {@link decide} → done / hangOut / escalate. */
221
+ private async handleAccepted(d: number, traffic: SeekerProbeReply["topicTraffic"]): Promise<AcceptedOutcome> {
222
+ // Record the hottest tier seen as soon as this Accepted reply's traffic is available — *before* the
223
+ // immediate-match/done short-circuit below, so the single-cohort-vs-sweep decision (public session /
224
+ // voting QuorumDiscovery binding) still sees a hot topic even when one cohort's query already met
225
+ // wantCount. Folding it only on the `decide` path would drop the signal for a small quorum that a
226
+ // single hot cohort satisfies, leaving the assembler with a prefix-biased sample it should sweep.
227
+ if (traffic !== undefined) {
228
+ this.maxChildCohortCount = Math.max(this.maxChildCohortCount, traffic.childCohortCount);
229
+ }
230
+
231
+ // Immediate-match check runs in every case — also satisfies edge case 2 (a stale arrivalsPerMin=0
232
+ // cohort that actually holds enough providers resolves here rather than escalating spuriously).
233
+ this.collect(await this.transport.query(d));
234
+ if (this.matched.size >= this.wantCount) {
235
+ return "done";
236
+ }
237
+
238
+ // Edge case 1: a reply without topicTraffic is treated as zero-rate — walk one tier toward the
239
+ // root without hanging out (no estimation against absent inputs).
240
+ if (traffic === undefined) {
241
+ return d <= 0 ? "terminal" : "escalate";
242
+ }
243
+
244
+ const decision = decide(
245
+ {
246
+ currentMatches: this.matched.size,
247
+ directParticipants: traffic.directParticipants,
248
+ arrivalsPerMin: traffic.arrivalsPerMin,
249
+ queriesPerMin: traffic.queriesPerMin,
250
+ childCohortCount: traffic.childCohortCount,
251
+ wantCount: this.wantCount,
252
+ patienceMsRemaining: this.remaining(),
253
+ filterAcceptRatio: filterAcceptRatio(this.ratioState, this.filterAcceptRatioInitial),
254
+ meanWantCount: this.meanWantCount,
255
+ },
256
+ this.config,
257
+ );
258
+
259
+ if (decision.action === "hangOut") {
260
+ await this.hangOut(d, decision.requeryIntervalMs);
261
+ if (this.matched.size >= this.wantCount) {
262
+ return "done";
263
+ }
264
+ return d <= 0 ? "terminal" : "escalate";
265
+ }
266
+
267
+ // escalate — but at the root there is nowhere to walk: hang out the remaining patience, then end.
268
+ if (d <= 0) {
269
+ await this.hangOut(d, this.config.requeryIntervalMs);
270
+ return "terminal";
271
+ }
272
+ return "escalate";
273
+ }
274
+
275
+ /** Keep the registration alive and re-query on the poll cadence until `wantCount` met or patience drains. */
276
+ private async hangOut(d: number, requeryIntervalMs: number): Promise<void> {
277
+ while (this.remaining() > 0 && this.matched.size < this.wantCount) {
278
+ await this.sleep(Math.min(requeryIntervalMs, this.remaining()));
279
+ await this.transport.renew();
280
+ this.collect(await this.transport.query(d));
281
+ }
282
+ }
283
+
284
+ private finish(terminalTier: number): SeekerWalkResult {
285
+ return {
286
+ providers: [...this.matched.values()],
287
+ metWantCount: this.matched.size >= this.wantCount,
288
+ terminalTier,
289
+ hops: this.hops,
290
+ maxChildCohortCount: this.maxChildCohortCount,
291
+ };
292
+ }
293
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Matchmaking — seeker-side adversarial traffic-reporting bounds + reputation cross-check hooks (db-p2p).
3
+ *
4
+ * `docs/matchmaking.md` §Adversarial cohort traffic reporting. A `QueryReplyV1.topicTraffic` (and the
5
+ * registration reply's traffic) is signed by the cohort **primary's single member key, not a threshold
6
+ * signature**, because the response is advisory — so a malicious primary can over- or under-report. The
7
+ * doc proves the harm is bounded either way; this module makes those bounds explicit for the seeker walk
8
+ * and emits the cross-check signals the reputation subsystem consumes.
9
+ *
10
+ * - **Over-reporting** (fake *hot* tier ⇒ seeker hangs out): bounded by the seeker's `patienceMs`. The
11
+ * worst case is wasted patience plus the one `register → walk` hop after timeout — there is **no
12
+ * spatial flood**, because the decision rule only ever walks *toward the root*, never speculatively
13
+ * outward. {@link boundReportedTraffic} therefore caps any hang-out to the seeker's remaining
14
+ * wall-clock patience ({@link TrafficBoundResult.capPatienceMs}).
15
+ * - **Under-reporting** (fake *cold* tier ⇒ seeker escalates): bounded to **one extra hop per affected
16
+ * tier**, terminating at the root where aggregated truth is hardest to fake
17
+ * ({@link TrafficBoundResult.escalateAfterTiers}).
18
+ * - **Cross-check via cohort gossip → reputation.** Other cohort members can detect a primary whose
19
+ * reported rate diverges from the gossip-derived view. Detection *routing* is the reputation
20
+ * subsystem's job (out of scope to implement); this module provides the **emission points**
21
+ * ({@link TrafficCrossCheckSignal}) and a thin bridge ({@link reportTrafficCrossCheck}) into the
22
+ * existing {@link IPeerReputation}. It scores nothing itself — the reputation subsystem owns the
23
+ * aggregation/decay policy that turns a stream of advisory signals into an actual penalty.
24
+ *
25
+ * **GROUNDING (matchmaking.md §Adversarial traffic reporting):** no threshold signature is added per
26
+ * `QueryReplyV1` — the single-member signature stands; the bounded worst-case here does not justify the
27
+ * per-reply threshold cost. This module enforces the bounds purely seeker-side instead.
28
+ */
29
+
30
+ import { type QueryReplyV1 } from "@optimystic/db-core";
31
+ import { PenaltyReason, type IPeerReputation } from "../reputation/index.js";
32
+
33
+ /**
34
+ * The seeker's running walk state at the moment a reply lands — enough to bound the reply's reported
35
+ * traffic and attribute a cross-check signal. The seeker walk client (or the public seeker session)
36
+ * supplies it per reply.
37
+ */
38
+ export interface SeekerWalkState {
39
+ /** Tree tier `d` the reply came from. */
40
+ readonly currentTier: number;
41
+ /** Starting tier `d_max` (the walk only descends from here toward `0`). */
42
+ readonly dMax: number;
43
+ /** Register hops issued so far (probes + escalations). Bounded by `d_max + 1` — the walk never loops. */
44
+ readonly tiersWalked: number;
45
+ /** Total patience budget for the task (ms). */
46
+ readonly patienceMs: number;
47
+ /** Patience left on the wall-clock deadline (ms) — the hang-out cap. */
48
+ readonly patienceRemainingMs: number;
49
+ /** Filter-matched providers the seeker's *own* immediate `QueryV1` at this tier actually returned. */
50
+ readonly observedMatches: number;
51
+ /** The reply's cohort primary peer id, for reputation attribution (absent → no attribution). */
52
+ readonly primaryId?: string;
53
+ }
54
+
55
+ /** Tunables for {@link boundReportedTraffic}. */
56
+ export interface TrafficBoundConfig {
57
+ /**
58
+ * Upper bound on extra register hops a single under-reported tier may cost. The doc fixes this at
59
+ * **1** ("one extra hop per affected tier"); exposed for tests / future policy, not meant to change.
60
+ */
61
+ readonly maxExtraHopsPerTier: number;
62
+ /**
63
+ * Over-report plausibility ratio. When a reply claims `directParticipants` more than this multiple of
64
+ * the seeker's *own* query yield, the over-report is flagged *suspect* (a cross-check signal — never a
65
+ * hard reject; the patience cap already bounds the harm).
66
+ */
67
+ readonly overReportSuspectRatio: number;
68
+ }
69
+
70
+ /** The documented defaults (`maxExtraHopsPerTier = 1`; suspect ratio chosen to tolerate normal churn). */
71
+ export const DEFAULT_TRAFFIC_BOUND_CONFIG: TrafficBoundConfig = {
72
+ maxExtraHopsPerTier: 1,
73
+ overReportSuspectRatio: 8,
74
+ };
75
+
76
+ /** The kind of divergence a {@link TrafficCrossCheckSignal} reports. */
77
+ export type TrafficCrossCheckKind = "over-report-suspected" | "under-report-suspected";
78
+
79
+ /**
80
+ * One advisory cross-check observation about a cohort primary's reported traffic. Emitted by
81
+ * {@link boundReportedTraffic}; consumed by the reputation subsystem (via {@link reportTrafficCrossCheck}
82
+ * or a custom sink). Carries the raw discrepancy so the reputation policy — not this module — decides
83
+ * whether it warrants a penalty.
84
+ */
85
+ export interface TrafficCrossCheckSignal {
86
+ readonly kind: TrafficCrossCheckKind;
87
+ /** The cohort primary peer id (the subject), if the walk state attributed one. */
88
+ readonly subjectId?: string;
89
+ /** The tier the reply came from. */
90
+ readonly tier: number;
91
+ /** `topicTraffic.directParticipants` as reported. */
92
+ readonly reportedDirectParticipants: number;
93
+ /** `topicTraffic.arrivalsPerMin` as reported. */
94
+ readonly reportedArrivalsPerMin: number;
95
+ /** The seeker's own immediate-query yield, which the report is cross-checked against. */
96
+ readonly observedMatches: number;
97
+ }
98
+
99
+ /** The outcome of {@link boundReportedTraffic}. */
100
+ export interface TrafficBoundResult {
101
+ /** Whether the reported traffic is within plausible bounds (advisory; a flag, not an admission). */
102
+ readonly trusted: boolean;
103
+ /**
104
+ * Over-report bound: never hang out at this tier beyond the seeker's remaining wall-clock patience.
105
+ * Equals `max(0, patienceRemainingMs)` — so a fabricated hot tier wastes at most that, then the walk
106
+ * proceeds with its one register hop.
107
+ */
108
+ readonly capPatienceMs: number;
109
+ /** Under-report bound: at most this many extra register hops attributable to one tier (== config). */
110
+ readonly escalateAfterTiers: number;
111
+ /** Cross-check emission points (possibly empty). Forward to the reputation subsystem if desired. */
112
+ readonly reputationSignals: TrafficCrossCheckSignal[];
113
+ }
114
+
115
+ /**
116
+ * Bound the harm of a (possibly adversarial) `topicTraffic` report and emit cross-check signals. Pure —
117
+ * no I/O, no clock, no reputation scoring. The seeker walk consults `capPatienceMs` before hanging out
118
+ * and treats `escalateAfterTiers` as the per-tier hop ceiling; the bounds it returns hold *by
119
+ * construction* of the toward-root-only walk, so this function documents + asserts them rather than
120
+ * changing the walk topology.
121
+ */
122
+ export function boundReportedTraffic(
123
+ reply: QueryReplyV1,
124
+ walkState: SeekerWalkState,
125
+ cfg: TrafficBoundConfig = DEFAULT_TRAFFIC_BOUND_CONFIG,
126
+ ): TrafficBoundResult {
127
+ const traffic = reply.topicTraffic;
128
+ const signals: TrafficCrossCheckSignal[] = [];
129
+
130
+ // Over-report: a primary advertising a hot tier (many directParticipants) whose own query yields far
131
+ // fewer matches is suspect. Harm is bounded regardless — the patience cap below means a fabricated hot
132
+ // tier can only waste the seeker's remaining patience before it walks on (no spatial flood, since the
133
+ // walk only steps toward the root).
134
+ const overReportSuspect =
135
+ traffic.directParticipants > cfg.overReportSuspectRatio * Math.max(walkState.observedMatches, 1);
136
+ if (overReportSuspect) {
137
+ signals.push(crossCheckSignal("over-report-suspected", traffic, walkState));
138
+ }
139
+
140
+ // Under-report: a primary claiming a cold tier (zero arrivals) while the seeker's own query yields
141
+ // matches is suspect. Either way the escalation costs at most one extra hop for this tier and
142
+ // terminates at the root.
143
+ const underReportSuspect = traffic.arrivalsPerMin === 0 && walkState.observedMatches > 0;
144
+ if (underReportSuspect) {
145
+ signals.push(crossCheckSignal("under-report-suspected", traffic, walkState));
146
+ }
147
+
148
+ return {
149
+ trusted: !overReportSuspect && !underReportSuspect,
150
+ capPatienceMs: Math.max(0, walkState.patienceRemainingMs),
151
+ escalateAfterTiers: cfg.maxExtraHopsPerTier,
152
+ reputationSignals: signals,
153
+ };
154
+ }
155
+
156
+ function crossCheckSignal(kind: TrafficCrossCheckKind, traffic: QueryReplyV1["topicTraffic"], walkState: SeekerWalkState): TrafficCrossCheckSignal {
157
+ const signal: TrafficCrossCheckSignal = {
158
+ kind,
159
+ tier: walkState.currentTier,
160
+ reportedDirectParticipants: traffic.directParticipants,
161
+ reportedArrivalsPerMin: traffic.arrivalsPerMin,
162
+ observedMatches: walkState.observedMatches,
163
+ };
164
+ if (walkState.primaryId !== undefined) {
165
+ (signal as { subjectId: string }).subjectId = walkState.primaryId;
166
+ }
167
+ return signal;
168
+ }
169
+
170
+ /**
171
+ * The total hop budget the toward-root-only walk can consume under any sequence of (honest or
172
+ * adversarial) traffic reports: at most `maxExtraHopsPerTier` per tier from `d_max` down to the root,
173
+ * plus the terminal hop — i.e. `(dMax + 1) * maxExtraHopsPerTier`. Exposed so the walk and its tests can
174
+ * assert the under-report bound holds (the walk never exceeds it, because escalation is monotone toward
175
+ * `d = 0`).
176
+ */
177
+ export function maxWalkHops(dMax: number, cfg: TrafficBoundConfig = DEFAULT_TRAFFIC_BOUND_CONFIG): number {
178
+ return Math.max(0, dMax + 1) * cfg.maxExtraHopsPerTier;
179
+ }
180
+
181
+ /**
182
+ * Integration hook: forward cross-check signals into the reputation subsystem. This is the **emission
183
+ * point only** — it records a `ProtocolViolation` against the reporting primary; the reputation
184
+ * subsystem's own weighting, decay, and deprioritize/ban thresholds (`PeerReputationService`) decide
185
+ * whether an accumulation of these advisory signals actually penalizes the peer. Signals without an
186
+ * attributed `subjectId` are skipped.
187
+ */
188
+ export function reportTrafficCrossCheck(reputation: IPeerReputation, signals: readonly TrafficCrossCheckSignal[]): void {
189
+ for (const signal of signals) {
190
+ if (signal.subjectId === undefined) {
191
+ continue;
192
+ }
193
+ reputation.reportPeer(signal.subjectId, PenaltyReason.ProtocolViolation, `matchmaking:${signal.kind}`);
194
+ }
195
+ }
@@ -92,13 +92,16 @@ export class NetworkManagerService implements Startable {
92
92
 
93
93
  /**
94
94
  * Initialize the spread-on-churn monitor. Call after libp2p, FRET are available.
95
- * Caller provides repo and peerNetwork (not held by NetworkManagerService directly).
95
+ * Caller provides repo and peerNetwork (not held by NetworkManagerService directly),
96
+ * plus the node's protocolPrefix (/optimystic/<networkName>) so churn pushes dial the
97
+ * same block-transfer protocol the node registers its handler under.
96
98
  */
97
99
  initSpreadOnChurnMonitor(
98
100
  partitionDetector: PartitionDetector,
99
101
  repo: SpreadOnChurnDeps['repo'],
100
102
  peerNetwork: SpreadOnChurnDeps['peerNetwork'],
101
103
  clusterSize: number,
104
+ protocolPrefix: string,
102
105
  config?: Partial<SpreadOnChurnConfig>
103
106
  ): SpreadOnChurnMonitor {
104
107
  const libp2p = this.getLibp2p()
@@ -106,8 +109,12 @@ export class NetworkManagerService implements Startable {
106
109
  if (!libp2p || !fret) {
107
110
  throw new Error('Cannot init SpreadOnChurnMonitor: libp2p or FRET not available')
108
111
  }
112
+ // protocolPrefix MUST flow into SpreadOnChurnDeps: performSpread builds its
113
+ // BlockTransferClient with it, and the receiving node registers the block-transfer
114
+ // handler under /optimystic/<networkName>. A missing/empty prefix dials the wrong
115
+ // protocol and every churn push fails to connect.
109
116
  this.spreadOnChurnMonitor = new SpreadOnChurnMonitor(
110
- { libp2p, fret, partitionDetector, repo, peerNetwork, clusterSize },
117
+ { libp2p, fret, partitionDetector, repo, peerNetwork, clusterSize, protocolPrefix },
111
118
  config
112
119
  )
113
120
  return this.spreadOnChurnMonitor
@@ -23,6 +23,23 @@ export class DialTimeoutError extends Error {
23
23
  }
24
24
  }
25
25
 
26
+ /**
27
+ * Thrown when a peer dialed successfully but the response-read deadline expired
28
+ * before it wrote a reply (it connected, then went silent). Distinct from
29
+ * {@link DialTimeoutError} (never connected) and from a parent cancellation
30
+ * (`options.signal`), so callers/diagnostics can tell "peer went quiet" apart
31
+ * from "peer was unreachable" and "we cancelled". `.code === RESPONSE_TIMEOUT_ERROR_CODE`.
32
+ */
33
+ export const RESPONSE_TIMEOUT_ERROR_CODE = 'RESPONSE_TIMEOUT';
34
+
35
+ export class ResponseTimeoutError extends Error {
36
+ readonly code = RESPONSE_TIMEOUT_ERROR_CODE;
37
+ constructor(peer: string, protocol: string, ms: number) {
38
+ super(`response timeout: peer=${peer} protocol=${protocol} after ${ms}ms`);
39
+ this.name = 'ResponseTimeoutError';
40
+ }
41
+ }
42
+
26
43
  /** Base class for clients that communicate via a libp2p protocol */
27
44
  export class ProtocolClient {
28
45
  constructor(
@@ -33,7 +50,7 @@ export class ProtocolClient {
33
50
  protected async processMessage<T>(
34
51
  message: unknown,
35
52
  protocol: string,
36
- options?: { signal?: AbortSignal; correlationId?: string; dialTimeoutMs?: number }
53
+ options?: { signal?: AbortSignal; correlationId?: string; dialTimeoutMs?: number; responseTimeoutMs?: number }
37
54
  ): Promise<T> {
38
55
  const peer = this.peerId.toString();
39
56
  const cid = options?.correlationId;
@@ -90,7 +107,34 @@ export class ProtocolClient {
90
107
  }
91
108
  log('dial:ok peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
92
109
 
110
+ // Per-peer response deadline. The dial controller is already cleared once a
111
+ // stream is established, so a peer that connects but then never writes a frame
112
+ // (and never closes the stream) would hang the `first(...)` read below forever.
113
+ // Setting a timer alone is not enough — the underlying `for await` over the
114
+ // libp2p stream keeps awaiting regardless. The decisive action is to actively
115
+ // `stream.abort(...)`, which rejects the stream's async iterator and unblocks
116
+ // the read. A parent `options.signal` abort is forwarded the same way so a
117
+ // cancelled caller tears the stream down rather than leaking it. When neither
118
+ // `responseTimeoutMs` nor `signal` is supplied, no cap is imposed — preserving
119
+ // every existing caller (mirrors how omitting `dialTimeoutMs` imposes no dial cap).
120
+ const responseTimeoutMs = options?.responseTimeoutMs;
121
+ let responseTimer: ReturnType<typeof setTimeout> | undefined;
122
+ let responseTimeoutError: ResponseTimeoutError | undefined;
123
+ const onParentAbortResponse = () => {
124
+ try { stream.abort(options?.signal?.reason ?? new Error('aborted')); } catch { /* already torn down */ }
125
+ };
93
126
  try {
127
+ if (responseTimeoutMs && responseTimeoutMs > 0) {
128
+ responseTimer = setTimeout(() => {
129
+ responseTimeoutError = new ResponseTimeoutError(peer, protocol, responseTimeoutMs);
130
+ try { stream.abort(responseTimeoutError); } catch { /* already torn down */ }
131
+ }, responseTimeoutMs);
132
+ }
133
+ if (options?.signal) {
134
+ if (options.signal.aborted) onParentAbortResponse();
135
+ else options.signal.addEventListener('abort', onParentAbortResponse, { once: true });
136
+ }
137
+
94
138
  // Send the request using length-prefixed encoding
95
139
  const encoded = pipe(
96
140
  [new TextEncoder().encode(JSON.stringify(message))],
@@ -118,11 +162,28 @@ export class ProtocolClient {
118
162
  }
119
163
  ) as AsyncIterable<T>;
120
164
 
121
- const result = await first(() => source, () => { throw new Error('No response received') });
165
+ let result: T;
166
+ try {
167
+ result = await first(() => source, () => { throw new Error('No response received') });
168
+ } catch (err) {
169
+ // Aborting the stream (our timer or a parent abort) surfaces here as an
170
+ // iterator error. Translate it so callers see why the read ended.
171
+ if (responseTimeoutError) {
172
+ log('response:timeout peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
173
+ throw responseTimeoutError;
174
+ }
175
+ if (options?.signal?.aborted) {
176
+ throw options.signal.reason;
177
+ }
178
+ throw err;
179
+ }
122
180
  log('response peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
123
181
  return result;
124
182
  } finally {
125
- await stream.close();
183
+ if (responseTimer) clearTimeout(responseTimer);
184
+ if (options?.signal) options.signal.removeEventListener('abort', onParentAbortResponse);
185
+ // Closing an already-aborted stream must be safe.
186
+ try { await stream.close(); } catch { /* already torn down */ }
126
187
  }
127
188
  }
128
189
  }