@optimystic/db-p2p 0.24.1 → 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 (257) 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 +21 -4
  43. package/dist/src/cluster/service.d.ts.map +1 -1
  44. package/dist/src/cluster/service.js +28 -17
  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 +78 -6
  77. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  78. package/dist/src/libp2p-key-network.js +136 -57
  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 +150 -62
  83. package/dist/src/libp2p-node-base.js.map +1 -1
  84. package/dist/src/logger.d.ts +2 -2
  85. package/dist/src/logger.js +2 -2
  86. package/dist/src/network/open-protocol-stream.d.ts +56 -0
  87. package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
  88. package/dist/src/network/open-protocol-stream.js +60 -0
  89. package/dist/src/network/open-protocol-stream.js.map +1 -0
  90. package/dist/src/network/register-protocol-handler.d.ts +66 -0
  91. package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
  92. package/dist/src/network/register-protocol-handler.js +51 -0
  93. package/dist/src/network/register-protocol-handler.js.map +1 -0
  94. package/dist/src/peer-address-book.d.ts +128 -1
  95. package/dist/src/peer-address-book.d.ts.map +1 -1
  96. package/dist/src/peer-address-book.js +198 -15
  97. package/dist/src/peer-address-book.js.map +1 -1
  98. package/dist/src/pend-validation.d.ts +65 -0
  99. package/dist/src/pend-validation.d.ts.map +1 -0
  100. package/dist/src/pend-validation.js +69 -0
  101. package/dist/src/pend-validation.js.map +1 -0
  102. package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
  103. package/dist/src/reactivity/notify-transport.js +2 -1
  104. package/dist/src/reactivity/notify-transport.js.map +1 -1
  105. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
  106. package/dist/src/reactivity/push-state-gossip.js +2 -1
  107. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  108. package/dist/src/repo/cluster-coordinator.d.ts +32 -6
  109. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  110. package/dist/src/repo/cluster-coordinator.js +44 -10
  111. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  112. package/dist/src/repo/coordinator-repo.d.ts +181 -25
  113. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  114. package/dist/src/repo/coordinator-repo.js +650 -62
  115. package/dist/src/repo/coordinator-repo.js.map +1 -1
  116. package/dist/src/repo/served-repo-proxy.d.ts +30 -0
  117. package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
  118. package/dist/src/repo/served-repo-proxy.js +50 -0
  119. package/dist/src/repo/served-repo-proxy.js.map +1 -0
  120. package/dist/src/repo/service.d.ts +22 -1
  121. package/dist/src/repo/service.d.ts.map +1 -1
  122. package/dist/src/repo/service.js +25 -13
  123. package/dist/src/repo/service.js.map +1 -1
  124. package/dist/src/reputation/types.d.ts +3 -1
  125. package/dist/src/reputation/types.d.ts.map +1 -1
  126. package/dist/src/reputation/types.js +3 -1
  127. package/dist/src/reputation/types.js.map +1 -1
  128. package/dist/src/rn.d.ts +24 -2
  129. package/dist/src/rn.d.ts.map +1 -1
  130. package/dist/src/rn.js +33 -4
  131. package/dist/src/rn.js.map +1 -1
  132. package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
  133. package/dist/src/routing/libp2p-known-peers.js +5 -0
  134. package/dist/src/routing/libp2p-known-peers.js.map +1 -1
  135. package/dist/src/storage/block-archive.d.ts +153 -0
  136. package/dist/src/storage/block-archive.d.ts.map +1 -0
  137. package/dist/src/storage/block-archive.js +194 -0
  138. package/dist/src/storage/block-archive.js.map +1 -0
  139. package/dist/src/storage/block-latch.d.ts +92 -0
  140. package/dist/src/storage/block-latch.d.ts.map +1 -0
  141. package/dist/src/storage/block-latch.js +132 -0
  142. package/dist/src/storage/block-latch.js.map +1 -0
  143. package/dist/src/storage/block-storage.d.ts +169 -12
  144. package/dist/src/storage/block-storage.d.ts.map +1 -1
  145. package/dist/src/storage/block-storage.js +415 -152
  146. package/dist/src/storage/block-storage.js.map +1 -1
  147. package/dist/src/storage/cached-raw-storage.d.ts +5 -1
  148. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
  149. package/dist/src/storage/cached-raw-storage.js +16 -1
  150. package/dist/src/storage/cached-raw-storage.js.map +1 -1
  151. package/dist/src/storage/cached-store-driver.d.ts +14 -4
  152. package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
  153. package/dist/src/storage/cached-store-driver.js +39 -5
  154. package/dist/src/storage/cached-store-driver.js.map +1 -1
  155. package/dist/src/storage/i-block-storage.d.ts +70 -19
  156. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  157. package/dist/src/storage/i-block-storage.js +21 -1
  158. package/dist/src/storage/i-block-storage.js.map +1 -1
  159. package/dist/src/storage/i-raw-storage.d.ts +38 -0
  160. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  161. package/dist/src/storage/kv-raw-storage.d.ts +14 -8
  162. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
  163. package/dist/src/storage/kv-raw-storage.js +29 -9
  164. package/dist/src/storage/kv-raw-storage.js.map +1 -1
  165. package/dist/src/storage/memory-store-driver.d.ts +4 -1
  166. package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
  167. package/dist/src/storage/memory-store-driver.js +22 -2
  168. package/dist/src/storage/memory-store-driver.js.map +1 -1
  169. package/dist/src/storage/raw-store-codec.js +2 -2
  170. package/dist/src/storage/raw-store-driver.d.ts +44 -3
  171. package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
  172. package/dist/src/storage/shared-cache-pool.d.ts +60 -3
  173. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
  174. package/dist/src/storage/shared-cache-pool.js +68 -4
  175. package/dist/src/storage/shared-cache-pool.js.map +1 -1
  176. package/dist/src/storage/storage-repo.d.ts +204 -26
  177. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  178. package/dist/src/storage/storage-repo.js +422 -119
  179. package/dist/src/storage/storage-repo.js.map +1 -1
  180. package/dist/src/storage/store-identity.d.ts +21 -0
  181. package/dist/src/storage/store-identity.d.ts.map +1 -0
  182. package/dist/src/storage/store-identity.js +33 -0
  183. package/dist/src/storage/store-identity.js.map +1 -0
  184. package/dist/src/storage/struct.d.ts +15 -0
  185. package/dist/src/storage/struct.d.ts.map +1 -1
  186. package/dist/src/storage/with-read-cache.d.ts +92 -0
  187. package/dist/src/storage/with-read-cache.d.ts.map +1 -0
  188. package/dist/src/storage/with-read-cache.js +132 -0
  189. package/dist/src/storage/with-read-cache.js.map +1 -0
  190. package/dist/src/sync/service.d.ts +13 -3
  191. package/dist/src/sync/service.d.ts.map +1 -1
  192. package/dist/src/sync/service.js +7 -30
  193. package/dist/src/sync/service.js.map +1 -1
  194. package/dist/src/testing/mesh-harness.d.ts +75 -2
  195. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  196. package/dist/src/testing/mesh-harness.js +193 -81
  197. package/dist/src/testing/mesh-harness.js.map +1 -1
  198. package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
  199. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  200. package/dist/src/testing/raw-storage-conformance.js +97 -2
  201. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  202. package/package.json +5 -6
  203. package/src/cluster/block-transfer-service.ts +528 -312
  204. package/src/cluster/block-transfer.ts +547 -424
  205. package/src/cluster/certified-claims.ts +319 -0
  206. package/src/cluster/client-signature-verifier.ts +34 -0
  207. package/src/cluster/cluster-policy.ts +294 -203
  208. package/src/cluster/cluster-repo.ts +2501 -2034
  209. package/src/cluster/commit-proof.ts +344 -0
  210. package/src/cluster/quorum-restore.ts +348 -223
  211. package/src/cluster/rebalance-monitor.ts +539 -254
  212. package/src/cluster/reconcile-block.ts +404 -203
  213. package/src/cluster/service.ts +308 -293
  214. package/src/cluster/spread-on-churn.ts +352 -346
  215. package/src/cohort-topic/host.ts +8 -7
  216. package/src/cohort-topic/stream-util.ts +14 -43
  217. package/src/dispute/cascade.ts +31 -15
  218. package/src/dispute/dispute-service.ts +706 -696
  219. package/src/dispute/invalidation.ts +162 -52
  220. package/src/dispute/service.ts +126 -125
  221. package/src/index.ts +7 -0
  222. package/src/libp2p-key-network.ts +142 -58
  223. package/src/libp2p-node-base.ts +1793 -1675
  224. package/src/logger.ts +27 -27
  225. package/src/network/open-protocol-stream.ts +87 -0
  226. package/src/network/register-protocol-handler.ts +85 -0
  227. package/src/peer-address-book.ts +360 -149
  228. package/src/pend-validation.ts +89 -0
  229. package/src/reactivity/notify-transport.ts +145 -144
  230. package/src/reactivity/push-state-gossip.ts +292 -291
  231. package/src/repo/cluster-coordinator.ts +65 -11
  232. package/src/repo/coordinator-repo.ts +729 -71
  233. package/src/repo/served-repo-proxy.ts +55 -0
  234. package/src/repo/service.ts +332 -313
  235. package/src/reputation/types.ts +3 -1
  236. package/src/rn.ts +34 -4
  237. package/src/routing/libp2p-known-peers.ts +31 -26
  238. package/src/storage/block-archive.ts +242 -0
  239. package/src/storage/block-latch.ts +144 -0
  240. package/src/storage/block-storage.ts +787 -498
  241. package/src/storage/cached-raw-storage.ts +21 -1
  242. package/src/storage/cached-store-driver.ts +898 -859
  243. package/src/storage/i-block-storage.ts +172 -116
  244. package/src/storage/i-raw-storage.ts +40 -0
  245. package/src/storage/kv-raw-storage.ts +34 -9
  246. package/src/storage/memory-store-driver.ts +26 -2
  247. package/src/storage/raw-store-codec.ts +42 -42
  248. package/src/storage/raw-store-driver.ts +45 -3
  249. package/src/storage/shared-cache-pool.ts +516 -452
  250. package/src/storage/storage-repo.ts +1421 -1014
  251. package/src/storage/store-identity.ts +41 -0
  252. package/src/storage/struct.ts +52 -34
  253. package/src/storage/with-read-cache.ts +182 -0
  254. package/src/sync/service.ts +178 -198
  255. package/src/testing/cohort-topic-mesh-harness.ts +673 -673
  256. package/src/testing/mesh-harness.ts +524 -344
  257. package/src/testing/raw-storage-conformance.ts +114 -2
