@optimystic/db-p2p 0.13.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) 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 +8 -1
  134. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  135. package/dist/src/libp2p-key-network.js +67 -8
  136. package/dist/src/libp2p-key-network.js.map +1 -1
  137. package/dist/src/libp2p-node-base.d.ts +57 -3
  138. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  139. package/dist/src/libp2p-node-base.js +523 -6
  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 +25 -0
  186. package/dist/src/protocol-client.d.ts.map +1 -1
  187. package/dist/src/protocol-client.js +126 -4
  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 -19
  235. package/dist/src/repo/client.js.map +1 -1
  236. package/dist/src/repo/cluster-coordinator.d.ts +9 -0
  237. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  238. package/dist/src/repo/cluster-coordinator.js +65 -22
  239. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  240. package/dist/src/repo/coordinator-repo.d.ts +20 -0
  241. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  242. package/dist/src/repo/coordinator-repo.js +101 -16
  243. package/dist/src/repo/coordinator-repo.js.map +1 -1
  244. package/dist/src/repo/service.d.ts +46 -1
  245. package/dist/src/repo/service.d.ts.map +1 -1
  246. package/dist/src/repo/service.js +79 -45
  247. package/dist/src/repo/service.js.map +1 -1
  248. package/dist/src/rpc-deadline.d.ts +37 -0
  249. package/dist/src/rpc-deadline.d.ts.map +1 -0
  250. package/dist/src/rpc-deadline.js +31 -0
  251. package/dist/src/rpc-deadline.js.map +1 -0
  252. package/dist/src/storage/block-storage.d.ts +2 -0
  253. package/dist/src/storage/block-storage.d.ts.map +1 -1
  254. package/dist/src/storage/block-storage.js +95 -2
  255. package/dist/src/storage/block-storage.js.map +1 -1
  256. package/dist/src/storage/i-block-storage.d.ts +25 -0
  257. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  258. package/dist/src/storage/storage-repo.d.ts +53 -2
  259. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  260. package/dist/src/storage/storage-repo.js +193 -9
  261. package/dist/src/storage/storage-repo.js.map +1 -1
  262. package/dist/src/sync/client.d.ts +4 -1
  263. package/dist/src/sync/client.d.ts.map +1 -1
  264. package/dist/src/sync/client.js +5 -2
  265. package/dist/src/sync/client.js.map +1 -1
  266. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
  267. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
  268. package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
  269. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
  270. package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
  271. package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
  272. package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
  273. package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
  274. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  275. package/dist/src/testing/mesh-harness.js +23 -1
  276. package/dist/src/testing/mesh-harness.js.map +1 -1
  277. package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
  278. package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
  279. package/dist/src/testing/reactivity-mesh-harness.js +663 -0
  280. package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
  281. package/package.json +7 -4
  282. package/src/cluster/block-transfer-service.ts +133 -68
  283. package/src/cluster/client.ts +64 -45
  284. package/src/cluster/cluster-error.ts +64 -0
  285. package/src/cluster/cluster-repo.ts +405 -55
  286. package/src/cluster/commit-cert.ts +139 -0
  287. package/src/cluster/service.ts +116 -44
  288. package/src/cluster/spread-on-churn.ts +328 -285
  289. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
  290. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
  291. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
  292. package/src/cohort-topic/change-bridge.ts +109 -0
  293. package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
  294. package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
  295. package/src/cohort-topic/fret-trust-anchor.ts +153 -0
  296. package/src/cohort-topic/host.ts +2175 -0
  297. package/src/cohort-topic/index.ts +13 -0
  298. package/src/cohort-topic/membership-publish-sink.ts +20 -0
  299. package/src/cohort-topic/membership-source.ts +68 -0
  300. package/src/cohort-topic/peer-codec.ts +31 -0
  301. package/src/cohort-topic/peer-sig.ts +86 -0
  302. package/src/cohort-topic/protocols.ts +71 -0
  303. package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
  304. package/src/cohort-topic/size-estimator.ts +16 -0
  305. package/src/cohort-topic/stream-util.ts +87 -0
  306. package/src/cohort-topic/threshold-crypto.ts +239 -0
  307. package/src/cohort-topic/topic-router.ts +77 -0
  308. package/src/dispute/cascade.ts +517 -0
  309. package/src/dispute/client.ts +12 -2
  310. package/src/dispute/dispute-service.ts +171 -24
  311. package/src/dispute/index.ts +42 -0
  312. package/src/dispute/invalidation.ts +587 -0
  313. package/src/dispute/types.ts +14 -0
  314. package/src/index.ts +4 -0
  315. package/src/libp2p-key-network.ts +67 -9
  316. package/src/libp2p-node-base.ts +633 -14
  317. package/src/matchmaking/aggregate-counts.ts +104 -0
  318. package/src/matchmaking/index.ts +20 -0
  319. package/src/matchmaking/module.ts +363 -0
  320. package/src/matchmaking/protocols.ts +51 -0
  321. package/src/matchmaking/provider-manager.ts +95 -0
  322. package/src/matchmaking/query-handler.ts +88 -0
  323. package/src/matchmaking/query-transport.ts +483 -0
  324. package/src/matchmaking/seeker-manager.ts +64 -0
  325. package/src/matchmaking/seeker-walk-client.ts +293 -0
  326. package/src/matchmaking/traffic-validation.ts +195 -0
  327. package/src/network/network-manager-service.ts +9 -2
  328. package/src/protocol-client.ts +119 -5
  329. package/src/reactivity/forwarder-host.ts +438 -0
  330. package/src/reactivity/index.ts +19 -0
  331. package/src/reactivity/notify-transport.ts +144 -0
  332. package/src/reactivity/origination-manager.ts +192 -0
  333. package/src/reactivity/protocols.ts +61 -0
  334. package/src/reactivity/push-state-gossip.ts +291 -0
  335. package/src/reactivity/recover-transport.ts +408 -0
  336. package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
  337. package/src/reactivity/subscriber-registry.ts +96 -0
  338. package/src/reactivity/subscription-manager.ts +450 -0
  339. package/src/reactivity/topic-bytes.ts +37 -0
  340. package/src/repo/client.ts +54 -19
  341. package/src/repo/cluster-coordinator.ts +66 -26
  342. package/src/repo/coordinator-repo.ts +99 -15
  343. package/src/repo/service.ts +90 -42
  344. package/src/rpc-deadline.ts +45 -0
  345. package/src/storage/block-storage.ts +106 -3
  346. package/src/storage/i-block-storage.ts +27 -0
  347. package/src/storage/storage-repo.ts +209 -13
  348. package/src/sync/client.ts +5 -2
  349. package/src/testing/cohort-topic-mesh-harness.ts +613 -0
  350. package/src/testing/matchmaking-mesh-harness.ts +475 -0
  351. package/src/testing/mesh-harness.ts +22 -2
  352. package/src/testing/reactivity-mesh-harness.ts +922 -0
