@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
@@ -0,0 +1,319 @@
1
+ import type { ActionId, BlockId, IBlock } from "@optimystic/db-core";
2
+ import {
3
+ verifyBlockCommitProofClaim, verifyBlockCommitProofContent,
4
+ type BlockCommitProof, type ProofClaim, type ProofFailure, type ProofThresholds
5
+ } from "./commit-proof.js";
6
+ import { createLogger } from "../logger.js";
7
+
8
+ const log = createLogger('certified-claims');
9
+
10
+ /**
11
+ * Certification of repair claims by cohort commit proof — the shared layer both block-restoration
12
+ * paths (read-repair in `CoordinatorRepo`, reconcile in `reconcile-block`) run a peer-attached
13
+ * {@link BlockCommitProof} through before marking a `RevClaim` / `BlockHashCandidate` as
14
+ * `certified` for the selection helpers in `quorum-restore.ts`.
15
+ *
16
+ * Mirrors the layered posture of `verifyInvalidationCertificate` (`dispute/invalidation.ts`):
17
+ *
18
+ * - **Layer 1 (always)**: offline cryptographic verification — thresholds of Ed25519 votes from
19
+ * the proof's own signer list (`verifyBlockCommitProofClaim` / `...Content`), preceded by the
20
+ * {@link MAX_PROOF_SIGNERS} cap this module owes the verifier (its caller obligation #2:
21
+ * verification cost is attacker-chosen, so bound the cohort BEFORE any hashing).
22
+ * - **Layer 2 (optional, observational)**: {@link ProofAnchoring.recomputeBlockCohort} re-derives
23
+ * the block's cohort from the caller's own topology view and the overlap with the proof's
24
+ * signers is LOGGED — never gated on. Historic cohort rotation makes zero overlap legitimate
25
+ * for old data; gating would re-create the very lone-holder-unreadable defect the certified
26
+ * path exists to fix. Like invalidation's `recomputeArbitratorSet`, no production caller wires
27
+ * this yet (see the note at the `clusterMember` construction in `libp2p-node-base.ts`).
28
+ * - **Degradation**: with no recompute capability, or an infeasible recompute, an accepted proof
29
+ * is certified anyway but never silently — logged, and surfaced via
30
+ * {@link ProofAnchoring.onUnanchored} so callers can count/report the residual, exactly as
31
+ * invalidation's `acceptUnanchored` does.
32
+ */
33
+
34
+ /**
35
+ * Verdict from a {@link RecomputeBlockCohort}:
36
+ * - `{ feasible: false }` — the caller could not re-derive the block's current cohort (no routing
37
+ * view, lookup failed); anchoring degrades to accept-and-surface.
38
+ * - `{ feasible: true, cohortPeerIds }` — the currently-derived cohort, compared (log-only)
39
+ * against the proof's signer list.
40
+ */
41
+ export type CohortRecomputeVerdict =
42
+ | { readonly feasible: false }
43
+ | { readonly feasible: true; readonly cohortPeerIds: string[] };
44
+
45
+ /**
46
+ * Injected layer-2 capability: re-derive the block's responsible cohort from the caller's own
47
+ * topology view. A real implementation would come from `IKeyNetwork.findCluster` — the same source
48
+ * `deriveExpectedCluster` uses in `libp2p-node-base.ts`. NOT wired in production yet; the overlap
49
+ * it enables is observational (logged), never a gate.
50
+ */
51
+ export type RecomputeBlockCohort = (blockId: BlockId) => Promise<CohortRecomputeVerdict>;
52
+
53
+ /** Reported when a proof is accepted on layer-1 cryptography alone — see {@link ProofAnchoring.onUnanchored}. */
54
+ export type UnanchoredProofAcceptance = {
55
+ readonly blockId: BlockId;
56
+ readonly rev: number;
57
+ readonly actionId: ActionId;
58
+ /** Size of the proof's signer list (`proof.peerIds.length`). */
59
+ readonly signerCount: number;
60
+ /** Why the anchoring layer did not run to a comparison. */
61
+ readonly reason: 'no-recompute-capability' | 'recompute-infeasible';
62
+ };
63
+
64
+ /** Optional anchoring capabilities a caller threads into {@link certifyClaim} / {@link certifyContent}. */
65
+ export type ProofAnchoring = {
66
+ /** Layer-2 recompute; omitted → layer-1-only certification with the documented surfacing. */
67
+ readonly recomputeBlockCohort?: RecomputeBlockCohort;
68
+ /**
69
+ * Invoked when a proof is accepted without a cohort comparison (capability absent or
70
+ * infeasible). Lets a caller surface "certified a claim it could not anchor to topology"
71
+ * alongside the internal log. Exceptions thrown from it are swallowed (logged), never allowed
72
+ * to un-certify an already-verified proof.
73
+ */
74
+ readonly onUnanchored?: (info: UnanchoredProofAcceptance) => void;
75
+ };
76
+
77
+ /** Everything {@link ProofFailure} names, plus the pre-verification cap decline this module adds. */
78
+ export type CertifyFailure = ProofFailure | 'oversized-cohort';
79
+
80
+ /**
81
+ * Hard bound on `proof.peerIds.length`, enforced BEFORE any hashing or signature work. Nothing on
82
+ * the repair wire bounds a proof's cohort count — only byte caps apply (1 MiB control / 8 MiB sync
83
+ * response), which would still admit ~80k signature verifications — so this module enforces the
84
+ * bound `commit-proof.ts` declares as its caller obligation #2. 256 is far above any plausible
85
+ * cohort (deployments run ~10) while keeping the worst-case verification cost trivial.
86
+ *
87
+ * NOTE: this cap bounds the SIGNATURE count (at most `MAX_PROOF_SIGNERS` Ed25519 verifies per
88
+ * round, since `countApprovals` skips signers outside `peerIds` before any crypto). It does NOT
89
+ * bound the proof's serialized SIZE: `computeClusterMessageHash` / `...PromiseHash` /
90
+ * `...CommitHash` canonically serialize `message`, `promises` and `commits`, whose entry counts a
91
+ * peer chooses freely inside the transport byte cap. That cost is linear and today bounded by the
92
+ * 1 MiB control-message cap; if repair verification ever shows up in a profile, or a deployment
93
+ * raises those transport caps, add a serialized-size bound here alongside the signer count.
94
+ */
95
+ export const MAX_PROOF_SIGNERS = 256;
96
+
97
+ /**
98
+ * Every {@link CertifyFailure}, classified: may it be held against the peer that SERVED the proof?
99
+ *
100
+ * Exhaustive by type — `Record<CertifyFailure, boolean>` means adding a {@link ProofFailure}
101
+ * variant in `commit-proof.ts` fails the build here until it is classified, rather than silently
102
+ * defaulting to `true` (penalize), which is the wrong direction for an unknown reason.
103
+ *
104
+ * `false` — never a penalty, because the identity behind the artifact was not proven (mirrors
105
+ * `ClusterMember.verifySignature`'s outcome discipline, restated in `commit-proof.ts`) or because
106
+ * the outcome is not misbehavior at all.
107
+ *
108
+ * `true` — the peer's own artifact provably lies, or provably does not cover what it was served
109
+ * for.
110
+ */
111
+ const ATTRIBUTABLE_PROOF_FAILURES: Readonly<Record<CertifyFailure, boolean>> = {
112
+ // Identity not proven — an unparseable or unbound signer, or structural garbage that could have
113
+ // been authored by anyone in the chain.
114
+ 'unknown-signer': false,
115
+ 'non-ed25519-signer': false,
116
+ 'malformed-signature': false,
117
+ 'malformed-proof': false,
118
+ // A v1 / unversioned record is history, not misbehavior: it binds no peer set and never could.
119
+ 'legacy-record': false,
120
+ // A genuine mega-cohort is conceivable and the cap declines it unexamined — no evidence either way.
121
+ 'oversized-cohort': false,
122
+ // Not a failure of the proof at all: the cohort declared no digest for this block, so there was
123
+ // nothing to compare the served bytes against. A verdict ("content uncertified, rev certified"),
124
+ // never misbehavior.
125
+ 'no-digest-declared': false,
126
+ // The artifact contradicts itself or the claim it was served for. `buildBlockCommitProof` derives
127
+ // `peerIds` from `Object.keys(record.peers)`, so honest construction cannot produce a duplicate —
128
+ // serving one implies authorship, as does a digest or hash that does not recompute.
129
+ 'membership-mismatch': true,
130
+ 'message-hash-mismatch': true,
131
+ 'duplicate-signer': true,
132
+ 'promise-threshold': true,
133
+ 'commit-threshold': true,
134
+ // The replay case: a genuine proof presented for a claim it does not cover.
135
+ 'claim-not-in-message': true,
136
+ // The peer served bytes that provably are not the committed content.
137
+ 'digest-mismatch': true
138
+ };
139
+
140
+ /**
141
+ * Failure reasons that must NEVER become a reputation penalty for the serving peer — the `false`
142
+ * half of {@link ATTRIBUTABLE_PROOF_FAILURES}, derived so the set and
143
+ * {@link isAttributableProofFailure} cannot drift apart. Prefer the predicate; this set is exported
144
+ * for callers that want to name the whole population (logs, tests).
145
+ */
146
+ export const NON_ATTRIBUTABLE_PROOF_FAILURES: ReadonlySet<CertifyFailure> = new Set(
147
+ (Object.keys(ATTRIBUTABLE_PROOF_FAILURES) as CertifyFailure[])
148
+ .filter(failure => !ATTRIBUTABLE_PROOF_FAILURES[failure])
149
+ );
150
+
151
+ /**
152
+ * May this failure be held against the peer that served the proof? One predicate so the two repair
153
+ * paths cannot drift on the classification — see {@link ATTRIBUTABLE_PROOF_FAILURES} for the
154
+ * per-reason rationale.
155
+ */
156
+ export function isAttributableProofFailure(failure: CertifyFailure): boolean {
157
+ return ATTRIBUTABLE_PROOF_FAILURES[failure] === true;
158
+ }
159
+
160
+ /**
161
+ * The {@link ProofThresholds} both repair paths verify against, from the cohort's configured
162
+ * `superMajorityThreshold`.
163
+ *
164
+ * `simpleMajorityThreshold` is hardcoded 0.5 — NOT the deployment's configured value (0.51 by
165
+ * default): cohort members enforce `count > total / 2` (`ClusterMember.hasMajority`), and
166
+ * `ProofThresholds` requires a verifier to mirror what the members actually enforced. Verifying
167
+ * against the configured value would reject proofs real cohorts produce.
168
+ *
169
+ * One function so the read path (`CoordinatorRepo.queryClusterForLatest`) and the commit-path
170
+ * reconcile (`cluster/reconcile-block.ts`) cannot drift on the one number that has to match
171
+ * `hasMajority`.
172
+ */
173
+ export function proofThresholds(superMajorityThreshold: number): ProofThresholds {
174
+ return { superMajorityThreshold, simpleMajorityThreshold: 0.5 };
175
+ }
176
+
177
+ /**
178
+ * Certification verdict for a claim — a discriminated union, mirroring `ProofVerdict`
179
+ * (`commit-proof.ts`) so "certified" and "why not" cannot disagree: a failure reason is REQUIRED
180
+ * when uncertified and unrepresentable when certified.
181
+ */
182
+ export type ClaimCertification =
183
+ /** The proof certifies `claim` — cap passed, thresholds of valid cohort signatures met. */
184
+ | { certified: true }
185
+ /** Classify the reason via {@link isAttributableProofFailure} before penalizing anyone. */
186
+ | { certified: false; failure: CertifyFailure };
187
+
188
+ /**
189
+ * Certification verdict for a claim AND the block bytes served for it. Three reachable outcomes,
190
+ * spelled as three arms so an impossible mix (content certified while the rev is not, a failure
191
+ * alongside full success) cannot be constructed:
192
+ */
193
+ export type ContentCertification =
194
+ /** Both halves passed: the proof certifies `(blockId, rev, actionId)` AND these exact bytes. */
195
+ | { revCertified: true; contentCertified: true }
196
+ /**
197
+ * The claim half passed, the content half did not: `digest-mismatch` (attributable — drop AND
198
+ * penalize the served bytes) or `no-digest-declared` (drop only; the cohort declared nothing to
199
+ * compare against). The `(rev, actionId)` is certified either way.
200
+ */
201
+ | { revCertified: true; contentCertified: false; failure: 'digest-mismatch' | 'no-digest-declared' }
202
+ /** The claim half failed, so nothing is certified. Classify via {@link isAttributableProofFailure}. */
203
+ | { revCertified: false; contentCertified: false; failure: CertifyFailure };
204
+
205
+ /**
206
+ * Certify a repair claim by its attached cohort commit proof: cap the cohort, verify offline
207
+ * ({@link verifyBlockCommitProofClaim}), then run the anchoring layer on success. Total on hostile
208
+ * input like the verifier it wraps — never throws.
209
+ */
210
+ export async function certifyClaim(
211
+ proof: BlockCommitProof, claim: ProofClaim, thresholds: ProofThresholds, anchoring: ProofAnchoring = {}
212
+ ): Promise<ClaimCertification> {
213
+ if (exceedsSignerCap(proof)) {
214
+ return { certified: false, failure: 'oversized-cohort' };
215
+ }
216
+ const verdict = await verifyBlockCommitProofClaim(proof, claim, thresholds);
217
+ if (!verdict.ok) {
218
+ return { certified: false, failure: verdict.reason };
219
+ }
220
+ await anchorAcceptedProof(proof, claim, anchoring);
221
+ return { certified: true };
222
+ }
223
+
224
+ /**
225
+ * Certify a claim AND the served block bytes ({@link verifyBlockCommitProofContent}). The two
226
+ * halves are reported separately because the content checks (`digest-mismatch` /
227
+ * `no-digest-declared`) only run after the claim half has already PASSED — a proof that certifies
228
+ * the revision while the bytes disagree still certifies the revision; the caller drops (and, for
229
+ * `digest-mismatch`, penalizes) the bytes. Anchoring runs whenever the claim half passed: that is
230
+ * the moment a proof was accepted as evidence.
231
+ */
232
+ export async function certifyContent(
233
+ proof: BlockCommitProof, claim: ProofClaim, block: IBlock, thresholds: ProofThresholds,
234
+ anchoring: ProofAnchoring = {}
235
+ ): Promise<ContentCertification> {
236
+ if (exceedsSignerCap(proof)) {
237
+ return { revCertified: false, contentCertified: false, failure: 'oversized-cohort' };
238
+ }
239
+ const verdict = await verifyBlockCommitProofContent(proof, claim, block, thresholds);
240
+ if (verdict.ok) {
241
+ await anchorAcceptedProof(proof, claim, anchoring);
242
+ return { revCertified: true, contentCertified: true };
243
+ }
244
+ if (verdict.reason === 'digest-mismatch' || verdict.reason === 'no-digest-declared') {
245
+ await anchorAcceptedProof(proof, claim, anchoring);
246
+ return { revCertified: true, contentCertified: false, failure: verdict.reason };
247
+ }
248
+ return { revCertified: false, contentCertified: false, failure: verdict.reason };
249
+ }
250
+
251
+ /**
252
+ * The cap check, structurally safe on hostile shapes: a `peerIds` that is not an array — or a
253
+ * property access that throws, which is why the whole read is guarded — falls through to the
254
+ * verifier, which reports `malformed-proof` without doing signature work either. This runs before
255
+ * {@link certifyClaim}'s and {@link certifyContent}'s only other call, so it is the one place their
256
+ * "never throws" contract could be broken.
257
+ */
258
+ function exceedsSignerCap(proof: BlockCommitProof): boolean {
259
+ try {
260
+ return proof !== null && typeof proof === 'object'
261
+ && Array.isArray(proof.peerIds) && proof.peerIds.length > MAX_PROOF_SIGNERS;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Layer 2 + degradation for a proof the offline layer accepted. Purely observational — nothing
269
+ * here can revoke the certification: overlap is logged (never gated — historic cohort rotation
270
+ * makes zero overlap legitimate for old data, and gating would re-create the lone-holder-unreadable
271
+ * defect), infeasibility degrades to accept-and-surface, and a throwing capability is logged and
272
+ * treated as infeasible.
273
+ */
274
+ async function anchorAcceptedProof(proof: BlockCommitProof, claim: ProofClaim, anchoring: ProofAnchoring): Promise<void> {
275
+ const signerCount = proof.peerIds.length;
276
+ if (!anchoring.recomputeBlockCohort) {
277
+ acceptUnanchored(claim, signerCount, 'no-recompute-capability', anchoring);
278
+ return;
279
+ }
280
+ let verdict: CohortRecomputeVerdict;
281
+ try {
282
+ verdict = await anchoring.recomputeBlockCohort(claim.blockId);
283
+ } catch (err) {
284
+ log('anchor-recompute-error block=%s error=%o', claim.blockId, err);
285
+ acceptUnanchored(claim, signerCount, 'recompute-infeasible', anchoring);
286
+ return;
287
+ }
288
+ // A verdict that is null or otherwise off-contract reads as infeasible rather than throwing —
289
+ // `recomputeBlockCohort` is caller-supplied, so its return shape is as untrusted as its behavior.
290
+ if (!verdict?.feasible) {
291
+ acceptUnanchored(claim, signerCount, 'recompute-infeasible', anchoring);
292
+ return;
293
+ }
294
+ try {
295
+ const cohort = new Set(verdict.cohortPeerIds);
296
+ const overlap = proof.peerIds.filter(id => cohort.has(id)).length;
297
+ log('anchor-overlap block=%s rev=%d action=%s overlap=%d signers=%d cohort=%d',
298
+ claim.blockId, claim.rev, claim.actionId, overlap, signerCount, cohort.size);
299
+ } catch (err) {
300
+ // A non-iterable `cohortPeerIds`. The comparison is observational, so losing it costs a log
301
+ // line, never the certification.
302
+ log('anchor-overlap-error block=%s error=%o', claim.blockId, err);
303
+ }
304
+ }
305
+
306
+ /** Accept-and-surface a proof certified on layer 1 alone — never silent, never throwing. */
307
+ function acceptUnanchored(
308
+ claim: ProofClaim, signerCount: number, reason: UnanchoredProofAcceptance['reason'], anchoring: ProofAnchoring
309
+ ): void {
310
+ log('accept-unanchored block=%s rev=%d action=%s signers=%d reason=%s',
311
+ claim.blockId, claim.rev, claim.actionId, signerCount, reason);
312
+ try {
313
+ anchoring.onUnanchored?.({
314
+ blockId: claim.blockId, rev: claim.rev, actionId: claim.actionId, signerCount, reason
315
+ });
316
+ } catch (err) {
317
+ log('accept-unanchored-callback-error block=%s error=%o', claim.blockId, err);
318
+ }
319
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The p2p-backed {@link ClientSignatureVerifier}: the closure a cluster member's
3
+ * {@link TransactionValidator} uses to decide whether a client transaction was really signed by the
4
+ * identity its stamp claims.
5
+ *
6
+ * Lives here — one exported factory rather than a closure inlined at each wiring site — because two
7
+ * consumers must agree byte-for-byte or the tests stop proving anything about production: the
8
+ * Quereus plugin's `createQuereusValidator` (the real deployment path once a composition root
9
+ * supplies `NodeOptions.validator`) and `db-p2p`'s own signature specs, whose whole claim is that
10
+ * they drive "the exact closure production binds".
11
+ */
12
+
13
+ import type { ClientSignatureVerifier } from '@optimystic/db-core';
14
+ import { b64urlToBytes } from '@optimystic/db-core';
15
+ import { verifyPeerSig } from '../cohort-topic/peer-sig.js';
16
+
17
+ /**
18
+ * Derive the signer's Ed25519 public key from the peer-id string embedded in `stamp.peerId` and
19
+ * verify the base64url signature over the canonical payload.
20
+ *
21
+ * TOTAL on adversarial input, which the {@link ClientSignatureVerifier} port requires: a throw here
22
+ * escapes `ClusterMember.validatePendOperations` instead of becoming a signed reject vote.
23
+ * `verifyPeerSig` already returns `false` rather than throwing on a non-Ed25519 or malformed
24
+ * peer-id; the try/catch adds the same guarantee for the base64url decode.
25
+ */
26
+ export function createPeerClientSignatureVerifier(): ClientSignatureVerifier {
27
+ return (peerId: string, payload: Uint8Array, signature: string): boolean => {
28
+ try {
29
+ return verifyPeerSig(peerId, payload, b64urlToBytes(signature));
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+ }