@@ -20,7 +20,9 @@ export enum PenaltyReason {
20
20
  FalseApproval = 'false-approval',
21
21
  /** Challenger lost a dispute (their rejection was wrong) */
22
22
  DisputeLost = 'dispute-lost',
23
- /** Peer reported a block latest (rev/actionId) or served content contradicted by a cluster quorum during restoration */
23
+ /** Peer served provably-bad restoration evidence: a latest claim naming a different action at the
24
+ * quorum-agreed revision, served content contradicted by a cluster quorum, or a cohort commit
25
+ * proof that provably lies or does not cover the claim it was attached to (replay) */
24
26
  InvalidRestoration = 'invalid-restoration',
25
27
  }
26
28
 
package/src/rn.ts CHANGED
@@ -1,23 +1,46 @@
1
+ // React Native / browser entry point. `package.json` routes the `react-native` condition on `.`
2
+ // here, plus the explicit `./rn` subpath.
3
+ //
4
+ // NOTE: this entry re-exports exactly the same module set as `./index.js`, with
5
+ // `./libp2p-node.js` -> `./libp2p-node-rn.js` as the single substitution. Because the module set
6
+ // is identical, a class obtained from either entry is the *same* class object on Node, so
7
+ // `instanceof` holds across them; downstream code (e.g. sereus' `cached-storage.ts`) relies on
8
+ // that. Enforced from this side by `test/entry-parity.spec.ts` — keep the two files in step, and
9
+ // keep every line here a plain `export * from '...'` so that spec can compare them.
10
+
1
11
  export * from './cluster/client.js';