@@ -1,7 +1,9 @@
1
- import type { IRepo, ClusterRecord, Signature, RepoMessage, ITransactionValidator, ClusterConsensusConfig } from "@optimystic/db-core";
1
+ import type { IRepo, ClusterRecord, Signature, RepoMessage, ITransactionValidator, ClusterConsensusConfig, CommitResult, BlockId, ActionId, ActionRev, CommitRequest, CommitCert, InvalidateRequest } from "@optimystic/db-core";
2
2
  import type { ICluster } from "@optimystic/db-core";
3
3
  import type { IPeerNetwork } from "@optimystic/db-core";
4
4
  import { blockIdsForTransforms } from "@optimystic/db-core";
5
+ import { verifyInvalidationCertificate, type ArbitratorSetRecompute } from "../dispute/invalidation.js";
6
+ import { buildCommitCert, invalidationActionId } from "./commit-cert.js";
5
7
  import { ClusterClient } from "./client.js";
6
8
  import type { PeerId, PrivateKey } from "@libp2p/interface";
7
9
  import { peerIdFromString } from "@libp2p/peer-id";
@@ -35,6 +37,54 @@ interface TransactionState {
35
37
  lastUpdate: number;
36
38
  }
37
39
 
40
+ /**
41
+ * Actively reconciles a block this member committed without having seen the matching
42
+ * pend (cohort drift between the independent pend and commit cluster-transactions).
43
+ * Pulls the committed revision from a cohort peer that holds it and restores it into
44
+ * local storage. Injected so {@link ClusterMember} stays transport-agnostic — mirrors
45
+ * how `CoordinatorRepo` receives its `clusterLatestCallback`.
46
+ *
47
+ * @param blockId the under-replicated block to restore
48
+ * @param committed the committed `(actionId, rev)` agreed by consensus
49
+ * @param cohortPeerIds cohort members to pull from (self already excluded)
50
+ */
51
+ export type ReconcileBlockCallback = (blockId: BlockId, committed: ActionRev, cohortPeerIds: string[]) => Promise<void>;
52
+
53
+ /**
54
+ * Sink for the {@link CommitCert} this member assembled at consensus, fired once per committed action
55
+ * **before** the commit is applied to local storage (so it is already retained when
56
+ * {@link StorageRepo.commit} emits the matching `CollectionChangeEvent` the reactivity bridge reads).
57
+ * The bytes are the cluster's own `approve` commit signatures, forwarded UNCHANGED — never re-signed.
58
+ * Optional; absent on nodes that do not originate reactivity notifications. A throwing sink is
59
+ * isolated + logged (it must never break consensus).
60
+ */
61
+ export type CommitCertificateSink = (actionId: ActionId, cert: CommitCert) => void;
62
+
63
+ /**
64
+ * Applies a consensus-ordered {@link InvalidateRequest} to local storage — the deterministic
65
+ * reversal every member runs once consensus on the invalidation is reached. The implementation
66
+ * recomputes the per-block as-if-`T_inv`-absent content, writes the compensating revisions, and
67
+ * appends the durable invalidation log entry (see `applyInvalidation` in the dispute module). It
68
+ * is injected so {@link ClusterMember} stays storage/log-agnostic (mirrors {@link ReconcileBlockCallback}).
69
+ *
70
+ * The certificate is verified by {@link ClusterMember} *before* the sink is invoked, so a sink
71
+ * implementation may assume `request.resolution` is already a valid challenger-wins certificate.
72
+ * A throwing sink is logged and tolerated (never resets the cluster stream); the in-memory dedup
73
+ * marker is rolled back so a re-broadcast can retry.
74
+ */
75
+ export type InvalidationApplySink = (request: InvalidateRequest) => Promise<void>;
76
+
77
+ /**
78
+ * Optional **layer-2** capability for invalidation-certificate verification: re-derives the
79
+ * legitimately-selected arbitrator set from this member's topology view and judges the carried set
80
+ * (see {@link ArbitratorSetRecompute}). Injected so {@link ClusterMember} stays network-agnostic; the
81
+ * composition root supplies it from FRET when available. When absent (or when the member cannot
82
+ * reconstruct the historical topology), {@link applyConsensusInvalidation} accepts a layer-1-valid
83
+ * certificate and logs that it applied an invalidation it could not fully anchor — the documented
84
+ * interim posture (see `tickets/plan/cohort-topic-membership-cert-trust-anchoring.md`).
85
+ */
86
+ export type RecomputeArbitratorSetCapability = ArbitratorSetRecompute;
87
+
38
88
  interface ClusterMemberComponents {
39
89
  storageRepo: IRepo;
40
90
  peerNetwork: IPeerNetwork;
@@ -48,6 +98,14 @@ interface ClusterMemberComponents {
48
98
  reputation?: IPeerReputation;
49
99
  consensusConfig?: ClusterConsensusConfig;
50
100
  stateStore?: ITransactionStateStore;
101
+ /** Restores a block under-replicated by cohort drift; see {@link ReconcileBlockCallback}. */
102
+ reconcileBlock?: ReconcileBlockCallback;
103
+ /** Receives the consensus commit cert per committed action; see {@link CommitCertificateSink}. */
104
+ onCommitCertificate?: CommitCertificateSink;
105
+ /** Applies a consensus-ordered invalidation to local storage; see {@link InvalidationApplySink}. */
106
+ onInvalidate?: InvalidationApplySink;
107
+ /** Layer-2 arbitrator-set recompute for invalidation verification; see {@link RecomputeArbitratorSetCapability}. */
108
+ recomputeArbitratorSet?: RecomputeArbitratorSetCapability;
51
109
  }
52
110
 
53
111
  export function clusterMember(components: ClusterMemberComponents): ClusterMember {
@@ -62,13 +120,32 @@ export function clusterMember(components: ClusterMemberComponents): ClusterMembe
62
120
  components.validator,
63
121
  components.reputation,
64
122
  components.consensusConfig,
65
- components.stateStore
123
+ components.stateStore,
124
+ components.reconcileBlock,
125
+ components.onCommitCertificate,
126
+ components.onInvalidate,
127
+ components.recomputeArbitratorSet
66
128
  );
67
129
  }
