@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,613 @@
1
+ /**
2
+ * Cohort-topic **mock-transport mesh harness** — the in-process, many-logical-node test substrate for
3
+ * the cohort-topic layer (`docs/cohort-topic.md`). It stands up an `N`-node mesh of real-Ed25519-keyed
4
+ * {@link CohortTopicHost}s over a mock libp2p transport and a shared FRET facade that routes the five
5
+ * cohort-topic protocols (`register`, `cohort-gossip`, `promote`, `membership`, `sign`) plus FRET's
6
+ * `routeAct` / `assembleCohort` directly between the in-process node engines — no real sockets, so a
7
+ * 50–200-node mesh runs fast and deterministically.
8
+ *
9
+ * This is the extracted, generalized form of the harness first written inline in
10
+ * `test/cohort-topic/live-tier.spec.ts`; that milestone spec and the at-scale suites
11
+ * (`cohort-topic-scale-*.spec.ts`) both drive it. It is a *sibling* of the cluster
12
+ * {@link import("./mesh-harness.js")} (which builds `ClusterMember` / coordinator-repo nodes over a
13
+ * different mock transport) — the two share no infrastructure, so they stay separate modules under
14
+ * `src/testing/` rather than one file.
15
+ *
16
+ * **What it is NOT.** It is not a wall-clock simulator. TTL eviction, renewal touches, gossip rounds,
17
+ * and demotion hysteresis are all driven by an *explicit* `now` passed to the engine methods
18
+ * (`handleRegister(reg, ctx, now)`, `gossipRound(now)`, `sweepStale(now)`, `demotionTick(now)`), so a
19
+ * suite advances virtual time by choosing timestamps — never by sleeping. The only real-time waits are
20
+ * the tiny async-settle polls ({@link waitFor}) the in-process gossip handlers need to drain.
21
+ */
22
+
23
+ import { generateKeyPair } from '@libp2p/crypto/keys';
24
+ import { peerIdFromPrivateKey } from '@libp2p/peer-id';
25
+ import type { PrivateKey, PeerId } from '@libp2p/interface';
26
+ import { hashPeerId, type RouteAndMaybeActV1, type NearAnchorV1 } from 'p2p-fret';
27
+ import {
28
+ RingHash,
29
+ createSlotAssigner,
30
+ createTierAddressing,
31
+ coreProfile,
32
+ edgeProfile,
33
+ bytesEqual,
34
+ bytesToB64url,
35
+ b64urlToBytes,
36
+ encodeCohortMessage,
37
+ decodeCohortMessage,
38
+ decodeRegisterReplyV1,
39
+ cohortGossipSigningPayload,
40
+ registerSigningPayload,
41
+ renewSigningPayload,
42
+ type CohortGossipV1,
43
+ type NodeProfile,
44
+ type PromotionConfig,
45
+ type RegisterResult,
46
+ type RegisterV1,
47
+ type RenewV1,
48
+ type RingCoord,
49
+ type Tier,
50
+ type WalkTrace,
51
+ } from '@optimystic/db-core';
52
+ import { createCohortTopicHost, type CohortTopicAntiDosOptions, type CohortTopicHost, type CoordEngine } from '../cohort-topic/host.js';
53
+ import { peerIdToBytes, bytesToPeerIdString } from '../cohort-topic/peer-codec.js';
54
+ import { signPeer } from '../cohort-topic/peer-sig.js';
55
+ import { DEFAULT_COHORT_TOPIC_PROTOCOLS as PROTOCOLS } from '../cohort-topic/protocols.js';
56
+
57
+ export { PROTOCOLS };
58
+
59
+ export const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
60
+
61
+ /** Poll `predicate` until true or `timeoutMs` elapses (deterministic-but-not-flaky async settle). */
62
+ export async function waitFor(predicate: () => boolean, timeoutMs = 2_000, intervalMs = 5): Promise<boolean> {
63
+ const deadline = Date.now() + timeoutMs;
64
+ while (Date.now() < deadline) {
65
+ if (predicate()) {
66
+ return true;
67
+ }
68
+ await delay(intervalMs);
69
+ }
70
+ return predicate();
71
+ }
72
+
73
+ // --- members (real Ed25519 keys) ---
74
+
75
+ /** A real cohort node identity: its libp2p key, peer-id, peer-id string, dialable member bytes, ring position. */
76
+ export interface Member {
77
+ readonly key: PrivateKey;
78
+ readonly peerId: PeerId;
79
+ readonly idStr: string;
80
+ /** Dialable member id (UTF-8 of the peer-id string) — the `participantCoord` / signer wire form. */
81
+ readonly bytes: Uint8Array;
82
+ /** Ring position `H(peerId)` — what FRET routes / assembles around. */
83
+ readonly ringPos: RingCoord;
84
+ }
85
+
86
+ export async function makeMember(): Promise<Member> {
87
+ const key = await generateKeyPair('Ed25519');
88
+ const peerId = peerIdFromPrivateKey(key);
89
+ return { key, peerId, idStr: peerId.toString(), bytes: peerIdToBytes(peerId), ringPos: await hashPeerId(peerId) };
90
+ }
91
+
92
+ export async function makeMembers(n: number): Promise<Member[]> {
93
+ const out: Member[] = [];
94
+ for (let i = 0; i < n; i++) {
95
+ out.push(await makeMember());
96
+ }
97
+ return out;
98
+ }
99
+
100
+ // --- in-process duplex stream (what readAllBounded iterates) ---
101
+
102
+ /**
103
+ * One end of an in-memory duplex pipe. `send` enqueues a frame onto the *peer's* inbox; `close`
104
+ * (half-close-write) signals EOF to the peer's reader. The async iterator yields this end's inbox until
105
+ * the peer closed its write and the buffer drains — so `p2p-fret`'s `readAllBounded` reads exactly the
106
+ * frames the other end wrote, then completes promptly on EOF (no idle-timeout wait).
107
+ */
108
+ export class MockStreamEnd {
109
+ private readonly inbox: Uint8Array[] = [];
110
+ private inboundClosed = false;
111
+ private waiter: (() => void) | undefined;
112
+ public peer!: MockStreamEnd;
113
+
114
+ send(frame: Uint8Array): void {
115
+ this.peer.accept(frame);
116
+ }
117
+
118
+ close(): Promise<void> {
119
+ this.peer.endInbound();
120
+ return Promise.resolve();
121
+ }
122
+
123
+ abort(_err?: unknown): void {
124
+ this.peer.endInbound();
125
+ }
126
+
127
+ private accept(frame: Uint8Array): void {
128
+ this.inbox.push(frame);
129
+ this.wake();
130
+ }
131
+
132
+ private endInbound(): void {
133
+ this.inboundClosed = true;
134
+ this.wake();
135
+ }
136
+
137
+ private wake(): void {
138
+ const w = this.waiter;
139
+ this.waiter = undefined;
140
+ w?.();
141
+ }
142
+
143
+ async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {
144
+ for (;;) {
145
+ if (this.inbox.length > 0) {
146
+ yield this.inbox.shift()!;
147
+ continue;
148
+ }
149
+ if (this.inboundClosed) {
150
+ return;
151
+ }
152
+ await new Promise<void>((resolve) => {
153
+ this.waiter = resolve;
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ export function streamPair(): [MockStreamEnd, MockStreamEnd] {
160
+ const a = new MockStreamEnd();
161
+ const b = new MockStreamEnd();
162
+ a.peer = b;
163
+ b.peer = a;
164
+ return [a, b];
165
+ }
166
+
167
+ // --- in-process libp2p stand-in ---
168
+
169
+ type ProtocolHandler = (stream: MockStreamEnd, connection: { remotePeer: PeerId }) => void;
170
+ const asList = (p: string | string[]): string[] => (Array.isArray(p) ? p : [p]);
171
+
172
+ /**
173
+ * A minimal libp2p node the cohort-topic host runs on. `dialProtocol` resolves the target node in the
174
+ * shared registry, hands its protocol handler one end of a fresh duplex, and returns the other — so a
175
+ * `requestResponse` / `sendOneWay` from one host drives the target host's real protocol handler. A node
176
+ * in the shared `down` set rejects inbound dials (crash / unreachable simulation).
177
+ */
178
+ export class MockNode {
179
+ public readonly handlers = new Map<string, ProtocolHandler>();
180
+
181
+ constructor(
182
+ public readonly peerId: PeerId,
183
+ private readonly registry: Map<string, MockNode>,
184
+ private readonly down: Set<string>,
185
+ ) {}
186
+
187
+ handle(protocol: string | string[], handler: ProtocolHandler): Promise<void> {
188
+ for (const p of asList(protocol)) {
189
+ this.handlers.set(p, handler);
190
+ }
191
+ return Promise.resolve();
192
+ }
193
+
194
+ unhandle(protocol: string | string[]): Promise<void> {
195
+ for (const p of asList(protocol)) {
196
+ this.handlers.delete(p);
197
+ }
198
+ return Promise.resolve();
199
+ }
200
+
201
+ getConnections(_peer?: PeerId): unknown[] {
202
+ return [];
203
+ }
204
+
205
+ dialProtocol(peer: PeerId, protocols: string | string[]): Promise<MockStreamEnd> {
206
+ const targetId = peer.toString();
207
+ if (this.down.has(targetId)) {
208
+ return Promise.reject(new Error(`peer ${targetId} is down`));
209
+ }
210
+ const target = this.registry.get(targetId);
211
+ if (target === undefined) {
212
+ return Promise.reject(new Error(`unknown peer ${targetId}`));
213
+ }
214
+ const handler = target.handlers.get(asList(protocols)[0]!);
215
+ if (handler === undefined) {
216
+ return Promise.reject(new Error(`no handler for ${asList(protocols)[0]} on ${targetId}`));
217
+ }
218
+ const [dialerEnd, handlerEnd] = streamPair();
219
+ handler(handlerEnd, { remotePeer: this.peerId });
220
+ return Promise.resolve(dialerEnd);
221
+ }
222
+
223
+ /** Inject a one-way frame as if `from` had sent it over `protocol` (the inbound transport seam). */
224
+ receive(protocol: string, frame: Uint8Array, from: PeerId): void {
225
+ const handler = this.handlers.get(protocol);
226
+ if (handler === undefined) {
227
+ throw new Error(`no handler for ${protocol}`);
228
+ }
229
+ const [dialerEnd, handlerEnd] = streamPair();
230
+ handler(handlerEnd, { remotePeer: from });
231
+ dialerEnd.send(frame);
232
+ void dialerEnd.close();
233
+ }
234
+ }
235
+
236
+ // --- mesh + shared FRET ---
237
+
238
+ export interface HostNode {
239
+ readonly member: Member;
240
+ readonly node: MockNode;
241
+ readonly host: CohortTopicHost;
242
+ }
243
+
244
+ export interface MeshOptions {
245
+ readonly wantK: number;
246
+ readonly minSigs: number;
247
+ /** Lowered `cap_promote` to drive promotion with a small participant count (live-tier / promotion suites). */
248
+ readonly capPromote?: number;
249
+ /**
250
+ * Extra {@link PromotionConfig} fields merged onto every node's lifecycle (alongside {@link capPromote}).
251
+ * Lets a virtual-time harness neutralise the wall-clock-rate heuristics (e.g. `tPromoteLookaheadMs: 0`
252
+ * to disable slope-based pre-promotion, which is meaningless when `now` is a fixed virtual instant).
253
+ */
254
+ readonly promotion?: Partial<PromotionConfig>;
255
+ /** Peers that reject inbound dials (crash / unreachable). They stay in FRET assembly (epoch unchanged). */
256
+ readonly downNodes?: readonly string[];
257
+ /** FRET network-size estimate (drives the walk start tier `d_max`). Default 256 → `d_max = 1`. */
258
+ readonly sizeEstimate?: number;
259
+ /** Per-node tier profile by index. Default all {@link coreProfile}. `'edge'` → {@link edgeProfile} (T0/T1 only). */
260
+ readonly profiles?: readonly ('edge' | 'core')[];
261
+ /** Gossip-driver cadence (ms). Default parks the timer far out so suites pump gossip deterministically. */
262
+ readonly gossipIntervalMs?: number;
263
+ /** Anti-DoS wiring applied to every node (e.g. a reputation view to force cold-root bootstrap denial). */
264
+ readonly antiDos?: CohortTopicAntiDosOptions;
265
+ }
266
+
267
+ /** One routed probe: the coord key it was issued at and the reply classification the walk saw. */
268
+ export interface RouteTraceEntry {
269
+ readonly key: string;
270
+ readonly result: RegisterResult;
271
+ }
272
+
273
+ export class CohortMesh {
274
+ readonly nodes: HostNode[] = [];
275
+ /** Coords every `routeAct` was keyed at (a walk's probe trail); a test clears + inspects it. */
276
+ readonly routeKeys: string[] = [];
277
+ /** Per-probe (key, reply-result) trace — richer than {@link routeKeys} for anti-flood walk assertions. */
278
+ readonly routeTrace: RouteTraceEntry[] = [];
279
+ private readonly registry = new Map<string, MockNode>();
280
+ private readonly activity = new Map<string, (activity: string, cohort: string[], minSigs: number, correlationId: string) => Promise<{ commitCertificate: string }>>();
281
+ private readonly down: Set<string>;
282
+ /** Members removed from FRET assembly (a membership change for rotation tests). They stay dialable. */
283
+ private readonly excluded = new Set<string>();
284
+
285
+ constructor(private readonly members: Member[], private readonly sizeEstimate: number, down: readonly string[]) {
286
+ this.down = new Set(down);
287
+ }
288
+
289
+ /**
290
+ * Drop `idStr` from FRET cohort assembly — the cohort serving any coord that included it now resolves to
291
+ * a different member set (a new `cohortEpoch`), which is the membership change that drives an epoch
292
+ * rotation. Unlike {@link crashNode}, the node stays dialable (it still answers `/sign`), so the outgoing
293
+ * cohort can co-sign the hand-off. Used by the rotation-attestation tests.
294
+ */
295
+ excludeFromAssembly(idStr: string): void {
296
+ this.excluded.add(idStr);
297
+ }
298
+
299
+ /** Restore `idStr` to FRET cohort assembly. */
300
+ includeInAssembly(idStr: string): void {
301
+ this.excluded.delete(idStr);
302
+ }
303
+
304
+ /** Deterministic FRET assembly: live (non-excluded) members sorted by XOR distance of ring position to `coord`. */
305
+ private sortedByDistance(coord: Uint8Array): Member[] {
306
+ return [...this.members].filter((m) => !this.excluded.has(m.idStr)).sort((a, b) => xorCompare(a.ringPos, b.ringPos, coord));
307
+ }
308
+
309
+ assembleCohort(coord: Uint8Array, wants: number): string[] {
310
+ return this.sortedByDistance(coord).slice(0, wants).map((m) => m.idStr);
311
+ }
312
+
313
+ /** The single node nearest `coord` — where `routeAct` runs the activity (the routed primary). */
314
+ nearest(coord: Uint8Array): Member {
315
+ return this.sortedByDistance(coord)[0]!;
316
+ }
317
+
318
+ nodeNearest(coord: Uint8Array): HostNode {
319
+ const id = this.nearest(coord).idStr;
320
+ return this.nodes.find((n) => n.member.idStr === id)!;
321
+ }
322
+
323
+ nodeOf(idStr: string): HostNode {
324
+ return this.nodes.find((n) => n.member.idStr === idStr)!;
325
+ }
326
+
327
+ /** Crash `idStr`: it rejects inbound dials but stays in FRET assembly, so `cohortEpoch` is unchanged. */
328
+ crashNode(idStr: string): void {
329
+ this.down.add(idStr);
330
+ }
331
+
332
+ /** Revive a previously-crashed node. */
333
+ reviveNode(idStr: string): void {
334
+ this.down.delete(idStr);
335
+ }
336
+
337
+ private async routeAct(msg: RouteAndMaybeActV1): Promise<NearAnchorV1 | { commitCertificate: string }> {
338
+ const key = b64urlToBytes(msg.key);
339
+ this.routeKeys.push(msg.key);
340
+ const target = this.nearest(key);
341
+ const handler = this.activity.get(target.idStr);
342
+ if (handler === undefined || this.down.has(target.idStr)) {
343
+ // No in-cluster activity to run (cold / unreachable target) → a bare anchor hint; the walk
344
+ // treats it as `no_state`.
345
+ this.routeTrace.push({ key: msg.key, result: 'no_state' });
346
+ return { v: 1, anchors: [], cohort_hint: [], estimated_cluster_size: this.members.length, confidence: 1 };
347
+ }
348
+ const cohort = this.assembleCohort(key, msg.want_k);
349
+ const reply = await handler(msg.activity ?? '', cohort, msg.min_sigs, msg.correlation_id);
350
+ this.routeTrace.push({ key: msg.key, result: replyResult(reply) });
351
+ return reply;
352
+ }
353
+
354
+ /** A FRET facade for one node, delegating routing/assembly to the shared mesh. */
355
+ fretFor(idStr: string): unknown {
356
+ return {
357
+ assembleCohort: (coord: Uint8Array, wants: number): string[] => this.assembleCohort(coord, wants),
358
+ setActivityHandler: (h: (activity: string, cohort: string[], minSigs: number, correlationId: string) => Promise<{ commitCertificate: string }>): void => {
359
+ this.activity.set(idStr, h);
360
+ },
361
+ routeAct: (msg: RouteAndMaybeActV1): Promise<NearAnchorV1 | { commitCertificate: string }> => this.routeAct(msg),
362
+ getNetworkSizeEstimate: (): { size_estimate: number; confidence: number; sources: number } => ({ size_estimate: this.sizeEstimate, confidence: 1, sources: 1 }),
363
+ };
364
+ }
365
+
366
+ registerNode(member: Member): MockNode {
367
+ const node = new MockNode(member.peerId, this.registry, this.down);
368
+ this.registry.set(member.idStr, node);
369
+ return node;
370
+ }
371
+
372
+ clearRouteLog(): void {
373
+ this.routeKeys.length = 0;
374
+ this.routeTrace.length = 0;
375
+ }
376
+
377
+ async stop(): Promise<void> {
378
+ await Promise.all(this.nodes.map((n) => n.host.stop()));
379
+ }
380
+ }
381
+
382
+ /** The reply classification a `routeAct` resolved with: decode the commit certificate's `RegisterReplyV1`. */
383
+ function replyResult(reply: NearAnchorV1 | { commitCertificate: string }): RegisterResult {
384
+ if ('commitCertificate' in reply) {
385
+ try {
386
+ return decodeRegisterReplyV1(b64urlToBytes(reply.commitCertificate)).result;
387
+ } catch {
388
+ return 'no_state';
389
+ }
390
+ }
391
+ return 'no_state';
392
+ }
393
+
394
+ /** Compare XOR distance of `a` vs `b` to `target` (big-endian) — a total order over distinct ring positions. */
395
+ export function xorCompare(a: Uint8Array, b: Uint8Array, target: Uint8Array): number {
396
+ for (let i = 0; i < target.length; i++) {
397
+ const da = (a[i] ?? 0) ^ target[i]!;
398
+ const db = (b[i] ?? 0) ^ target[i]!;
399
+ if (da !== db) {
400
+ return da - db;
401
+ }
402
+ }
403
+ return 0;
404
+ }
405
+
406
+ function profileAt(profiles: readonly ('edge' | 'core')[] | undefined, index: number): NodeProfile {
407
+ return profiles?.[index] === 'edge' ? edgeProfile() : coreProfile();
408
+ }
409
+
410
+ /** Build and start an N-node cohort mesh: one real-keyed node + FRET facade + cohort-topic host each. */
411
+ export async function buildMesh(members: Member[], opts: MeshOptions): Promise<CohortMesh> {
412
+ const mesh = new CohortMesh(members, opts.sizeEstimate ?? 256, opts.downNodes ?? []);
413
+ let index = 0;
414
+ for (const member of members) {
415
+ const node = mesh.registerNode(member);
416
+ const host = await createCohortTopicHost(node as never, mesh.fretFor(member.idStr) as never, {
417
+ privateKey: member.key,
418
+ wantK: opts.wantK,
419
+ minSigs: opts.minSigs,
420
+ profile: profileAt(opts.profiles, index),
421
+ // Park the periodic driver by default; tests pump gossip / membership / promotion deterministically.
422
+ gossipIntervalMs: opts.gossipIntervalMs ?? 3_600_000,
423
+ // Virtual time: tests drive publish `stabilizedAt` from explicit (often future-advanced) timestamps,
424
+ // not wall clock, so the `/sign` membership endorser's far-future `stabilizedAt` bound must not trip
425
+ // on them. An infinite clock disables that bound while leaving the finiteness check intact.
426
+ now: (): number => Number.POSITIVE_INFINITY,
427
+ ...((opts.capPromote === undefined && opts.promotion === undefined)
428
+ ? {}
429
+ : { promotion: { ...(opts.capPromote === undefined ? {} : { capPromote: opts.capPromote }), ...(opts.promotion ?? {}) } }),
430
+ ...(opts.antiDos === undefined ? {} : { antiDos: opts.antiDos }),
431
+ });
432
+ mesh.nodes.push({ member, node, host });
433
+ index++;
434
+ }
435
+ return mesh;
436
+ }
437
+
438
+ // --- signed frame builders (real participant peer-key signatures) ---
439
+
440
+ export async function signedWillingness(from: Member, coord: Uint8Array, epoch: Uint8Array, now: number, willingnessBits = 'f'): Promise<Uint8Array> {
441
+ const g: CohortGossipV1 = {
442
+ v: 1,
443
+ fromMember: bytesToB64url(from.bytes),
444
+ coord: bytesToB64url(coord),
445
+ cohortEpoch: bytesToB64url(epoch),
446
+ willingnessBits, // default 'f' → willing at every tier
447
+ loadBuckets: [0, 0, 0, 0],
448
+ windowSeconds: 60,
449
+ topicSummaries: [],
450
+ timestamp: now,
451
+ signature: '',
452
+ };
453
+ g.signature = bytesToB64url(await signPeer(from.key, cohortGossipSigningPayload(g)));
454
+ return encodeCohortMessage(g);
455
+ }
456
+
457
+ export interface SignedRegisterOptions {
458
+ readonly tier?: number;
459
+ readonly treeTier?: number;
460
+ readonly bootstrap?: boolean;
461
+ readonly ttl?: number;
462
+ }
463
+
464
+ export async function signedRegister(participant: Member, topic: Uint8Array, now: number, correlationId: string, opts: SignedRegisterOptions = {}): Promise<RegisterV1> {
465
+ const body: Omit<RegisterV1, 'signature'> = {
466
+ v: 1,
467
+ topicId: bytesToB64url(topic),
468
+ tier: opts.tier ?? 0,
469
+ treeTier: opts.treeTier ?? 0,
470
+ participantCoord: bytesToB64url(participant.bytes),
471
+ ttl: opts.ttl ?? 90_000,
472
+ bootstrap: opts.bootstrap ?? true,
473
+ timestamp: now,
474
+ correlationId: bytesToB64url(new TextEncoder().encode(correlationId)),
475
+ };
476
+ return { ...body, signature: bytesToB64url(await signPeer(participant.key, registerSigningPayload(body))) };
477
+ }
478
+
479
+ /** A plain ping (no `reattach`) — touches `lastPing` only when it lands on the computed primary / override. */
480
+ export async function signedPing(participant: Member, topic: Uint8Array, now: number, correlationId: string): Promise<RenewV1> {
481
+ const body: Omit<RenewV1, 'signature'> = {
482
+ v: 1,
483
+ topicId: bytesToB64url(topic),
484
+ participantId: bytesToB64url(participant.bytes),
485
+ correlationId: bytesToB64url(new TextEncoder().encode(correlationId)),
486
+ timestamp: now,
487
+ };
488
+ return { ...body, signature: bytesToB64url(await signPeer(participant.key, renewSigningPayload(body))) };
489
+ }
490
+
491
+ /** A signed crash-failover re-attach (`reattach: true` in the signed body) — promotes a backup. */
492
+ export async function signedReattach(participant: Member, topic: Uint8Array, now: number, correlationId = 'reattach'): Promise<RenewV1> {
493
+ const body: Omit<RenewV1, 'signature'> = {
494
+ v: 1,
495
+ topicId: bytesToB64url(topic),
496
+ participantId: bytesToB64url(participant.bytes),
497
+ correlationId: bytesToB64url(new TextEncoder().encode(correlationId)),
498
+ timestamp: now,
499
+ reattach: true,
500
+ };
501
+ return { ...body, signature: bytesToB64url(await signPeer(participant.key, renewSigningPayload(body))) };
502
+ }
503
+
504
+ export const slots = createSlotAssigner(new RingHash());
505
+ export const addressing = createTierAddressing(new RingHash());
506
+
507
+ // --- walk-trace reconstruction (feeds the db-core anti-flood invariant predicates) ---
508
+
509
+ /**
510
+ * Map each tier coordinate `coord_d(participant, topic)` for `d ∈ [0, dMax]` to its tier `d` (base64url
511
+ * key → tier). A real walk's routed keys are matched against this map to recover the per-probe tier, so
512
+ * the {@link import("@optimystic/db-core").WalkTrace}-shaped trace can be fed to `outwardMovesArePromoted`
513
+ * / `inwardStepsFollowNoState` / `retriesRestartAtDMax`. `coord_0` is participant-independent (it equals
514
+ * `coord0(topic)`); the bootstrap re-issue at the root reuses that same key, so both root probes map to 0.
515
+ */
516
+ export function coordTierMap(participant: Member, topic: Uint8Array, dMax: number, tierAddr = addressing): Map<string, number> {
517
+ const map = new Map<string, number>();
518
+ // Walk inward so the participant-independent coord_0 wins the key if a deeper coord ever aliased it.
519
+ for (let d = dMax; d >= 0; d--) {
520
+ map.set(bytesToB64url(tierAddr.coord(d, participant.bytes, topic)), d);
521
+ }
522
+ return map;
523
+ }
524
+
525
+ /** Reconstruct a {@link WalkTrace} from the mesh's recorded `routeTrace`, keeping only this walk's coords. */
526
+ export function walkTraceFrom(routeTrace: readonly RouteTraceEntry[], tierMap: Map<string, number>, dMax: number): WalkTrace {
527
+ const probes = routeTrace
528
+ .filter((e) => tierMap.has(e.key))
529
+ .map((e) => ({ treeTier: tierMap.get(e.key)!, result: e.result }));
530
+ return { dMax, probes };
531
+ }
532
+
533
+ /**
534
+ * Generate a real-keyed participant whose deterministic slot-**primary** (under `engine`'s cohort epoch
535
+ * + member set) is `primaryNode`. The cohort-side renewal only serves a plain ping / `reattach` with
536
+ * `ok` (the path that touches the record into the gossip deltas) when it lands on the participant's
537
+ * computed primary or a backup; for an arbitrary participant the node nearest `coord_0` is that primary
538
+ * only ~1/k of the time, so seeding/replication via a fixed deciding node is non-deterministic without
539
+ * pinning the participant to it.
540
+ */
541
+ export async function participantPrimaryAt(primaryNode: HostNode, engine: CoordEngine): Promise<Member> {
542
+ const { members, cohortEpoch } = engine.cohort();
543
+ for (;;) {
544
+ const p = await makeMember();
545
+ if (bytesEqual(slots.assignSlots(p.bytes, cohortEpoch, members).primary, primaryNode.member.bytes)) {
546
+ return p;
547
+ }
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Generate a real-keyed participant whose computed primary is `primaryNode` **and** whose `backups[0]`
553
+ * is `backupNode` (under `engine`'s epoch + member set). The crash-failover suite needs a participant
554
+ * whose first warm backup is a known sibling so a `reattach` landing there promotes deterministically.
555
+ */
556
+ export async function participantPrimaryBackupAt(primaryNode: HostNode, backupNode: HostNode, engine: CoordEngine): Promise<Member> {
557
+ const { members, cohortEpoch } = engine.cohort();
558
+ for (;;) {
559
+ const p = await makeMember();
560
+ const slot = slots.assignSlots(p.bytes, cohortEpoch, members);
561
+ if (bytesEqual(slot.primary, primaryNode.member.bytes) && slot.backups[0] !== undefined && bytesEqual(slot.backups[0], backupNode.member.bytes)) {
562
+ return p;
563
+ }
564
+ }
565
+ }
566
+
567
+ // --- topic setup (instantiate coord-0 engines on the cohort + seed willingness quorum) ---
568
+
569
+ export interface TopicSetup {
570
+ readonly coord0: RingCoord;
571
+ /** Engine on every coord-0 cohort member, keyed by member id string. */
572
+ readonly engines: Map<string, CoordEngine>;
573
+ /** The routed primary for `coord_0` (where `routeAct` lands a bootstrap register). */
574
+ readonly deciding: HostNode;
575
+ readonly decidingEngine: CoordEngine;
576
+ /** The coord-0 cohort member id strings (the `wantK` nearest to `coord_0`). */
577
+ readonly cohortIds: readonly string[];
578
+ }
579
+
580
+ /**
581
+ * Instantiate the tier-0 coord engine for `topic` on every **coord-0 cohort member** and seed each one's
582
+ * coord-0 gossip view with every *other* cohort member's willingness, so any cohort member (in
583
+ * particular the routed primary) meets the willingness quorum and can admit. Mirrors the willingness
584
+ * bootstrap the gossip-cadence tests do — an idle engine builds no willingness frame, so the first
585
+ * registration needs a seed. Operating on the cohort (not all `N` nodes) keeps setup `O(wantK²)` at
586
+ * scale; for a whole-network cohort (`wantK = N`) it covers every node, matching the live-tier milestone.
587
+ */
588
+ export async function setupTopic(mesh: CohortMesh, topic: Uint8Array, tierAddr = addressing): Promise<TopicSetup> {
589
+ const coord0 = tierAddr.coord0(topic);
590
+ const seedParticipant = mesh.nodes[0]!.member.bytes; // dummy participantCoord (unused at tier 0)
591
+ // Resolve the cohort the host actually assembles around coord_0 from any node (they all agree).
592
+ const decidingNode = mesh.nodeNearest(coord0);
593
+ const probeEngine = decidingNode.host.registry.forCoord(coord0, 0 as Tier, seedParticipant);
594
+ const cohortIds = probeEngine.cohort().members.map((m) => bytesToPeerIdString(m));
595
+ const cohortNodes = cohortIds.map((id) => mesh.nodeOf(id)).filter((n): n is HostNode => n !== undefined);
596
+
597
+ const engines = new Map<string, CoordEngine>();
598
+ for (const node of cohortNodes) {
599
+ engines.set(node.member.idStr, node.host.registry.forCoord(coord0, 0 as Tier, seedParticipant));
600
+ }
601
+ const now = Date.now();
602
+ for (const node of cohortNodes) {
603
+ const epoch = engines.get(node.member.idStr)!.cohort().cohortEpoch;
604
+ for (const other of cohortNodes) {
605
+ if (other.member.idStr === node.member.idStr) {
606
+ continue;
607
+ }
608
+ node.node.receive(PROTOCOLS.gossip, await signedWillingness(other.member, coord0, epoch, now), other.member.peerId);
609
+ }
610
+ }
611
+ await delay(20); // let the async gossip handlers merge the willingness contributions
612
+ return { coord0, engines, deciding: decidingNode, decidingEngine: engines.get(decidingNode.member.idStr)!, cohortIds };
613
+ }