2
- export * from './cluster/cluster-policy.js';
12
+ export * from './cluster/client-signature-verifier.js';
3
13
  export * from './cluster/cluster-repo.js';
14
+ export * from './cluster/cluster-policy.js';
15
+ export * from './cluster/commit-cert.js';
16
+ export * from './cluster/commit-proof.js';
4
17
  export * from './cluster/service.js';
18
+ export * from './cluster/rebalance-monitor.js';
19
+ export * from './cluster/spread-on-churn.js';
20
+ export * from './cluster/block-transfer.js';
21
+ export * from './cluster/block-transfer-service.js';
22
+ export * from './inbound-authorization.js';
5
23
  export * from './protocol-client.js';
6
24
  export * from './repo/client.js';
7
25
  export * from './repo/cluster-coordinator.js';
8
26
  export * from './repo/coordinator-repo.js';
27
+ export * from './repo/served-repo-proxy.js';
9
28
  export * from './repo/service.js';
10
29
  export * from './storage/block-storage.js';
30
+ export * from './storage/block-latch.js';
31
+ export * from './storage/store-identity.js';
11
32
  export * from './storage/raw-store-driver.js';
12
33
  export * from './storage/kv-raw-storage.js';
13
34
  export * from './storage/shared-cache-pool.js';
14
35
  export * from './storage/cached-store-driver.js';
15
36
  export * from './storage/cached-raw-storage.js';
37
+ export * from './storage/with-read-cache.js';
16
38
  export * from './storage/memory-store-driver.js';
17
39
  export * from './storage/memory-storage.js';
18
40
  export * from './storage/i-block-storage.js';
19
41
  export * from './storage/i-raw-storage.js';
20
42
  export * from './storage/struct.js';
43
+ export * from './storage/block-archive.js';
21
44
  export * from './storage/storage-repo.js';
22
45
  export * from './storage/restoration-coordinator.js';
23
46
  export * from './storage/ring-selector.js';
@@ -34,6 +57,13 @@ export * from './routing/responsibility.js';
34
57
  export * from './routing/libp2p-known-peers.js';
35
58
  export * from './network/network-manager-service.js';
36
59
  export * from './network/get-network-manager.js';