68
130
 
69
131
  // How long to keep executed transaction records (10 minutes)
70
132
  const ExecutedTransactionTtlMs = 10 * 60 * 1000;
71
133
 
134
+ // Upper bound on an awaited active reconciliation of a divergent commit. Bounds the
135
+ // consensus path so a slow/unreachable cohort peer can't stall the cluster stream;
136
+ // a timeout is logged and tolerated (never thrown — that would reset the stream).
137
+ const ReconcileTimeoutMs = 5000;
138
+
139
+ /**
140
+ * True when a thrown storage error reports a missing pending action — i.e. this
141
+ * member reached commit-consensus without having seen the matching pend phase.
142
+ * That is recoverable local divergence (reconciled via sync / read-repair), not
143
+ * a transaction fault, so it must not reset the cluster stream.
144
+ */
145
+ function isMissingPendingActionError(err: unknown): boolean {
146
+ return err instanceof Error && /pending action .+ not found/i.test(err.message);
147
+ }
148
+
72
149
  /**
73
150
  * Handles cluster-side operations, managing promises and commits for cluster updates
74
151
  * and coordinating with the local storage repo.
@@ -78,12 +155,15 @@ export class ClusterMember implements ICluster {
78
155
  private activeTransactions: Map<string, TransactionState> = new Map();
79
156
  // Track executed consensus transactions to prevent duplicate execution (messageHash -> executedAt timestamp)
80
157
  private executedTransactions: Map<string, number> = new Map();
158
+ // Fast in-memory dedup for applied invalidations, keyed `${invalidatedActionId}:${disputeId}`.
159
+ // The durable source of truth is the invalidation log entry (Log.findInvalidation, re-checked
160
+ // inside the sink); this map only spares redundant work when the same invalidation reaches
161
+ // consensus twice (rebroadcast / sync) under different message hashes. (-> appliedAt timestamp)
162
+ private appliedInvalidations: Map<string, number> = new Map();
81
163
  // Queue of transactions to clean up
82
164
  private cleanupQueue: string[] = [];
83
165
  // Serialize concurrent updates for the same transaction
84
166
  private pendingUpdates: Map<string, Promise<ClusterRecord>> = new Map();
85
- // Temporarily set during validateSignatures so verifySignature can access the record
86
- private currentValidationRecord?: ClusterRecord;
87
167
  // Interval handles for periodic cleanup (stored so dispose() can clear them)
88
168
  private readonly expirationInterval: NodeJS.Timeout;
89
169
  private readonly cleanupInterval: NodeJS.Timeout;
@@ -103,7 +183,11 @@ export class ClusterMember implements ICluster {
103
183
  private readonly validator?: ITransactionValidator,
104
184
  private readonly reputation?: IPeerReputation,
105
185
  consensusConfig?: ClusterConsensusConfig,
106
- private readonly stateStore?: ITransactionStateStore
186
+ private readonly stateStore?: ITransactionStateStore,
187
+ private readonly reconcileBlock?: ReconcileBlockCallback,
188
+ private readonly onCommitCertificate?: CommitCertificateSink,
189
+ private readonly onInvalidate?: InvalidationApplySink,
190
+ private readonly recomputeArbitratorSet?: RecomputeArbitratorSetCapability
107
191
  ) {
108
192
  this.superMajorityThreshold = consensusConfig?.superMajorityThreshold ?? 1.0;
109
193
  // Periodically clean up expired transactions (.unref() so tests/short-lived processes can exit)
@@ -440,27 +524,22 @@ export class ClusterMember implements ICluster {
440
524
  }
441
525
 
442
526
  private async validateSignatures(record: ClusterRecord): Promise<void> {
443
- this.currentValidationRecord = record;
444
- try {
445
- // Validate promise signatures
446
- const promiseHash = await this.computePromiseHash(record);
447
- for (const [peerId, signature] of Object.entries(record.promises)) {
448
- if (!await this.verifySignature(peerId, promiseHash, signature)) {
449
- this.reputation?.reportPeer(peerId, PenaltyReason.InvalidSignature, `promise:${record.messageHash}`);
450
- throw new Error(`Invalid promise signature from ${peerId}`);
451
- }
527
+ // Validate promise signatures
528
+ const promiseHash = await this.computePromiseHash(record);
529
+ for (const [peerId, signature] of Object.entries(record.promises)) {
530
+ if (!await this.verifySignature(record, peerId, promiseHash, signature)) {
531
+ this.reputation?.reportPeer(peerId, PenaltyReason.InvalidSignature, `promise:${record.messageHash}`);
532
+ throw new Error(`Invalid promise signature from ${peerId}`);
452
533
  }
534
+ }
453
535
 
454
- // Validate commit signatures
455
- const commitHash = await this.computeCommitHash(record);
456
- for (const [peerId, signature] of Object.entries(record.commits)) {
457
- if (!await this.verifySignature(peerId, commitHash, signature)) {
458
- this.reputation?.reportPeer(peerId, PenaltyReason.InvalidSignature, `commit:${record.messageHash}`);
459
- throw new Error(`Invalid commit signature from ${peerId}`);
460
- }
536
+ // Validate commit signatures
537
+ const commitHash = await this.computeCommitHash(record);
538
+ for (const [peerId, signature] of Object.entries(record.commits)) {
539
+ if (!await this.verifySignature(record, peerId, commitHash, signature)) {
540
+ this.reputation?.reportPeer(peerId, PenaltyReason.InvalidSignature, `commit:${record.messageHash}`);
541
+ throw new Error(`Invalid commit signature from ${peerId}`);
461
542
  }
462
- } finally {
463
- this.currentValidationRecord = undefined;
464
543
  }
465
544
  }
466
545
 
@@ -496,8 +575,8 @@ export class ClusterMember implements ICluster {
496
575
  return uint8ArrayToString(sigBytes, 'base64url');
497
576
  }
498
577
 
499
- private async verifySignature(peerId: string, hash: string, signature: Signature): Promise<boolean> {
500
- const peerInfo = this.currentValidationRecord?.peers[peerId];
578
+ private async verifySignature(record: ClusterRecord, peerId: string, hash: string, signature: Signature): Promise<boolean> {
579
+ const peerInfo = record.peers[peerId];
501
580
  if (!peerInfo?.publicKey?.length) {
502
581
  throw new Error(`No public key for peer ${peerId}`);
503
582
  }
@@ -677,44 +756,305 @@ export class ClusterMember implements ICluster {
677
756
  .catch(err => log('cluster-member:persist-executed-error', { messageHash: record.messageHash, error: (err as Error).message }));
678
757
 
679
758
  try {
680
- // Execute the operations - check return values for failures
681
759
  for (const operation of record.message.operations) {
682
- if ('get' in operation) {
683
- await this.storageRepo.get(operation.get);
684
- } else if ('pend' in operation) {
685
- const result = await this.storageRepo.pend(operation.pend);
686
- if (!result.success) {
687
- log('cluster-member:consensus-pend-failed', {
688
- messageHash: record.messageHash,
689
- actionId: operation.pend.actionId,
690
- reason: result.reason,
691
- hasMissing: !!result.missing?.length,
692
- hasPending: !!result.pending?.length
693
- });
694
- throw new Error(`Consensus pend failed for action ${operation.pend.actionId}: ${result.reason ?? 'stale revision'}`);
695
- }
696
- } else if ('commit' in operation) {
697
- const result = await this.storageRepo.commit(operation.commit);
698
- if (!result.success) {
699
- log('cluster-member:consensus-commit-failed', {
700
- messageHash: record.messageHash,
701
- actionId: operation.commit.actionId,
702
- reason: result.reason,
703
- hasMissing: !!result.missing?.length
704
- });
705
- throw new Error(`Consensus commit failed for action ${operation.commit.actionId}: ${result.reason ?? 'stale revision'}`);
706
- }
707
- } else if ('cancel' in operation) {
708
- await this.storageRepo.cancel(operation.cancel.actionRef);
709
- }
760
+ await this.applyConsensusOperation(record, operation);
710
761
  }
711
762
  } catch (err) {
712
- // On failure, remove from executedTransactions so it can be retried
763
+ // A genuinely unexpected fault (e.g. storage I/O) roll back the executed
764
+ // marker so a corrected retry can re-run, and propagate so the caller learns
765
+ // the real cause. Recoverable local divergence is absorbed inside
766
+ // applyConsensusOperation and never reaches here.
713
767
  this.executedTransactions.delete(record.messageHash);
714
768
  throw err;
715
769
  }
716
770
  }
717
771
 
772
+ /**
773
+ * Applies one consensus-approved operation to local storage.
774
+ *
775
+ * By the time we reach here the cluster has *already* reached consensus, so the
776
+ * operation is authoritative cluster-wide. A failure applying it to THIS member's
777
+ * local store therefore does not mean the operation is invalid — it means our
778
+ * local state has diverged from the agreed history:
779
+ *
780
+ * - **ahead**: we already hold a newer revision, so a stale pend/commit is a
781
+ * no-op for us (`StorageRepo.commit` returns `success:false` with `missing`);
782
+ * - **behind**: we missed the prior `pend` cluster-transaction (cohort drift
783
+ * between the independent pend and commit phases, or transient unreachability),
784
+ * so we lack the pending action — `StorageRepo.commit` *throws* "Pending
785
+ * action … not found".
786
+ *
787
+ * For the **behind** case we hold no revision of the committed blocks at all, so we
788
+ * actively reconcile: pull the committed revision from a cohort peer that holds it
789
+ * (`reconcileBlock`) and restore it locally. Lazy read-repair on a later read cannot
790
+ * recover it on its own when cohort drift has left the block under-replicated (no
791
+ * reachable peer the reader sees holds the newer rev), so reconciling here is what
792
+ * keeps cross-cohort transactions converging. For the **ahead** case we already hold
793
+ * ≥ the committed rev, so we tolerate the no-op without reconciling downward.
794
+ *
795
+ * Whatever happens, we must NOT reset the stream: throwing here would reset the
796
+ * cluster stream the coordinator is awaiting and surface as a spurious
797
+ * `StreamResetError`, sinking an otherwise-successful transaction. So divergence —
798
+ * and any reconciliation failure — is tolerated (logged for observability). A
799
+ * genuinely *invalid* pend can never get this far: it is rejected during the promise
800
+ * phase (`validatePendOperations`, which validates pend ops only — commits carry no
801
+ * promise-phase validation).
802
+ *
803
+ * The propagate-vs-tolerate split keys off the failure's *nature* via `CommitResult`,
804
+ * not throw-vs-return: a missing pend (thrown "not found") or a stale/ahead commit
805
+ * (`success:false` with `missing`) is divergence and tolerated, whereas a genuine
806
+ * mid-commit `internalCommit` fault (`success:false` with a bare `reason`, no
807
+ * `missing`) is propagated so {@link handleConsensus} rolls back the executed marker
808
+ * and rethrows — exactly like an unexpected thrown fault.
809
+ */
810
+ private async applyConsensusOperation(record: ClusterRecord, operation: RepoMessage['operations'][number]): Promise<void> {
811
+ const messageHash = record.messageHash;
812
+ if ('get' in operation) {
813
+ await this.storageRepo.get(operation.get);
814
+ return;
815
+ }
816
+ if ('cancel' in operation) {
817
+ await this.storageRepo.cancel(operation.cancel.actionRef);
818
+ return;
819
+ }
820
+ if ('pend' in operation) {
821
+ const result = await this.storageRepo.pend(operation.pend);
822
+ if (!result.success) {
823
+ log('cluster-member:consensus-pend-diverged', {
824
+ messageHash,
825
+ actionId: operation.pend.actionId,
826
+ reason: result.reason,
827
+ hasMissing: !!result.missing?.length,
828
+ hasPending: !!result.pending?.length
829
+ });
830
+ }
831
+ return;
832
+ }
833
+ if ('commit' in operation) {
834
+ const commit = operation.commit;
835
+ // Capture the consensus commit cert BEFORE applying to storage: StorageRepo.commit emits the
836
+ // CollectionChangeEvent synchronously at the end of the call below, and the reactivity bridge
837
+ // resolves the cert from that event — so it must already be retained when commit() returns.
838
+ //
839
+ // The commit-vote signed preimage is computed HERE, before the synchronous capture+commit
840
+ // sequence below: it is the exact bytes each approving signer endorsed
841
+ // (`utf8(commitHash + ":approve")`), reproduced from this already-validated `record` (its
842
+ // commit signatures were verified against the same `computeCommitHash`). Reactivity sets a
843
+ // notification's `digest` from it so a subscriber's threshold-verify over `digest` succeeds.
844
+ // The one `await` happens before `captureCommitCert` runs, so the cert is still retained
845
+ // synchronously before `commit()` emits its change event (do not move this past the commit).
846
+ // Gated on the sink: with no reactivity wired the preimage has no consumer, so a sink-less
847
+ // node pays neither the extra `sha256` nor the extra microtask — the true zero-cost default.
848
+ if (this.onCommitCertificate) {
849
+ const commitSignedPayload = this.computeSigningPayload(await this.computeCommitHash(record), 'approve');
850
+ this.captureCommitCert(record, commit.actionId, commitSignedPayload);
851
+ }
852
+ let result: CommitResult;
853
+ try {
854
+ result = await this.storageRepo.commit(commit);
855
+ } catch (err) {
856
+ // `StorageRepo.commit` throws (rather than returning success:false) when
857
+ // the pending action is missing — the canonical "behind" signal: this
858
+ // member reached commit-consensus without the matching pend (cohort drift).
859
+ if (isMissingPendingActionError(err)) {
860
+ log('cluster-member:consensus-commit-diverged', {
861
+ messageHash,
862
+ actionId: commit.actionId,
863
+ divergence: 'behind',
864
+ reason: (err as Error).message
865
+ });
866
+ // We hold no revision of these blocks; pull the committed revision from a
867
+ // cohort peer so the block is no longer under-replicated. Best-effort:
868
+ // failures are logged inside, never thrown (a throw would reset the stream).
869
+ await this.reconcileDivergentCommit(record, commit);
870
+ return;
871
+ }
872
+ throw err;
873
+ }
874
+ if (!result.success) {
875
+ // success:false is a StaleFailure. `missing` ⇒ ahead/stale divergence
876
+ // (we already hold ≥ this rev): tolerate, do NOT reconcile downward. A bare
877
+ // `reason` with no `missing` ⇒ a genuine internalCommit fault: propagate so
878
+ // handleConsensus rolls back the executed marker and rethrows.
879
+ if (result.missing?.length) {
880
+ log('cluster-member:consensus-commit-diverged', {
881
+ messageHash,
882
+ actionId: commit.actionId,
883
+ divergence: 'ahead',
884
+ reason: result.reason,
885
+ hasMissing: true
886
+ });
887
+ return;
888
+ }
889
+ throw new Error(`Consensus commit for action ${commit.actionId} failed: ${result.reason ?? 'unknown reason'}`);
890
+ }
891
+ return;
892
+ }
893
+ if ('invalidate' in operation) {
894
+ await this.applyConsensusInvalidation(record, operation.invalidate);
895
+ return;
896
+ }
897
+ }
898
+
899
+ /**
900
+ * Applies a consensus-ordered invalidation on this member: dedup → certificate verification →
901
+ * capture the invalidation's commit cert (for reactivity reuse) → delegate the compensating write +
902
+ * durable log append to the injected {@link InvalidationApplySink}.
903
+ *
904
+ * Like every other branch of {@link applyConsensusOperation}, a failure here is tolerated rather
905
+ * than thrown — a throw would reset the cluster stream. A forged/sub-threshold certificate is
906
+ * rejected (and never reaches the sink); a sink fault is logged and its dedup marker rolled back
907
+ * so a re-broadcast can retry. The durable, authoritative dedup is the invalidation log entry the
908
+ * sink consults; the in-memory map is only a fast path.
909
+ */
910
+ private async applyConsensusInvalidation(record: ClusterRecord, request: InvalidateRequest): Promise<void> {
911
+ const messageHash = record.messageHash;
912
+ const dedupKey = `${request.invalidatedActionId}:${request.resolution.disputeId}`;
913
+ if (this.appliedInvalidations.has(dedupKey)) {
914
+ log('cluster-member:consensus-invalidate-duplicate', { messageHash, dedupKey });
915
+ return;
916
+ }
917
+
918
+ // Certificate verification BEFORE apply — never trust the originator's say-so. Verify the proof
919
+ // against THIS request's own target: the votes are bound to the transaction the dispute resolved,
920
+ // so a genuine proof carried in a request that points at a different (innocent) action/blocks fails
921
+ // here (the network-facing replay boundary, #2). The proof is also bound to the legitimately-selected
922
+ // arbitrator set (#1): membership + the challenger's set signature gate it (layer 1), and when a
923
+ // recompute capability is wired we re-derive the eligible set from our topology and reject a forged
924
+ // one (layer 2). When we cannot reconstruct the historical topology, we accept on layer 1 and LOG
925
+ // that the invalidation was applied without full anchoring — the documented interim posture.
926
+ const certified = await verifyInvalidationCertificate(
927
+ request.resolution,
928
+ { invalidatedActionId: request.invalidatedActionId, blockIds: request.blockIds },
929
+ {
930
+ recomputeArbitratorSet: this.recomputeArbitratorSet,
931
+ onUnanchored: (info) => log('cluster-member:consensus-invalidate-unanchored', {
932
+ messageHash,
933
+ invalidatedActionId: request.invalidatedActionId,
934
+ disputeId: info.disputeId,
935
+ reason: info.reason,
936
+ arbitratorSetSize: info.arbitratorSet.length
937
+ })
938
+ }
939
+ );
940
+ if (!certified) {
941
+ log('cluster-member:consensus-invalidate-reject-certificate', {
942
+ messageHash,
943
+ invalidatedActionId: request.invalidatedActionId,
944
+ disputeId: request.resolution.disputeId,
945
+ outcome: request.resolution.outcome
946
+ });
947
+ return;
948
+ }
949
+
950
+ // Capture the invalidation's own commit cert — the threshold signature the cohort produced for
951
+ // THIS consensus-ordered `invalidate` op (over `computeCommitHash(record)`), the reversal analogue
952
+ // of the commit-cert capture in {@link applyConsensusOperation}. Reactivity reuses it bit-for-bit
953
+ // as the invalidation notification's `sig` (never re-signed). Keyed on the deterministic
954
+ // {@link invalidationActionId} the invalidation's change event also carries, so the bridge's
955
+ // cert extractor resolves it. Gated on the sink — a node with no reactivity wired pays nothing.
956
+ if (this.onCommitCertificate) {
957
+ const invSignedPayload = this.computeSigningPayload(await this.computeCommitHash(record), 'approve');
958
+ this.captureCommitCert(record, invalidationActionId(request.invalidatedActionId, request.resolution.disputeId), invSignedPayload);
959
+ }
960
+
961
+ if (!this.onInvalidate) {
962
+ log('cluster-member:consensus-invalidate-no-sink', { messageHash, dedupKey });
963
+ return;
964
+ }
965
+
966
+ this.appliedInvalidations.set(dedupKey, Date.now());
967
+ try {
968
+ await this.onInvalidate(request);
969
+ log('cluster-member:consensus-invalidate-applied', {
970
+ messageHash,
971
+ invalidatedActionId: request.invalidatedActionId,
972
+ disputeId: request.resolution.disputeId,
973
+ blockCount: request.blockIds.length
974
+ });
975
+ } catch (err) {
976
+ // Tolerate (don't reset the stream); roll back the marker so a re-broadcast retries.
977
+ this.appliedInvalidations.delete(dedupKey);
978
+ log('cluster-member:consensus-invalidate-sink-error', { messageHash, dedupKey, error: (err as Error).message });
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Record the consensus commit cert for `actionId` into the injected {@link CommitCertificateSink},
984
+ * if one is wired. The cert is built from the agreed `record.commits` (the per-member `approve`
985
+ * commit signatures), forwarded UNCHANGED for reactivity to reuse. `signedPayload` is the exact
986
+ * commit-vote preimage those signatures were produced over (`utf8(commitHash + ":approve")`),
987
+ * computed by the caller from the same `record` *before* the synchronous commit — reactivity sets a
988
+ * notification's `digest` from it so a real threshold-verify over `digest` succeeds. Stays
989
+ * **synchronous** (no `await` here) so the cert is retained before `StorageRepo.commit` emits its
990
+ * change event. No-op (zero cost) when no sink is configured; a throwing sink is isolated + logged so
991
+ * it can never break consensus.
992
+ */
993
+ private captureCommitCert(record: ClusterRecord, actionId: ActionId, signedPayload: Uint8Array): void {
994
+ if (!this.onCommitCertificate) {
995
+ return;
996
+ }
997
+ const minSigs = Math.ceil(Object.keys(record.peers).length * this.superMajorityThreshold);
998
+ try {
999
+ this.onCommitCertificate(actionId, buildCommitCert(record, minSigs, signedPayload));
1000
+ } catch (err) {
1001
+ log('cluster-member:commit-cert-sink-error', { actionId, error: (err as Error).message });
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * After tolerating a "behind" commit divergence, pull the committed revision of
1007
+ * each block from a cohort peer that holds it and restore it locally. Best-effort:
1008
+ * a missing callback, an empty cohort, or a per-block failure/timeout is logged and
1009
+ * tolerated — never thrown, since a throw out of consensus execution resets the
1010
+ * cluster stream.
1011
+ */
1012
+ private async reconcileDivergentCommit(record: ClusterRecord, commit: CommitRequest): Promise<void> {
1013
+ if (!this.reconcileBlock) {
1014
+ log('cluster-member:consensus-commit-reconcile-skip', { messageHash: record.messageHash, reason: 'no-callback' });
1015
+ return;
1016
+ }
1017
+ const cohortPeerIds = Object.keys(record.peers).filter(id => id !== this.peerId.toString());
1018
+ if (cohortPeerIds.length === 0) {
1019
+ log('cluster-member:consensus-commit-reconcile-skip', { messageHash: record.messageHash, reason: 'no-cohort-peers' });
1020
+ return;
1021
+ }
1022
+ const committed: ActionRev = { actionId: commit.actionId, rev: commit.rev };
1023
+ await Promise.all(
1024
+ commit.blockIds.map(blockId => this.reconcileOneBlock(record.messageHash, blockId, committed, cohortPeerIds))
1025
+ );
1026
+ }
1027
+
1028
+ /** Reconcile a single block, bounding the awaited callback and swallowing failures. */
1029
+ private async reconcileOneBlock(messageHash: string, blockId: BlockId, committed: ActionRev, cohortPeerIds: string[]): Promise<void> {
1030
+ try {
1031
+ await this.withReconcileTimeout(this.reconcileBlock!(blockId, committed, cohortPeerIds), blockId);
1032
+ log('cluster-member:consensus-commit-reconciled', { messageHash, blockId, rev: committed.rev });
1033
+ } catch (err) {
1034
+ log('cluster-member:consensus-commit-reconcile-failed', {
1035
+ messageHash,
1036
+ blockId,
1037
+ rev: committed.rev,
1038
+ error: (err as Error).message
1039
+ });
1040
+ }
1041
+ }
1042
+
1043
+ /** Bound an awaited reconcile so a slow/unreachable cohort peer can't stall consensus. */
1044
+ private withReconcileTimeout<T>(promise: Promise<T>, blockId: BlockId): Promise<T> {
1045
+ let timer: NodeJS.Timeout | undefined;
1046
+ const timeout = new Promise<never>((_, reject) => {
1047
+ timer = setTimeout(
1048
+ () => reject(new Error(`reconcile for block ${blockId} timed out after ${ReconcileTimeoutMs}ms`)),
1049
+ ReconcileTimeoutMs
1050
+ );
1051
+ timer.unref();
1052
+ });
1053
+ return Promise.race([promise, timeout]).finally(() => {
1054
+ if (timer) clearTimeout(timer);
1055
+ });
1056
+ }
1057
+
718
1058
  private async handleRejection(_record: ClusterRecord): Promise<void> {
719
1059
  // Clean up any resources - will be cleared by shouldPersist = false in the main flow
720
1060
  }
@@ -867,6 +1207,10 @@ export class ClusterMember implements ICluster {
867
1207
  operation.commit.blockIds.forEach(id => blockIds.add(id));
868
1208
  } else if ('cancel' in operation) {
869
1209
  operation.cancel.actionRef.blockIds.forEach(id => blockIds.add(id));
1210
+ } else if ('invalidate' in operation) {
1211
+ // The invalidation writes compensating revisions to these blocks; surfacing them lets
1212
+ // conflict detection serialize a concurrent commit racing the invalidation on a block.
1213
+ operation.invalidate.blockIds.forEach(id => blockIds.add(id));
870
1214
  }
871
1215
  }
872
1216
 
@@ -939,6 +1283,12 @@ export class ClusterMember implements ICluster {
939
1283
  this.executedTransactions.delete(messageHash);
940
1284
  }
941
1285
  }
1286
+ // Prune old applied-invalidation dedup markers on the same TTL.
1287
+ for (const [dedupKey, appliedAt] of Array.from(this.appliedInvalidations.entries())) {
1288
+ if (appliedAt < expirationThreshold) {
1289
+ this.appliedInvalidations.delete(dedupKey);
1290
+ }
1291
+ }
942
1292
  this.stateStore?.pruneExecuted(expirationThreshold)
943
1293
  .catch(err => log('cluster-member:prune-executed-error', { error: (err as Error).message }));
944
1294
  }