@optimystic/db-p2p 0.24.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/{readme.md → README.md} +688 -668
  2. package/dist/src/cluster/block-transfer-service.d.ts +133 -10
  3. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  4. package/dist/src/cluster/block-transfer-service.js +155 -9
  5. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  6. package/dist/src/cluster/block-transfer.d.ts +67 -7
  7. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  8. package/dist/src/cluster/block-transfer.js +127 -24
  9. package/dist/src/cluster/block-transfer.js.map +1 -1
  10. package/dist/src/cluster/certified-claims.d.ts +170 -0
  11. package/dist/src/cluster/certified-claims.d.ts.map +1 -0
  12. package/dist/src/cluster/certified-claims.js +200 -0
  13. package/dist/src/cluster/certified-claims.js.map +1 -0
  14. package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
  15. package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
  16. package/dist/src/cluster/client-signature-verifier.js +33 -0
  17. package/dist/src/cluster/client-signature-verifier.js.map +1 -0
  18. package/dist/src/cluster/cluster-policy.d.ts +9 -3
  19. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  20. package/dist/src/cluster/cluster-policy.js +111 -27
  21. package/dist/src/cluster/cluster-policy.js.map +1 -1
  22. package/dist/src/cluster/cluster-repo.d.ts +140 -11
  23. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  24. package/dist/src/cluster/cluster-repo.js +485 -29
  25. package/dist/src/cluster/cluster-repo.js.map +1 -1
  26. package/dist/src/cluster/commit-proof.d.ts +134 -0
  27. package/dist/src/cluster/commit-proof.d.ts.map +1 -0
  28. package/dist/src/cluster/commit-proof.js +263 -0
  29. package/dist/src/cluster/commit-proof.js.map +1 -0
  30. package/dist/src/cluster/quorum-restore.d.ts +95 -8
  31. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  32. package/dist/src/cluster/quorum-restore.js +113 -14
  33. package/dist/src/cluster/quorum-restore.js.map +1 -1
  34. package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
  35. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  36. package/dist/src/cluster/rebalance-monitor.js +207 -5
  37. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  38. package/dist/src/cluster/reconcile-block.d.ts +27 -7
  39. package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
  40. package/dist/src/cluster/reconcile-block.js +179 -38
  41. package/dist/src/cluster/reconcile-block.js.map +1 -1
  42. package/dist/src/cluster/service.d.ts +13 -4
  43. package/dist/src/cluster/service.d.ts.map +1 -1
  44. package/dist/src/cluster/service.js +17 -18
  45. package/dist/src/cluster/service.js.map +1 -1
  46. package/dist/src/cluster/spread-on-churn.d.ts +8 -2
  47. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  48. package/dist/src/cluster/spread-on-churn.js +7 -7
  49. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  50. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  51. package/dist/src/cohort-topic/host.js +8 -7
  52. package/dist/src/cohort-topic/host.js.map +1 -1
  53. package/dist/src/cohort-topic/stream-util.d.ts +7 -6
  54. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  55. package/dist/src/cohort-topic/stream-util.js +14 -41
  56. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  57. package/dist/src/dispute/cascade.d.ts +7 -8
  58. package/dist/src/dispute/cascade.d.ts.map +1 -1
  59. package/dist/src/dispute/cascade.js +24 -7
  60. package/dist/src/dispute/cascade.js.map +1 -1
  61. package/dist/src/dispute/dispute-service.d.ts +5 -0
  62. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  63. package/dist/src/dispute/dispute-service.js +13 -3
  64. package/dist/src/dispute/dispute-service.js.map +1 -1
  65. package/dist/src/dispute/invalidation.d.ts +33 -15
  66. package/dist/src/dispute/invalidation.d.ts.map +1 -1
  67. package/dist/src/dispute/invalidation.js +127 -34
  68. package/dist/src/dispute/invalidation.js.map +1 -1
  69. package/dist/src/dispute/service.d.ts.map +1 -1
  70. package/dist/src/dispute/service.js +2 -1
  71. package/dist/src/dispute/service.js.map +1 -1
  72. package/dist/src/index.d.ts +7 -0
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +7 -0
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/libp2p-key-network.d.ts +14 -10
  77. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  78. package/dist/src/libp2p-key-network.js +28 -58
  79. package/dist/src/libp2p-key-network.js.map +1 -1
  80. package/dist/src/libp2p-node-base.d.ts +38 -7
  81. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  82. package/dist/src/libp2p-node-base.js +148 -63
  83. package/dist/src/libp2p-node-base.js.map +1 -1
  84. package/dist/src/network/open-protocol-stream.d.ts +56 -0
  85. package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
  86. package/dist/src/network/open-protocol-stream.js +60 -0
  87. package/dist/src/network/open-protocol-stream.js.map +1 -0
  88. package/dist/src/network/register-protocol-handler.d.ts +66 -0
  89. package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
  90. package/dist/src/network/register-protocol-handler.js +51 -0
  91. package/dist/src/network/register-protocol-handler.js.map +1 -0
  92. package/dist/src/peer-address-book.d.ts +60 -2
  93. package/dist/src/peer-address-book.d.ts.map +1 -1
  94. package/dist/src/peer-address-book.js +89 -1
  95. package/dist/src/peer-address-book.js.map +1 -1
  96. package/dist/src/pend-validation.d.ts +65 -0
  97. package/dist/src/pend-validation.d.ts.map +1 -0
  98. package/dist/src/pend-validation.js +69 -0
  99. package/dist/src/pend-validation.js.map +1 -0
  100. package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
  101. package/dist/src/reactivity/notify-transport.js +2 -1
  102. package/dist/src/reactivity/notify-transport.js.map +1 -1
  103. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
  104. package/dist/src/reactivity/push-state-gossip.js +2 -1
  105. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  106. package/dist/src/repo/cluster-coordinator.d.ts +32 -6
  107. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  108. package/dist/src/repo/cluster-coordinator.js +44 -10
  109. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  110. package/dist/src/repo/coordinator-repo.d.ts +181 -25
  111. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  112. package/dist/src/repo/coordinator-repo.js +650 -62
  113. package/dist/src/repo/coordinator-repo.js.map +1 -1
  114. package/dist/src/repo/served-repo-proxy.d.ts +30 -0
  115. package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
  116. package/dist/src/repo/served-repo-proxy.js +50 -0
  117. package/dist/src/repo/served-repo-proxy.js.map +1 -0
  118. package/dist/src/repo/service.d.ts +16 -1
  119. package/dist/src/repo/service.d.ts.map +1 -1
  120. package/dist/src/repo/service.js +18 -16
  121. package/dist/src/repo/service.js.map +1 -1
  122. package/dist/src/reputation/types.d.ts +3 -1
  123. package/dist/src/reputation/types.d.ts.map +1 -1
  124. package/dist/src/reputation/types.js +3 -1
  125. package/dist/src/reputation/types.js.map +1 -1
  126. package/dist/src/rn.d.ts +24 -2
  127. package/dist/src/rn.d.ts.map +1 -1
  128. package/dist/src/rn.js +33 -4
  129. package/dist/src/rn.js.map +1 -1
  130. package/dist/src/storage/block-archive.d.ts +153 -0
  131. package/dist/src/storage/block-archive.d.ts.map +1 -0
  132. package/dist/src/storage/block-archive.js +194 -0
  133. package/dist/src/storage/block-archive.js.map +1 -0
  134. package/dist/src/storage/block-latch.d.ts +92 -0
  135. package/dist/src/storage/block-latch.d.ts.map +1 -0
  136. package/dist/src/storage/block-latch.js +132 -0
  137. package/dist/src/storage/block-latch.js.map +1 -0
  138. package/dist/src/storage/block-storage.d.ts +169 -12
  139. package/dist/src/storage/block-storage.d.ts.map +1 -1
  140. package/dist/src/storage/block-storage.js +415 -152
  141. package/dist/src/storage/block-storage.js.map +1 -1
  142. package/dist/src/storage/cached-raw-storage.d.ts +5 -1
  143. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
  144. package/dist/src/storage/cached-raw-storage.js +16 -1
  145. package/dist/src/storage/cached-raw-storage.js.map +1 -1
  146. package/dist/src/storage/cached-store-driver.d.ts +14 -4
  147. package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
  148. package/dist/src/storage/cached-store-driver.js +39 -5
  149. package/dist/src/storage/cached-store-driver.js.map +1 -1
  150. package/dist/src/storage/i-block-storage.d.ts +70 -19
  151. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  152. package/dist/src/storage/i-block-storage.js +21 -1
  153. package/dist/src/storage/i-block-storage.js.map +1 -1
  154. package/dist/src/storage/i-raw-storage.d.ts +38 -0
  155. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  156. package/dist/src/storage/kv-raw-storage.d.ts +14 -8
  157. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
  158. package/dist/src/storage/kv-raw-storage.js +29 -9
  159. package/dist/src/storage/kv-raw-storage.js.map +1 -1
  160. package/dist/src/storage/memory-store-driver.d.ts +4 -1
  161. package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
  162. package/dist/src/storage/memory-store-driver.js +22 -2
  163. package/dist/src/storage/memory-store-driver.js.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +2 -2
  165. package/dist/src/storage/raw-store-driver.d.ts +44 -3
  166. package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
  167. package/dist/src/storage/shared-cache-pool.d.ts +60 -3
  168. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
  169. package/dist/src/storage/shared-cache-pool.js +68 -4
  170. package/dist/src/storage/shared-cache-pool.js.map +1 -1
  171. package/dist/src/storage/storage-repo.d.ts +204 -26
  172. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  173. package/dist/src/storage/storage-repo.js +422 -119
  174. package/dist/src/storage/storage-repo.js.map +1 -1
  175. package/dist/src/storage/store-identity.d.ts +21 -0
  176. package/dist/src/storage/store-identity.d.ts.map +1 -0
  177. package/dist/src/storage/store-identity.js +33 -0
  178. package/dist/src/storage/store-identity.js.map +1 -0
  179. package/dist/src/storage/struct.d.ts +15 -0
  180. package/dist/src/storage/struct.d.ts.map +1 -1
  181. package/dist/src/storage/with-read-cache.d.ts +92 -0
  182. package/dist/src/storage/with-read-cache.d.ts.map +1 -0
  183. package/dist/src/storage/with-read-cache.js +132 -0
  184. package/dist/src/storage/with-read-cache.js.map +1 -0
  185. package/dist/src/sync/service.d.ts +13 -3
  186. package/dist/src/sync/service.d.ts.map +1 -1
  187. package/dist/src/sync/service.js +7 -30
  188. package/dist/src/sync/service.js.map +1 -1
  189. package/dist/src/testing/mesh-harness.d.ts +75 -2
  190. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  191. package/dist/src/testing/mesh-harness.js +193 -81
  192. package/dist/src/testing/mesh-harness.js.map +1 -1
  193. package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
  194. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  195. package/dist/src/testing/raw-storage-conformance.js +97 -2
  196. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  197. package/package.json +5 -6
  198. package/src/cluster/block-transfer-service.ts +528 -312
  199. package/src/cluster/block-transfer.ts +547 -424
  200. package/src/cluster/certified-claims.ts +319 -0
  201. package/src/cluster/client-signature-verifier.ts +34 -0
  202. package/src/cluster/cluster-policy.ts +294 -203
  203. package/src/cluster/cluster-repo.ts +2501 -2034
  204. package/src/cluster/commit-proof.ts +344 -0
  205. package/src/cluster/quorum-restore.ts +348 -223
  206. package/src/cluster/rebalance-monitor.ts +539 -254
  207. package/src/cluster/reconcile-block.ts +404 -203
  208. package/src/cluster/service.ts +23 -20
  209. package/src/cluster/spread-on-churn.ts +352 -346
  210. package/src/cohort-topic/host.ts +8 -7
  211. package/src/cohort-topic/stream-util.ts +14 -43
  212. package/src/dispute/cascade.ts +31 -15
  213. package/src/dispute/dispute-service.ts +706 -696
  214. package/src/dispute/invalidation.ts +162 -52
  215. package/src/dispute/service.ts +126 -125
  216. package/src/index.ts +7 -0
  217. package/src/libp2p-key-network.ts +1204 -1235
  218. package/src/libp2p-node-base.ts +185 -70
  219. package/src/network/open-protocol-stream.ts +87 -0
  220. package/src/network/register-protocol-handler.ts +85 -0
  221. package/src/peer-address-book.ts +96 -2
  222. package/src/pend-validation.ts +89 -0
  223. package/src/reactivity/notify-transport.ts +2 -1
  224. package/src/reactivity/push-state-gossip.ts +2 -1
  225. package/src/repo/cluster-coordinator.ts +65 -11
  226. package/src/repo/coordinator-repo.ts +729 -71
  227. package/src/repo/served-repo-proxy.ts +55 -0
  228. package/src/repo/service.ts +25 -16
  229. package/src/reputation/types.ts +3 -1
  230. package/src/rn.ts +34 -4
  231. package/src/storage/block-archive.ts +242 -0
  232. package/src/storage/block-latch.ts +144 -0
  233. package/src/storage/block-storage.ts +787 -498
  234. package/src/storage/cached-raw-storage.ts +21 -1
  235. package/src/storage/cached-store-driver.ts +898 -859
  236. package/src/storage/i-block-storage.ts +172 -116
  237. package/src/storage/i-raw-storage.ts +40 -0
  238. package/src/storage/kv-raw-storage.ts +34 -9
  239. package/src/storage/memory-store-driver.ts +26 -2
  240. package/src/storage/raw-store-codec.ts +42 -42
  241. package/src/storage/raw-store-driver.ts +45 -3
  242. package/src/storage/shared-cache-pool.ts +516 -452
  243. package/src/storage/storage-repo.ts +1421 -1014
  244. package/src/storage/store-identity.ts +41 -0
  245. package/src/storage/struct.ts +52 -34
  246. package/src/storage/with-read-cache.ts +182 -0
  247. package/src/sync/service.ts +178 -198
  248. package/src/testing/mesh-harness.ts +524 -344
  249. package/src/testing/raw-storage-conformance.ts +114 -2