37
- // Browser-safe peer signing seam. The rest of ./cohort-topic pulls node-heavy host.js,
38
- // so only peer-sig (@noble/curves + peer-id) is surfaced through the RN/browser entry.
39
- export * from './cohort-topic/peer-sig.js';
60
+ export * from './reputation/index.js';
61
+ export * from './dispute/index.js';
62
+ export * from './cohort-topic/index.js';
63
+ export * from './matchmaking/index.js';
64
+ export * from './reactivity/index.js';
65
+ export * from './cluster/i-transaction-state-store.js';
66
+ export * from './cluster/memory-transaction-state-store.js';
67
+ export * from './cluster/persistent-transaction-state-store.js';
68
+ export * from './storage/i-kv-store.js';
69
+ export * from './storage/memory-kv-store.js';
@@ -1,26 +1,31 @@
1
- import type { Libp2p } from 'libp2p'
2
- import type { PeerId } from '@libp2p/interface'
3
- import type { KnownPeer } from './responsibility.js'
4
-
5
- export function buildKnownPeers(libp2p: Libp2p): KnownPeer[] {
6
- const self: KnownPeer = {
7
- id: libp2p.peerId as unknown as PeerId,
8
- addrs: libp2p.getMultiaddrs().map(ma => ma.toString())
9
- }
10
-
11
- const connections = libp2p.getConnections()
12
- const byPeer: Record<string, { id: PeerId, addrs: Set<string> }> = {}
13
-
14
- for (const c of connections) {
15
- const pid = c.remotePeer
16
- const key = pid.toString()
17
- const entry = byPeer[key] ?? (byPeer[key] = { id: pid as unknown as PeerId, addrs: new Set() })
18
- const addrStr = c.remoteAddr?.toString?.()
19
- if (addrStr) entry.addrs.add(addrStr)
20
- }
21
-
22
- const others: KnownPeer[] = Object.values(byPeer).map(e => ({ id: e.id, addrs: Array.from(e.addrs) }))
23
- return [self, ...others]
24
- }
25
-
26
-
1
+ import type { Libp2p } from 'libp2p'
2
+ import type { PeerId } from '@libp2p/interface'
3
+ import type { KnownPeer } from './responsibility.js'
4
+
5
+ export function buildKnownPeers(libp2p: Libp2p): KnownPeer[] {
6
+ const self: KnownPeer = {
7
+ id: libp2p.peerId as unknown as PeerId,
8
+ addrs: libp2p.getMultiaddrs().map(ma => ma.toString())
9
+ }
10
+
11
+ const connections = libp2p.getConnections()
12
+ const byPeer: Record<string, { id: PeerId, addrs: Set<string> }> = {}
13
+
14
+ for (const c of connections) {
15
+ const pid = c.remotePeer
16
+ const key = pid.toString()
17
+ const entry = byPeer[key] ?? (byPeer[key] = { id: pid as unknown as PeerId, addrs: new Set() })
18
+ // NOTE: this takes every connection's remoteAddr regardless of direction, so an inbound
19
+ // connection contributes the far side's ephemeral source socket. Harmless today: nothing in
20
+ // this repo calls buildKnownPeers, and responsibility.ts reads only `id`. But this is a public
21
+ // export — if a caller ever hands KnownPeer.addrs to a THIRD party (a record, a redirect, a
22
+ // discovery reply), route it through `publishableConnectionAddr` in peer-address-book.ts first.
23
+ const addrStr = c.remoteAddr?.toString?.()
24
+ if (addrStr) entry.addrs.add(addrStr)
25
+ }
26
+
27
+ const others: KnownPeer[] = Object.values(byPeer).map(e => ({ id: e.id, addrs: Array.from(e.addrs) }))
28
+ return [self, ...others]
29
+ }
30
+
31
+
@@ -0,0 +1,242 @@
1
+ import type { ActionContext, ActionRev, BlockId, IBlock, IRepo } from "@optimystic/db-core";
2
+ import type { BlockArchive } from "./struct.js";
3
+ import { proofClaimsCommit, type BlockCommitProof } from "../cluster/commit-proof.js";
4
+ import { createLogger } from "../logger.js";
5
+
6
+ const log = createLogger('block-archive');
7
+
8
+ /**
9
+ * The archive shape every block-repair path exchanges: ONE revision — the one being served —
10
+ * carrying its action, the block itself when the serving repo materialized it, and the cohort's
11
+ * commit proof for that revision when the serving repo retained one.
12
+ *
13
+ * One function rather than the shape re-typed at each site, because three sites had already drifted
14
+ * while each claimed to mirror the others: the sync service served `block: undefined` for a block
15
+ * with no materialized content, the mesh test harness served no archive at all for that same repo
16
+ * state (turning a corroborable revision claim into a phantom non-holder), and the reconcile unit
17
+ * spec's stand-in emitted a third shape again. `createReconcileBlock` reads
18
+ * `revisions[rev].action.actionId` and `revisions[rev].block`, and the difference between "absent
19
+ * block" and "absent archive" decides whether a peer votes in the revision quorum at all — so the
20
+ * shape is a contract, not a detail.
21
+ *
22
+ * `block` stays optional on purpose: a revision whose content the serving repo cannot materialize
23
+ * (a deleted block — see `GetBlockResult.block`) is still a revision that peer legitimately claims.
24
+ * It votes on `(rev, actionId)` and abstains from the content quorum, which is exactly the evidence
25
+ * it holds.
26
+ *
27
+ * `proof` is optional for the same kind of reason (see `ArchiveRevisions.proof`): a pre-proof
28
+ * revision, a diverged member, or an un-upgraded peer serves none, and every consumer must behave
29
+ * exactly as it did before proofs existed when it is absent.
30
+ *
31
+ * NOTE: the `range` emitted here is a COMPATIBILITY CONTRACT with `BlockStorage.vetRestoredArchive`,
32
+ * which refuses an archive whose declared range does not start at its lowest revision, end past its
33
+ * highest, or which is open-ended. `[rev, rev + 1]` around a single revision satisfies all three
34
+ * trivially, and this is currently the ONLY producer of an archive that crosses the restore wire —
35
+ * which is why no test drives a refusal end to end through `RestorationCoordinator`. If a second
36
+ * producer ever appears (a multi-revision or sparse archive, say), that contract stops being
37
+ * trivially true and needs a test pinning the two ends together.
38
+ */
39
+ export function singleRevisionArchive(
40
+ blockId: BlockId,
41
+ source: ActionRev,
42
+ block: IBlock | undefined,
43
+ proof?: BlockCommitProof
44
+ ): BlockArchive {
45
+ return {
46
+ blockId,
47
+ revisions: {
48
+ [source.rev]: {
49
+ action: { actionId: source.actionId, transform: { insert: block } },
50
+ ...(block ? { block } : {}),
51
+ ...(proof ? { proof } : {})
52
+ }
53
+ },
54
+ range: [source.rev, source.rev + 1]
55
+ };
56
+ }
57
+
58
+ /**
59
+ * A peer's answer to the latest-revision consult: the `(rev, actionId)` it claims, plus the cohort's
60
+ * commit proof for that revision when it retained one.
61
+ *
62
+ * Defined HERE, alongside the archive shape, rather than beside the callback that returns it
63
+ * (`ClusterLatestCallback` in `repo/coordinator-repo.ts`, which re-exports this name): a remote
64
+ * peer's answer IS a projection of the archive it served, so keeping the two in one file is what
65
+ * stops the projection from drifting from the shape — the same reason `singleRevisionArchive` is a
66
+ * function rather than a shape re-typed at each site.
67
+ *
68
+ * The proof is OPTIONAL and means nothing until a consumer verifies it. A pre-proof revision, a
69
+ * diverged member, and an un-upgraded peer all legitimately answer without one, and a peer is free
70
+ * to attach whatever it likes — verification, never presence, is what makes a proof evidence.
71
+ */
72
+ export type CertifiedActionRev = ActionRev & { proof?: BlockCommitProof };
73
+
74
+ /**
75
+ * Highest revision an archive covers, or `undefined` when it covers none.
76
+ *
77
+ * ONE implementation for every site that asks an untrusted archive this question — the two repair
78
+ * wires ({@link latestClaimFromArchive}) and the reconcile pass (`cluster/reconcile-block.ts`) — for
79
+ * the same reason {@link singleRevisionArchive} is one function: two copies is how one of them ends
80
+ * up without the guards below.
81
+ *
82
+ * Both guards matter on input a remote peer chose. `Object.keys` on a JSON-parsed archive yields
83
+ * strings, so a non-numeric key coerces to `NaN` and is skipped rather than poisoning the maximum.
84
+ * And the fold is deliberate rather than `Math.max(...keys)`: the spread passes one ARGUMENT per
85
+ * revision, which throws `RangeError: Maximum call stack size exceeded` past ~125k arguments —
86
+ * comfortably inside the 8 MiB `MAX_BLOCK_MESSAGE_BYTES` a sync response may carry (130k minimal
87
+ * revision entries serialize to ~6.3 MiB), so a peer could otherwise choose to make this throw.
88
+ */
89
+ export function maxArchiveRevision(revisions: BlockArchive['revisions'] | undefined): number | undefined {
90
+ let max: number | undefined;
91
+ for (const key of Object.keys(revisions ?? {})) {
92
+ const rev = Number(key);
93
+ if (Number.isFinite(rev) && (max === undefined || rev > max)) max = rev;
94
+ }
95
+ return max;
96
+ }
97
+
98
+ /**
99
+ * The highest-revision claim an archive carries, or `undefined` when it holds no usable revision.
100
+ * `undefined` is the peer having ANSWERED without data — an absent claim, never silence (see
101
+ * `ClusterLatestCallback`'s three-way contract).
102
+ *
103
+ * The proof is read from the SAME revision entry as the `(rev, actionId)`, so a serving peer cannot
104
+ * pair a genuine proof with a revision it does not certify by choosing a different layout. Nothing
105
+ * here verifies anything: the result is the peer's unverified assertion until a caller checks it.
106
+ */
107
+ export function latestClaimFromArchive(archive: BlockArchive): CertifiedActionRev | undefined {
108
+ const maxRev = maxArchiveRevision(archive.revisions);
109
+ if (maxRev === undefined) return undefined;
110
+ const entry = archive.revisions[maxRev];
111
+ if (!entry?.action) return undefined;
112
+ return {
113
+ actionId: entry.action.actionId,
114
+ rev: maxRev,
115
+ ...(entry.proof ? { proof: entry.proof } : {})
116
+ };
117
+ }
118
+
119
+ /**
120
+ * What {@link serveBlockArchive} needs of the repo it reads: `IRepo`, plus — OPTIONALLY — the
121
+ * revision-keyed commit-proof accessor `StorageRepo` implements.
122
+ *
123
+ * Optional rather than required so the unit-test doubles and any other plain `IRepo` that serves
124
+ * archives keep compiling and keep working (they simply serve no proof). A repo that CAN serve
125
+ * proofs and does not is not a type error here — it is the pre-proof behaviour, which stays valid.
126
+ */
127
+ export type ArchiveServingRepo = IRepo & {
128
+ getBlockProof?(blockId: BlockId, rev: number): Promise<BlockCommitProof | undefined>;
129
+ };
130
+
131
+ /**
132
+ * An {@link ArchiveServingRepo} that definitely CAN serve proofs — the accessor required rather
133
+ * than optional.
134
+ *
135
+ * The optionality above exists for a serving repo that legitimately has no proofs (a test double, a
136
+ * plain-`IRepo` embedder). It is the wrong default for a node's OWN store: forgetting the accessor
137
+ * there degrades every archive it serves to proof-less, silently and without a type error. Naming
138
+ * the stronger shape lets a composition root demand it (`createServedRepoProxy`).
139
+ */
140
+ export type ProofRetainingRepo = IRepo & Required<Pick<ArchiveServingRepo, 'getBlockProof'>>;
141
+
142
+ /**
143
+ * Serve `blockId` out of a local repo as a {@link singleRevisionArchive} — what a peer answers a
144
+ * block-repair fetch with. `undefined` when the repo holds no revision of the block at all, which
145
+ * callers report as "holds nothing" (`ReconcileBlockDeps.fetchArchive`'s contract folds
146
+ * "unreachable" into that same answer).
147
+ *
148
+ * The read skips the cluster deliberately: a peer answering a repair fetch reports what IT holds,
149
+ * and one that re-asked its own cohort would launder another peer's claim as its own.
150
+ *
151
+ * `rev` pins the read: `StorageRepo.get` materializes the highest committed revision of the block
152
+ * at or below it and reports that revision as `GetBlockResult.materialized`, which is what the
153
+ * archive is labelled with — its revision number, its action id, and its proof all belong to the
154
+ * bytes actually served. `state.latest` (the repo's NEWEST revision) is never the label: a caller
155
+ * asking for an older revision (`RestorationCoordinator` is the one that does) is answered with
156
+ * that revision as itself, or with nothing. Never with old bytes under a newer label — that pairing
157
+ * is what a receiver keyed by action id writes over its own good copy of the newer revision.
158
+ *
159
+ * The proof served is looked up for the revision ACTUALLY served, never for one chosen
160
+ * independently, so the archive can never publish a proof paired with a revision it does not
161
+ * certify. (A newer revision's proof over older bytes would pass `verifyBlockCommitProofClaim` and
162
+ * fail `verifyBlockCommitProofContent` — strictly worse than no proof.)
163
+ *
164
+ * NOTE: a single-revision archive's proof is a rounding error against the wire cap. A sync
165
+ * *response* is bounded by `MAX_BLOCK_MESSAGE_BYTES` (8 MiB — `SyncClient.requestBlock` sets the
166
+ * response decoder's `maxDataLength`; the 1 MiB `MAX_CONTROL_MESSAGE_BYTES` bounds the inbound
167
+ * REQUEST, not this). A proof's serialized size is dominated by two signatures plus a peer id per
168
+ * cohort member: a whole proof-carrying single-revision archive measured 4801 bytes at a 10-peer
169
+ * cohort and 8851 bytes at 20 (`test/block-archive-proof.spec.ts`, "far below the sync response
170
+ * cap", which prints both), i.e. ~405 bytes per additional peer. Reaching 8 MiB would take a cohort in the tens of thousands, so
171
+ * no plausible cluster size puts this near the cap — the block bytes the archive already carries
172
+ * are the term that matters. Revisit only if a proof ever grows a per-peer payload beyond its two
173
+ * signatures.
174
+ */
175
+ export async function serveBlockArchive(repo: ArchiveServingRepo, blockId: BlockId, rev?: number): Promise<BlockArchive | undefined> {
176
+ // `ActionContext` is `{ committed, rev, actionId? }` — nothing else. `committed: []` claims no
177
+ // uncommitted-but-known action, and omitting `actionId` is what keeps this off `StorageRepo`'s
178
+ // pending-overlay path, so the read is purely "the highest COMMITTED revision at or below `rev`".
179
+ const context: ActionContext | undefined = rev !== undefined ? { rev, committed: [] } : undefined;
180
+ const result = await repo.get({ blockIds: [blockId], context }, { skipClusterFetch: true } as any);
181
+ const entry = result[blockId];
182
+ const latest = entry?.state?.latest;
183
+ if (!latest) return undefined;
184
+ // The revision the content in hand IS. A repo that reports `materialized` (`StorageRepo`) has
185
+ // served the highest committed revision at or below the pin, and that — never `state.latest`,
186
+ // the repo's NEWEST revision — is the archive's label. A repo that does not report it (a plain
187
+ // `IRepo`) can only be describing its latest.
188
+ const served = entry.materialized ?? latest;
189
+ // Fail closed rather than mislabel. A served revision ABOVE the pin is never a right answer:
190
+ // either the repo could not say what it materialized and its latest is newer than what was
191
+ // asked for (so the bytes may be pinned or may not — and the only label in hand is wrong for
192
+ // one of them), or the repo misreported. Labelling old bytes with a newer revision's number and
193
+ // action id is what the asker's `saveRestored` — keyed by action id — then writes over the good
194
+ // copy it already holds. Serve nothing instead: every caller already handles "holds nothing",
195
+ // and `restoreRevision` turns it into a loud "not found during restore attempt". A served
196
+ // revision AT OR BELOW the pin is the block unchanged since the pin, served exactly as before.
197
+ if (rev !== undefined && served.rev > rev) {
198
+ log('serve:skip blockId=%s served=%d requested=%d latest=%d (pinned read — refusing to mislabel content)',
199
+ blockId, served.rev, rev, latest.rev);
200
+ return undefined;
201
+ }
202
+ const proof = await servableProof(repo, blockId, served);
203
+ return singleRevisionArchive(blockId, served, entry.block, proof);
204
+ }
205
+
206
+ /**
207
+ * The stored proof for the revision being served, or `undefined` — including whenever anything is
208
+ * off about it. Serving no proof is always safe (every consumer must already handle its absence),
209
+ * so this fails closed on all three unhappy paths:
210
+ *
211
+ * - the repo has no proof accessor at all (a plain `IRepo`, a test double);
212
+ * - the lookup throws — a storage fault must not turn a servable archive into "holds nothing",
213
+ * because that would recreate the phantom-non-holder bug the archive shape exists to prevent;
214
+ * - the stored proof's own message does not name this `(blockId, rev, actionId)`. That is a local
215
+ * storage-integrity fault, not a peer's doing, and publishing a mis-paired proof would hand
216
+ * every receiver an artifact that cannot verify. Logged loudly, because it means a proof was
217
+ * written under a key its content contradicts.
218
+ *
219
+ * Exported for the SAME reason {@link singleRevisionArchive} is: the mesh test harness answers the
220
+ * latest-revision consult out of a sibling's repo directly rather than over the sync protocol, and
221
+ * a harness that attached proofs by a different rule than a real peer would let every mesh-tier
222
+ * test silently exercise a path production does not have.
223
+ */
224
+ export async function servableProof(
225
+ repo: ArchiveServingRepo, blockId: BlockId, latest: ActionRev
226
+ ): Promise<BlockCommitProof | undefined> {
227
+ if (typeof repo.getBlockProof !== 'function') return undefined;
228
+ let proof: BlockCommitProof | undefined;
229
+ try {
230
+ proof = await repo.getBlockProof(blockId, latest.rev);
231
+ } catch (error) {
232
+ log('serve:proof-lookup-failed blockId=%s rev=%d error=%s', blockId, latest.rev,
233
+ error instanceof Error ? error.message : String(error));
234
+ return undefined;
235
+ }
236
+ if (!proof) return undefined;
237
+ if (!proofClaimsCommit(proof, { blockId, rev: latest.rev, actionId: latest.actionId })) {
238
+ log('serve:proof-claim-mismatch blockId=%s rev=%d actionId=%s', blockId, latest.rev, latest.actionId);
239
+ return undefined;
240
+ }
241
+ return proof;
242
+ }
@@ -0,0 +1,144 @@
1
+ import type { BlockId } from "@optimystic/db-core";
2
+ import { Latches } from "@optimystic/db-core";
3
+
4
+ /**
5
+ * The ONE per-block write lock.
6
+ *
7
+ * A block's metadata is stored as a single blob — `{ latest, ranges }` — read and written whole, so
8
+ * any read-modify-write of it overwrites `latest` whether it meant to or not. The invariant is
9
+ * therefore stated over the whole blob, not over `latest`:
10
+ *
11
+ * > A block's metadata, revision records, action transforms, pending records, and stored proofs are
12
+ * > only ever written while holding {@link blockWriteLatchKey}`(blockId)`.
13
+ *
14
+ * This module is the single acquirer of that key. The check, which deliberately matches the call
15
+ * shape (the escapes keep this very comment from matching):
16
+ *
17
+ * > grep -rnE "Latches\.acquire\(" packages/db-p2p/src
18
+ *
19
+ * That must return exactly one line — the call in `acquireBlockWriteLatch` below. A second hit
20
+ * anywhere means a caller has started taking the key directly and the token discipline has a hole.
21
+ * Every writing method on `IBlockStorage` demands a
22
+ * {@link BlockWriteLatch} token, which only this module can mint, so an unlatched write does not
23
+ * type-check rather than merely being documented as forbidden.
24
+ *
25
+ * `Latches` is a plain FIFO promise-chain mutex — no owner tracking, no re-entrancy — so a holder
26
+ * must never call back into something that acquires the same block's key. The token is what lets a
27
+ * callee prove it is already inside the latch instead of re-acquiring.
28
+ */
29
+ export const blockWriteLatchKey = (blockId: BlockId): string => `Block.write:${blockId}`;
30
+
31
+ let mint!: (blockId: BlockId) => BlockWriteLatch;
32
+ let expire!: (latch: BlockWriteLatch) => void;
33
+
34
+ /**
35
+ * Opaque proof that the bearer is executing inside {@link blockWriteLatchKey}`(blockId)`. Only
36
+ * {@link acquireBlockWriteLatch} (and {@link withBlockWriteLatch} through it) can construct one:
37
+ * the constructor is private and the module-scoped minter is assigned from a static block, where
38
+ * the private constructor is callable — no cast, nothing outside this module can build a token.
39
+ *
40
+ * A token is only valid while the latch it proves is actually held: releasing expires it, so a
41
+ * callback that stashes its token and writes after its scope closed is rejected instead of silently
42
+ * writing unlatched. `live` is the check; only this module can clear it.
43
+ *
44
+ * `BlockStorage` checks `latch.blockId` against its own id on every write, so a token for one block
45
+ * cannot be presented for another.
46
+ */
47
+ export class BlockWriteLatch {
48
+ #live = true;
49
+
50
+ private constructor(readonly blockId: BlockId) { }
51
+
52
+ /** False once the latch this token proves has been released. */
53
+ get live(): boolean {
54
+ return this.#live;
55
+ }
56
+
57
+ static {
58
+ mint = (blockId) => new BlockWriteLatch(blockId);
59
+ expire = (latch) => { latch.#live = false; };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Acquire the write latch for `blockId`. The non-scoped single-block form is the building block for
65
+ * {@link acquireBlockWriteLatches} and for a caller that must hold one latch across control flow a
66
+ * callback cannot express; everything else should prefer {@link withBlockWriteLatch}. A caller that
67
+ * wants N latches must go through {@link acquireBlockWriteLatches}, not loop over this. The caller
68
+ * MUST call `release` exactly once, in a `finally`. Releasing expires the token, so a write attempted
69
+ * with it afterwards is rejected rather than running outside the latch.
70
+ */
71
+ export async function acquireBlockWriteLatch(blockId: BlockId): Promise<{ latch: BlockWriteLatch; release: () => void }> {
72
+ const releaseLatch = await Latches.acquire(blockWriteLatchKey(blockId));
73
+ const latch = mint(blockId);
74
+ return {
75
+ latch,
76
+ release: () => {
77
+ expire(latch);
78
+ releaseLatch();
79
+ }
80
+ };
81
+ }
82
+
83
+ /** The N-latch hold {@link acquireBlockWriteLatches} returns: a token per block, and one release. */
84
+ export type BlockWriteLatches = {
85
+ /** Token per requested block id, so each write can prove it runs inside that block's latch. */
86
+ readonly latches: ReadonlyMap<BlockId, BlockWriteLatch>;
87
+ /** Releases every latch, in reverse acquisition order. Call exactly once, in a `finally`. */
88
+ readonly release: () => void;
89
+ };
90
+
91
+ /**
92
+ * Acquire the write latches for a whole set of blocks at once — the only sanctioned way to hold more
93
+ * than one. Two callers need it (`StorageRepo.commit` and `applyInvalidation`), and the property that
94
+ * keeps them from deadlocking against each other is not local to either: it is that EVERY multi-latch
95
+ * holder acquires in the one global order. Owning that here makes it a property of the module that
96
+ * owns the key rather than a rule each call site restates and a third one could get wrong.
97
+ *
98
+ * Three things this does that a hand-rolled loop keeps getting wrong:
99
+ * - **Dedups** the ids. `Latches` is a plain FIFO mutex with no re-entrancy, so a repeated id in the
100
+ * request would deadlock the acquirer against itself.
101
+ * - **Sorts** them, so any two multi-latch holders acquire in the same order and no cycle exists.
102
+ * - **Releases what it already took** if an acquisition partway through the set throws, rather than
103
+ * stranding those latches forever.
104
+ *
105
+ * The returned map is keyed by the ids actually held, so a caller that wants its own (e.g. request)
106
+ * ordering can iterate its own list and look each token up.
107
+ */
108
+ export async function acquireBlockWriteLatches(blockIds: Iterable<BlockId>): Promise<BlockWriteLatches> {
109
+ const ordered = Array.from(new Set(blockIds)).sort();
110
+ const releases: (() => void)[] = [];
111
+ const release = () => {
112
+ for (let i = releases.length - 1; i >= 0; i--) {
113
+ releases[i]!();
114
+ }
115
+ releases.length = 0;
116
+ };
117
+ const latches = new Map<BlockId, BlockWriteLatch>();
118
+ try {
119
+ for (const id of ordered) {
120
+ const acquired = await acquireBlockWriteLatch(id);
121
+ releases.push(acquired.release);
122
+ latches.set(id, acquired.latch);
123
+ }
124
+ } catch (err) {
125
+ release();
126
+ throw err;
127
+ }
128
+ return { latches, release };
129
+ }
130
+
131
+ /**
132
+ * Run `fn` while holding the write latch for `blockId`, handing it the token to pass down to the
133
+ * storage writes it makes. Acquire/release is per call, so a caller holds at most one block latch at
134
+ * a time and cannot deadlock against a sorted, up-front {@link acquireBlockWriteLatches} hold — as
135
+ * long as `fn` does not itself acquire another block's latch (nothing in this package does).
136
+ */
137
+ export async function withBlockWriteLatch<T>(blockId: BlockId, fn: (latch: BlockWriteLatch) => Promise<T>): Promise<T> {
138
+ const { latch, release } = await acquireBlockWriteLatch(blockId);
139
+ try {
140
+ return await fn(latch);
141
+ } finally {
142
+ release();
143
+ }
144
+ }