@@ -25,11 +25,12 @@ export const MAX_MERGED_ADDRS_PER_PEER = 8
25
25
  */
26
26
  export const MAX_LEARNED_PEERS_PER_RECORD = 64
27
27
 
28
- /** The narrow slice of libp2p this module needs — a peer id and (optionally) a peerStore writer. */
28
+ /** The narrow slice of libp2p this module needs — a peer id and (optionally) a peerStore. */
29
29
  export interface PeerAddressBookHost {
30
30
  peerId: PeerId
31
31
  peerStore?: {
32
32
  merge?: (id: PeerId, data: { multiaddrs: Multiaddr[] }) => Promise<unknown>
33
+ get?: (id: PeerId) => Promise<{ addresses?: Array<{ multiaddr: { toString(): string } }> }>
33
34
  }
34
35
  }
35
36
 
@@ -91,7 +92,25 @@ export interface DirectionalConnection {
91
92
  * that merges it. So the cure has to be here, at the producer.
92
93
  *
93
94
  * An inbound-only peer loses nothing by this: its own advertised addresses reach us through
94
- * `identify`/`identifyPush` and are published from the peerStore instead.
95
+ * `identify`/`identifyPush` and are published from the peerStore instead — see
96
+ * {@link publishableAddrsForPeer}, which is where the two halves are joined.
97
+ *
98
+ * NOTE: accepted tradeoff — making an INBOUND connection's `remoteAddr` publishable when it is a
99
+ * circuit address was proposed (the "at least a relayed dialer has a real address" reading) and
100
+ * declined. Read from `@libp2p/circuit-relay-v2@4.1.3` as vendored under
101
+ * `packages/db-p2p/node_modules`: the destination side composes that address as
102
+ * `ourConnectionToTheRelay.remoteAddr` encapsulated with `/p2p-circuit/p2p/<dialer>`
103
+ * (`dist/src/transport/index.js:272`), so the relay it names is the one WE hold a reservation
104
+ * with, not one the dialer is reachable on. A relay's `handleConnect` requires a reservation for
105
+ * the DESTINATION only (`dist/src/server/index.js:219-222`, status `NO_RESERVATION`) — a dialer
106
+ * needs none — so a third party dialing that composed address reaches the dialer only if the
107
+ * dialer coincidentally also holds a reservation on our relay, which nothing establishes. When our
108
+ * own hop to the relay was itself inbound the prefix is an ephemeral source socket, making it
109
+ * undialable twice over. And in the one case where it would work — dialer and we share a relay —
110
+ * the dialer's genuine self-advertised circuit address has already reached us through `identify`,
111
+ * so publishing the composed form adds nothing and costs a slot against
112
+ * {@link MAX_MERGED_ADDRS_PER_PEER}. Revisit only on a MEASURED case where a peer's genuine
113
+ * circuit address reaches a third party by no other route.
95
114
  */
96
115
  export function publishableConnectionAddr(conn: DirectionalConnection, log: AddressLog): string | undefined {
97
116
  if (conn.direction !== 'outbound') return undefined
@@ -100,6 +119,81 @@ export function publishableConnectionAddr(conn: DirectionalConnection, log: Addr
100
119
  return isCarriableMultiaddrString(addr, log) ? addr : undefined
101
120
  }
102
121
 
122
+ /**
123
+ * Join the two sources of a third-party-publishable address set: the publishable half of our live
124
+ * connections, then the peer's own advertised addresses. De-duplicated, **connection-first**.
125
+ *
126
+ * The ordering is not cosmetic. An address that reaches `connectionAddrs` is one we OUTBOUND-dialed,
127
+ * so libp2p has just succeeded with it; an advertised address is one we have never tried. So the
128
+ * proven one goes first, and the recipient — which caps what it merges at
129
+ * {@link MAX_MERGED_ADDRS_PER_PEER} — keeps the proven ones when it truncates.
130
+ *
131
+ * `connectionAddrs` are already validated by {@link publishableConnectionAddr}; the advertised half
132
+ * arrives from a peerStore or a record and is put through {@link validMultiaddrStrings} here, so the
133
+ * union is uniformly carriable regardless of which side an address came from.
134
+ *
135
+ * Split out from {@link publishableAddrsForPeer} for the one caller that has already read the
136
+ * peerStore for other reasons (`findCluster`'s membership-scoped path reads protocols and addresses
137
+ * in a single `store.get` per member) and must not pay a second read to reuse the rule.
138
+ */
139
+ export function unionPublishableAddrs(connectionAddrs: string[], advertisedAddrs: string[], log: AddressLog): string[] {
140
+ return Array.from(new Set([...connectionAddrs, ...validMultiaddrStrings(advertisedAddrs, log)]))
141
+ }
142
+
143
+ /**
144
+ * Every address we may hand a **third** party for `peerId`.
145
+ *
146
+ * The single answer to that question: `findCluster` (via {@link unionPublishableAddrs}) and all
147
+ * three redirect-address resolvers — `RepoService.getPeerAddrs`, `ClusterService.getPeerAddrs`, and
148
+ * the `getConnectionAddrs` the node wires into the cluster service — go through this one rule.
149
+ * They used to answer it two different ways, and the connections-only half was the weaker one: a
150
+ * cohort member that only ever dialed US and is reachable only through a relay has its real circuit
151
+ * address in exactly one place — the peerStore, where `identify`/`identifyPush` put it — so a
152
+ * redirect built from connections alone described it as having no address at all.
153
+ *
154
+ * A peerStore read that fails or finds nothing yields the connection-derived half rather than
155
+ * throwing: a redirect carrying half the answer is strictly better than a redirect that errors.
156
+ */
157
+ export async function publishableAddrsForPeer(
158
+ host: PeerAddressBookHost,
159
+ connections: DirectionalConnection[],
160
+ peerId: PeerId,
161
+ log: AddressLog
162
+ ): Promise<string[]> {
163
+ const connectionAddrs: string[] = []
164
+ for (const conn of connections) {
165
+ const addr = publishableConnectionAddr(conn, log)
166
+ if (addr !== undefined) connectionAddrs.push(addr)
167
+ }
168
+ return unionPublishableAddrs(connectionAddrs, await advertisedAddrsForPeer(host, peerId, log), log)
169
+ }
170
+
171
+ /**
172
+ * The addresses `peerId` has advertised to us, as libp2p's peerStore holds them.
173
+ *
174
+ * A miss is the common case, not an anomaly — libp2p's `peerStore.get` THROWS for a peer it has no
175
+ * record of, and a redirect routinely names cohort members we have never met — so this logs under
176
+ * the ordinary `peer-address-book:*` tag family rather than `WARN:`, which is reserved for input we
177
+ * were handed and rejected.
178
+ *
179
+ * NOTE: this adds one `peerStore.get` per redirect target where the redirect resolvers previously
180
+ * did none. A redirect names at most `clusterSize` peers — single digits — and only fires when this
181
+ * node is NOT responsible for the key, so the reads are bounded and off the hot path. Unmeasured;
182
+ * if redirect volume ever shows up in a profile, batch the reads per payload (they are already
183
+ * issued concurrently by `Promise.all` at both call sites) rather than dropping the peerStore arm.
184
+ */
185
+ async function advertisedAddrsForPeer(host: PeerAddressBookHost, peerId: PeerId, log: AddressLog): Promise<string[]> {
186
+ const get = host.peerStore?.get
187
+ if (typeof get !== 'function') return []
188
+ try {
189
+ const peer = await get.call(host.peerStore, peerId)
190
+ return (peer?.addresses ?? []).map(a => a.multiaddr.toString())
191
+ } catch (err) {
192
+ log('peer-address-book:peerstore-miss peer=%s %o', peerId.toString().substring(0, 12), err)
193
+ return []
194
+ }
195
+ }
196
+
103
197
  /**
104
198
  * How useful the addresses we hold for a peer are **to this node's own dialer**.
105
199
  *
@@ -0,0 +1,89 @@
1
+ import type { PendRequest, PendValidationResult, UnvalidatablePendPolicy } from "@optimystic/db-core";
2
+
3
+ /**
4
+ * Stable, greppable prefix on the failure reason a validating receiver emits when it refuses a pend
5
+ * that carries no `validation` payload (nothing to re-execute — the single-collection
6
+ * `Collection.sync` shape) under the fail-closed `unvalidatablePendPolicy: 'reject'`.
7
+ */
8
+ export const PEND_NOT_VALIDATABLE = 'pend-not-validatable';
9
+
10
+ /**
11
+ * Stable, greppable prefix on the failure reason a validating receiver emits when the checker
12
+ * itself THREW (engine fault, missing table, parse error) rather than returning a verdict. Distinct
13
+ * from a content verdict on purpose: an operator reading a reject reason can tell "this transaction
14
+ * is wrong" from "this node could not tell".
15
+ */
16
+ export const VALIDATOR_FAULT = 'validator-fault';
17
+
18
+ /**
19
+ * One re-check attempt: hand the pend's `validation` pair to whatever checker this tier holds
20
+ * (a `ClusterMember`'s `ITransactionValidator`, a `StorageRepo`'s `validatePend` hook) and
21
+ * return its verdict. Throwing is allowed — {@link checkPendValidation} converts a throw into a
22
+ * {@link VALIDATOR_FAULT} rejection.
23
+ */
24
+ export type PendChecker = (validation: NonNullable<PendRequest['validation']>) => Promise<PendValidationResult>;
25
+
26
+ /** Structured trace of the decision this helper took, rendered by whichever tier called it. */
27
+ export type PendValidationEvent =
28
+ | { kind: 'unvalidatable'; policy: UnvalidatablePendPolicy }
29
+ | { kind: 'validator-fault'; error: string };
30
+
31
+ /**
32
+ * The fail-closed pend re-check, shared by BOTH tiers that hold a checker — `ClusterMember`
33
+ * (voting on a cluster record) and `StorageRepo` (applying a pend locally). One implementation
34
+ * rather than two mirrored copies, because the two tiers refusing with *different* prefixes or
35
+ * *different* policy semantics is a silent inconsistency nothing would catch: they are read by the
36
+ * same operator, and a member that votes approve while its own storage refuses at apply burns a
37
+ * consensus round for nothing.
38
+ *
39
+ * Two decisions live here:
40
+ *
41
+ * - **No `validation` pair** (the single-collection `Collection.sync` shape — bare transforms, so
42
+ * there is nothing to re-execute): an explicit, LOGGED policy branch on both arms, never a silent
43
+ * fall-through. `'accept'` admits it unchecked, preserving the historical behaviour; `'reject'`
44
+ * refuses with {@link PEND_NOT_VALIDATABLE}. The presence test is on the whole pair, so a sender
45
+ * cannot talk a receiver out of validating by omitting half of it.
46
+ * - **A checker that throws**: caught and turned into a {@link VALIDATOR_FAULT} rejection, never an
47
+ * escaping error. At the cluster tier an escaping throw costs the member its vote entirely —
48
+ * indistinguishable from an unreachable peer, and with no signed reason for the dispute path.
49
+ *
50
+ * NOTE: a TRANSIENT checker fault (database busy, momentary connection loss) therefore produces a
51
+ * terminal reject where a redelivery might have produced an approve. `CoordinatorRepo`'s two
52
+ * rejection classifiers confirm retryability against LOCAL storage state and never read this prose,
53
+ * so a validator-fault reject is returned as retryable only when local state independently shows a
54
+ * stale revision or a rival pending — otherwise it reaches the writer as a throw. If transient
55
+ * validator faults ever show up in practice, give the classifier an arm keyed on this prefix rather
56
+ * than reverting to a silent pass or a lost vote.
57
+ *
58
+ * @param request the pend under consideration
59
+ * @param check the tier's checker, or undefined when this node re-validates nothing (then every
60
+ * pend passes and the policy is irrelevant — a storage-only node)
61
+ * @param policy what to do with a pend that carries no `validation` pair
62
+ * @param onEvent trace sink; called on every decision this helper takes, so the two tiers log the
63
+ * same facts in their own formats
64
+ */
65
+ export async function checkPendValidation(
66
+ request: PendRequest,
67
+ check: PendChecker | undefined,
68
+ policy: UnvalidatablePendPolicy,
69
+ onEvent: (event: PendValidationEvent) => void
70
+ ): Promise<PendValidationResult> {
71
+ if (!check) {
72
+ return { valid: true };
73
+ }
74
+ if (!request.validation) {
75
+ onEvent({ kind: 'unvalidatable', policy });
76
+ return policy === 'reject'
77
+ // Plain prose after the stable prefix: at the cluster tier this reason is fed to
78
+ // computeSigningPayload and carried as Signature.rejectReason, so it must stay a string.
79
+ ? { valid: false, reason: `${PEND_NOT_VALIDATABLE}: pend carries no transaction to re-execute` }
80
+ : { valid: true };
81
+ }
82
+ try {
83
+ return await check(request.validation);
84
+ } catch (err) {
85
+ const error = (err as Error).message;
86
+ onEvent({ kind: 'validator-fault', error });
87
+ return { valid: false, reason: `${VALIDATOR_FAULT}: ${error}` };
88
+ }
89
+ }
@@ -27,6 +27,7 @@ import { readFramed } from "p2p-fret";
27
27
  import { peerIdToBytes } from "../cohort-topic/peer-codec.js";
28
28
  import { sendOneWay, DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
29
29
  import { PROTOCOL_REACTIVITY_NOTIFY } from "./protocols.js";
30
+ import { registerProtocolHandler } from "../network/register-protocol-handler.js";
30
31
  import { createLogger } from "../logger.js";
31
32
 
32
33
  const log = createLogger("reactivity-notify");
@@ -126,7 +127,7 @@ export function registerNotifyHandler(
126
127
  transport: ReactivityNotifyTransport,
127
128
  maxBytes = DEFAULT_STREAM_MAX_BYTES,
128
129
  ): void {
129
- void node.handle(protocol, (stream: Stream, connection: Connection) => {
130
+ void registerProtocolHandler(node, protocol, (stream: Stream, connection: Connection) => {
130
131
  void (async (): Promise<void> => {
131
132
  try {
132
133
  const frame = await readFramed(stream, maxBytes);
@@ -45,6 +45,7 @@ import type { FretCohortGossipTransport } from "../cohort-topic/cohort-gossip-tr
45
45
  import { DEFAULT_GOSSIP_INTERVAL_MS } from "../cohort-topic/cohort-gossip-driver.js";
46
46
  import { DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
47
47
  import { PROTOCOL_REACTIVITY_PUSH_STATE_GOSSIP } from "./protocols.js";
48
+ import { registerProtocolHandler } from "../network/register-protocol-handler.js";
48
49
  import { createLogger } from "../logger.js";
49
50
 
50
51
  const log = createLogger("reactivity-push-state-gossip");
@@ -273,7 +274,7 @@ export function registerPushStateGossipHandler(
273
274
  driver: Pick<ReactivityPushStateGossipDriver, "deliver">,
274
275
  maxBytes = DEFAULT_STREAM_MAX_BYTES,
275
276
  ): void {
276
- void node.handle(protocol, (stream: Stream, connection: Connection) => {
277
+ void registerProtocolHandler(node, protocol, (stream: Stream, connection: Connection) => {
277
278
  void (async (): Promise<void> => {
278
279
  try {
279
280
  const frame = await readFramed(stream, maxBytes);
@@ -1,5 +1,5 @@
1
1
  import { peerIdFromString } from "@libp2p/peer-id";
2
- import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig, ICluster } from "@optimystic/db-core";
2
+ import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig, ICluster, PendResult, CommitResult } from "@optimystic/db-core";
3
3
  import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, membershipDigest } from "@optimystic/db-core";
4
4
  import { Pending } from "@optimystic/db-core";
5
5
  import type { PeerId } from "@libp2p/interface";
@@ -37,10 +37,17 @@ export class ValidatorRejectionError extends Error {
37
37
  * vote (they hold a rival transaction that won the deterministic race on the same blocks) and
38
38
  * approvals fell short of super-majority. Distinct from {@link ValidatorRejectionError} — nobody
39
39
  * judged this write invalid; it lost an optimistic-concurrency race and a fresh retry can win.
40
- * `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the normal
41
- * retry machinery (`isConflictFailure`) absorbs it; it should escape as a thrown error only from
42
- * paths other than pend. The conflicting peers and the winning hashes ride as structured data
43
- * (from the signed `conflictWith` fields), never parsed out of prose.
40
+ * `CoordinatorRepo.pend` AND `CoordinatorRepo.commit` both convert this into a `StaleFailure` with
41
+ * `conflict: true` so the normal retry machinery (`isConflictFailure`) absorbs it; it should escape
42
+ * as a thrown error only from other paths. The commit conversion matters as much as the pend one:
43
+ * at the moment this is thrown zero members approved and the members hold the winner — nothing of
44
+ * the loser landed — yet a THROWN commit error is retried verbatim by db-core's `commitCollection`
45
+ * (it treats throws as transport faults), and that re-driven commit races into the window after
46
+ * members apply the winner and clear its reservation, where it can assemble a consensus no member
47
+ * will durably store. A returned conflict is instead surfaced immediately as a stale loss, and the
48
+ * writer re-reads and re-drives the whole pend+commit at a fresh revision. The conflicting peers
49
+ * and the winning hashes ride as structured data (from the signed `conflictWith` fields), never
50
+ * parsed out of prose.
44
51
  */
45
52
  export class ConflictRaceLostError extends Error {
46
53
  constructor(
@@ -123,6 +130,10 @@ export class ClusterCoordinator {
123
130
  update: (record: ClusterRecord) => Promise<ClusterRecord>;
124
131
  peerId: PeerId;
125
132
  wasTransactionExecuted?: (messageHash: string) => boolean;
133
+ /** Local storage's verdict for a pend applied during consensus; see ClusterMember.getExecutedPendResult. */
134
+ getExecutedPendResult?: (messageHash: string) => PendResult | undefined;
135
+ /** Local storage's verdict for a commit applied during consensus; see ClusterMember.getExecutedCommitResult. */
136
+ getExecutedCommitResult?: (messageHash: string) => CommitResult | undefined;
126
137
  },
127
138
  private readonly fretService?: FretService,
128
139
  private readonly reputation?: IPeerReputation,
@@ -183,7 +194,7 @@ export class ClusterCoordinator {
183
194
  * reproduces the legacy v1 hash byte-for-byte.
184
195
  *
185
196
  * NOTE: the whole `message` is hashed (canonicalJson), so a transaction's advisory aged priority —
186
- * which rides inside the pend operation as `pend.transaction.priority` (multi-collection) or
197
+ * which rides inside the pend operation as `pend.validation.transaction.priority` (multi-collection) or
187
198
  * `pend.priority` (single-collection) — is automatically covered here and by the derived
188
199
  * promise/commit hashes. That is what makes priority integrity-protected in transit: a relaying peer
189
200
  * cannot strip or inflate it without invalidating the message hash the members verify. No separate
@@ -219,7 +230,6 @@ export class ClusterCoordinator {
219
230
  membershipVersion: CURRENT_MEMBERSHIP_VERSION,
220
231
  membershipDigest: membershipDigestValue,
221
232
  message,
222
- coordinatingBlockIds: message.coordinatingBlockIds,
223
233
  promises: {},
224
234
  commits: {},
225
235
  suggestedClusterSize: peerCount || undefined,
@@ -249,7 +259,41 @@ export class ClusterCoordinator {
249
259
  async executeClusterTransaction(blockId: BlockId, message: RepoMessage, _options?: MessageOptions): Promise<{
250
260
  record: ClusterRecord;
251
261
  localExecuted: boolean;
262
+ /**
263
+ * Local storage's verdict for a pend operation this node's own cluster member applied during
264
+ * consensus, when the member retained one. Meaningful only when `localExecuted` is true;
265
+ * absent for non-pend messages, for a member that predates the retention, or after the
266
+ * retention TTL. `CoordinatorRepo.pend` returns this instead of fabricating a success.
267
+ */
268
+ localPendResult?: PendResult;
269
+ /**
270
+ * Local storage's verdict for a commit operation this node's own cluster member applied
271
+ * during consensus, when the member retained one. Same availability contract as
272
+ * `localPendResult`. `CoordinatorRepo.commit` uses a retained refusal to detect a rival's
273
+ * win swallowed by the member-side ahead-divergence tolerance, instead of fabricating a
274
+ * success no member durably stored.
275
+ */
276
+ localCommitResult?: CommitResult;
252
277
  }> {
278
+ // The coordinating block id is derived HERE, from the key this method is already handed, rather
279
+ // than being set by each caller's message builder: a member's membership admission gate derives
280
+ // its own cohort view from this field, and a builder that forgets it silently downgrades the gate
281
+ // to its fallback floor on that path (which is how `commit` and `cancel` used to strand writes —
282
+ // admitted at pend, refused at commit). Doing it at the single choke point means a future message
283
+ // builder cannot reintroduce the gap.
284
+ //
285
+ // Two constraints this shape exists to satisfy:
286
+ // - COPY, never mutate: `CoordinatorRepo.cancel` builds ONE message and hands the same object to
287
+ // N concurrent calls, one per block. In-place mutation would leak one block's id into another
288
+ // block's transaction.
289
+ // - Preserve an already-present list: `pend` deliberately declares the whole consolidated batch,
290
+ // not just its first block, so this must not overwrite it. Tested on `length`, not on the
291
+ // field: an empty list carries no id for a member to derive from, so preserving one would be
292
+ // the same silent downgrade to the fallback floor this choke point exists to prevent.
293
+ const coordinated: RepoMessage = message.coordinatingBlockIds?.length
294
+ ? message
295
+ : { ...message, coordinatingBlockIds: [blockId] };
296
+
253
297
  // Get the cluster peers for this block
254
298
  const peers = await this.getClusterForBlock(blockId);
255
299
 
@@ -258,11 +302,14 @@ export class ClusterCoordinator {
258
302
  // than one hash with a silent internal disagreement about who is responsible.
259
303
  const membershipDigestValue = await membershipDigest(peers);
260
304
 
261
- // Create a unique hash for this transaction (over message + membership digest)
262
- const messageHash = await this.createMessageHash(message, membershipDigestValue);
305
+ // Create a unique hash for this transaction (over message + membership digest). Hashing the
306
+ // coordinating-block-bearing copy is what makes the field tamper-evident in transit — and it also
307
+ // makes a multi-block `cancel` produce a distinct hash per block, where before two blocks with
308
+ // identical cohorts collided on one `messageHash` in `this.transactions` / `wasTransactionExecuted`.
309
+ const messageHash = await this.createMessageHash(coordinated, membershipDigestValue);
263
310
 
264
311
  // Create a cluster record for this transaction
265
- const record = this.makeRecord(peers, messageHash, message, membershipDigestValue);
312
+ const record = this.makeRecord(peers, messageHash, coordinated, membershipDigestValue);
266
313
  log('cluster-tx:start', {
267
314
  messageHash,
268
315
  blockId,
@@ -296,7 +343,14 @@ export class ClusterCoordinator {
296
343
  const result = await pending.result();
297
344
  // Check if the local cluster already executed the operations during consensus
298
345
  const localExecuted = this.localCluster?.wasTransactionExecuted?.(messageHash) ?? false;
299
- return { record: result, localExecuted };
346
+ const localPendResult = localExecuted ? this.localCluster?.getExecutedPendResult?.(messageHash) : undefined;
347
+ const localCommitResult = localExecuted ? this.localCluster?.getExecutedCommitResult?.(messageHash) : undefined;
348
+ return {
349
+ record: result,
350
+ localExecuted,
351
+ ...(localPendResult === undefined ? {} : { localPendResult }),
352
+ ...(localCommitResult === undefined ? {} : { localCommitResult })
353
+ };
300
354
  } finally {
301
355
  const stored = this.transactions.get(messageHash);
302
356
  const retrySnapshot = stored?.retry ? {