@optimystic/db-p2p 0.22.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/src/cluster/client.d.ts +10 -0
  2. package/dist/src/cluster/client.d.ts.map +1 -1
  3. package/dist/src/cluster/client.js +30 -1
  4. package/dist/src/cluster/client.js.map +1 -1
  5. package/dist/src/cluster/cluster-repo.d.ts +39 -14
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +226 -119
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/service.d.ts +32 -1
  10. package/dist/src/cluster/service.d.ts.map +1 -1
  11. package/dist/src/cluster/service.js +43 -2
  12. package/dist/src/cluster/service.js.map +1 -1
  13. package/dist/src/cohort-topic/host.js +34 -11
  14. package/dist/src/cohort-topic/host.js.map +1 -1
  15. package/dist/src/cohort-topic/stream-util.d.ts +37 -7
  16. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  17. package/dist/src/cohort-topic/stream-util.js +77 -19
  18. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  19. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  20. package/dist/src/dispute/dispute-service.js +9 -3
  21. package/dist/src/dispute/dispute-service.js.map +1 -1
  22. package/dist/src/index.d.ts +3 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +3 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/libp2p-key-network.d.ts +88 -2
  27. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  28. package/dist/src/libp2p-key-network.js +134 -28
  29. package/dist/src/libp2p-key-network.js.map +1 -1
  30. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  31. package/dist/src/libp2p-node-base.js +25 -1
  32. package/dist/src/libp2p-node-base.js.map +1 -1
  33. package/dist/src/logger.d.ts +17 -1
  34. package/dist/src/logger.d.ts.map +1 -1
  35. package/dist/src/logger.js +19 -2
  36. package/dist/src/logger.js.map +1 -1
  37. package/dist/src/matchmaking/query-transport.js +3 -3
  38. package/dist/src/matchmaking/query-transport.js.map +1 -1
  39. package/dist/src/owned-block-seed.d.ts +6 -3
  40. package/dist/src/owned-block-seed.d.ts.map +1 -1
  41. package/dist/src/owned-block-seed.js +16 -3
  42. package/dist/src/owned-block-seed.js.map +1 -1
  43. package/dist/src/peer-address-book.d.ts +72 -0
  44. package/dist/src/peer-address-book.d.ts.map +1 -0
  45. package/dist/src/peer-address-book.js +123 -0
  46. package/dist/src/peer-address-book.js.map +1 -0
  47. package/dist/src/reactivity/notify-transport.d.ts +4 -4
  48. package/dist/src/reactivity/notify-transport.js +6 -6
  49. package/dist/src/reactivity/notify-transport.js.map +1 -1
  50. package/dist/src/reactivity/push-state-gossip.js +2 -2
  51. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  52. package/dist/src/reactivity/recover-transport.d.ts +6 -2
  53. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  54. package/dist/src/reactivity/recover-transport.js +7 -3
  55. package/dist/src/reactivity/recover-transport.js.map +1 -1
  56. package/dist/src/repo/client.d.ts.map +1 -1
  57. package/dist/src/repo/client.js +11 -2
  58. package/dist/src/repo/client.js.map +1 -1
  59. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  60. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  61. package/dist/src/repo/cluster-coordinator.js +95 -3
  62. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  63. package/dist/src/repo/coordinator-repo.d.ts +62 -9
  64. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  65. package/dist/src/repo/coordinator-repo.js +242 -73
  66. package/dist/src/repo/coordinator-repo.js.map +1 -1
  67. package/dist/src/rn.d.ts +3 -0
  68. package/dist/src/rn.d.ts.map +1 -1
  69. package/dist/src/rn.js +3 -0
  70. package/dist/src/rn.js.map +1 -1
  71. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  72. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  73. package/dist/src/storage/cached-raw-storage.js +152 -0
  74. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  75. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  76. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  77. package/dist/src/storage/cached-store-driver.js +775 -0
  78. package/dist/src/storage/cached-store-driver.js.map +1 -0
  79. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  80. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  81. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  82. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  83. package/dist/src/storage/shared-cache-pool.js +354 -0
  84. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  85. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
  86. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  87. package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
  88. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  89. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  90. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  91. package/dist/src/testing/raw-storage-conformance.js +35 -2
  92. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  93. package/package.json +3 -3
  94. package/readme.md +668 -668
  95. package/src/cluster/block-transfer.ts +424 -424
  96. package/src/cluster/client.ts +119 -88
  97. package/src/cluster/cluster-error.ts +64 -64
  98. package/src/cluster/cluster-policy.ts +203 -203
  99. package/src/cluster/cluster-repo.ts +242 -122
  100. package/src/cluster/cluster-size-coupling.ts +45 -45
  101. package/src/cluster/commit-cert.ts +139 -139
  102. package/src/cluster/i-transaction-state-store.ts +43 -43
  103. package/src/cluster/memory-transaction-state-store.ts +56 -56
  104. package/src/cluster/peer-key-binding.ts +37 -37
  105. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  106. package/src/cluster/quorum-restore.ts +223 -223
  107. package/src/cluster/reconcile-block.ts +203 -203
  108. package/src/cluster/service.ts +293 -241
  109. package/src/cluster/supermajority-coupling.ts +37 -37
  110. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  111. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  112. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  113. package/src/cohort-topic/change-bridge.ts +109 -109
  114. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  115. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  116. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  117. package/src/cohort-topic/host.ts +42 -11
  118. package/src/cohort-topic/index.ts +13 -13
  119. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  120. package/src/cohort-topic/membership-source.ts +68 -68
  121. package/src/cohort-topic/peer-codec.ts +31 -31
  122. package/src/cohort-topic/peer-sig.ts +86 -86
  123. package/src/cohort-topic/protocols.ts +71 -71
  124. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  125. package/src/cohort-topic/size-estimator.ts +16 -16
  126. package/src/cohort-topic/stream-util.ts +79 -19
  127. package/src/cohort-topic/threshold-crypto.ts +239 -239
  128. package/src/cohort-topic/topic-router.ts +77 -77
  129. package/src/dispute/arbitrator-selection.ts +138 -138
  130. package/src/dispute/cascade.ts +524 -524
  131. package/src/dispute/dispute-service.ts +11 -5
  132. package/src/dispute/invalidation.ts +625 -625
  133. package/src/inbound-authorization.ts +190 -190
  134. package/src/index.ts +52 -49
  135. package/src/libp2p-key-network.ts +1120 -990
  136. package/src/libp2p-node-base.ts +1675 -1651
  137. package/src/libp2p-node-rn.ts +30 -30
  138. package/src/libp2p-node.ts +36 -36
  139. package/src/logger.ts +19 -2
  140. package/src/matchmaking/aggregate-counts.ts +104 -104
  141. package/src/matchmaking/index.ts +20 -20
  142. package/src/matchmaking/module.ts +363 -363
  143. package/src/matchmaking/protocols.ts +51 -51
  144. package/src/matchmaking/provider-manager.ts +95 -95
  145. package/src/matchmaking/query-handler.ts +88 -88
  146. package/src/matchmaking/query-transport.ts +3 -3
  147. package/src/matchmaking/seeker-manager.ts +64 -64
  148. package/src/matchmaking/seeker-walk-client.ts +293 -293
  149. package/src/matchmaking/traffic-validation.ts +195 -195
  150. package/src/optimystic-node.ts +36 -36
  151. package/src/owned-block-seed.ts +53 -40
  152. package/src/peer-address-book.ts +149 -0
  153. package/src/protocol-limits.ts +33 -33
  154. package/src/reactivity/forwarder-host.ts +438 -438
  155. package/src/reactivity/index.ts +19 -19
  156. package/src/reactivity/notify-transport.ts +144 -144
  157. package/src/reactivity/origination-manager.ts +192 -192
  158. package/src/reactivity/protocols.ts +61 -61
  159. package/src/reactivity/push-state-gossip.ts +291 -291
  160. package/src/reactivity/recover-transport.ts +7 -3
  161. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  162. package/src/reactivity/subscriber-registry.ts +96 -96
  163. package/src/reactivity/subscription-manager.ts +450 -450
  164. package/src/reactivity/topic-bytes.ts +37 -37
  165. package/src/repo/client.ts +12 -2
  166. package/src/repo/cluster-coordinator.ts +99 -3
  167. package/src/repo/coordinator-repo.ts +281 -74
  168. package/src/repo/types.ts +7 -7
  169. package/src/rn.ts +39 -36
  170. package/src/rpc-deadline.ts +45 -45
  171. package/src/storage/arachnode-partition.ts +74 -74
  172. package/src/storage/cached-raw-storage.ts +180 -0
  173. package/src/storage/cached-store-driver.ts +859 -0
  174. package/src/storage/i-kv-store.ts +8 -8
  175. package/src/storage/i-raw-storage.ts +12 -5
  176. package/src/storage/kv-raw-storage.ts +135 -135
  177. package/src/storage/memory-kv-store.ts +28 -28
  178. package/src/storage/memory-storage.ts +25 -25
  179. package/src/storage/memory-store-driver.ts +157 -157
  180. package/src/storage/raw-store-codec.ts +42 -42
  181. package/src/storage/raw-store-driver.ts +80 -80
  182. package/src/storage/ring-selector.ts +317 -317
  183. package/src/storage/ring-shift-coordinator.ts +271 -271
  184. package/src/storage/shared-cache-pool.ts +452 -0
  185. package/src/storage/storage-repo.ts +1014 -1014
  186. package/src/testing/cohort-topic-mesh-harness.ts +673 -663
  187. package/src/testing/index.ts +8 -8
  188. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  189. package/src/testing/raw-storage-conformance.ts +453 -417
  190. package/src/testing/reactivity-mesh-harness.ts +922 -922
  191. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  192. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  193. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  194. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
@@ -1,159 +1,159 @@
1
- /**
2
- * Cohort-topic substrate — real signed-parent-reference bootstrap-evidence verifier (db-p2p side).
3
- *
4
- * The committed-work proxy for the no-proof-of-work path: a `bootstrap: true` register carries a **signed
5
- * reference to a parent topic that actually exists**, and the cohort admits it only after confirming the
6
- * parent topic exists in locally-available committed / membership state. This is the *only* accepted
7
- * evidence for the committed tiers (T0/T1) and the third option for T2/T3 (`PoW || reputation || parent-ref`).
8
- *
9
- * It replaces the interim reputation stand-in for `verifyParentReference` that
10
- * `cohort-topic-bootstrap-evidence-verifiers` left in `host.ts`.
11
- *
12
- * The verifier is **synchronous** (it runs in `member-engine.ts`'s `runGuards` on every register) and
13
- * **total** (any parse / decode / verify failure on attacker input yields `false`, never a throw). The
14
- * existence check therefore consults a **synchronous local view** — never a network fetch, which inside an
15
- * admission gate would itself be a DoS amplifier.
16
- *
17
- * Two independent checks, both must pass (`docs/cohort-topic.md` §Anti-DoS bullet 4):
18
- *
19
- * 1. **Signed reference (anti-replay).** The participant peer-key-signs the {@link parentRefSigningImage}
20
- * — the bound tuple extended with `parentTopicId` — so a reference minted for one
21
- * `(topic, tier, peer, time, parent)` cannot be lifted onto another register. Verified against the
22
- * participant's own peer key, so it stands alone even in key-less mode (where the outer register
23
- * signature is absent).
24
- * 2. **Existence.** The referenced parent topic must exist in locally-available committed / membership
25
- * state, via the injectable synchronous {@link BootstrapParentTopicView}.
26
- *
27
- * A node only admits a parent-ref bootstrap for a parent topic it has *locally cached* a cert/commit for
28
- * (fail-closed when unknown). That is acceptable for an admission gate: a participant whose parent the node
29
- * does not know retries / uses PoW (T2/T3); a genuinely-new committed (T0/T1) topic is bootstrapped by
30
- * nodes that already serve the parent's committed work, which hold its cert. A *richer* check — that the
31
- * parent's commit certificate names *this* child topic — is the follow-on
32
- * `cohort-topic-parent-ref-tx-log-content`, not this module.
33
- */
34
-
35
- import {
36
- parseBootstrapEvidenceEnvelope,
37
- parentRefSigningImage,
38
- DEFAULT_MAX_NO_POW_TIER,
39
- b64urlToBytes,
40
- type RegisterV1,
41
- type RingCoord,
42
- } from "@optimystic/db-core";
43
- import { verifyPeerSig } from "./peer-sig.js";
44
-
45
- /**
46
- * A synchronous local view answering "does this node locally know parent topic `parentTopicId` exists?".
47
- * Backed (in the host default) by the committed / FRET membership state the node already holds — never a
48
- * network lookup (a round-trip inside an admission gate is a DoS amplifier).
49
- */
50
- export interface BootstrapParentTopicView {
51
- /**
52
- * True iff the node locally knows parent topic `parentTopicId` exists (a committed cohort serves it /
53
- * its membership cert is cached) for a cohort at `tier`. `tier` is the registering child's tier and
54
- * routes the lookup to the committed (T0/T1) or FRET (T2/T3) backing, mirroring the membership source.
55
- */
56
- exists(parentTopicId: Uint8Array, tier: number): boolean;
57
- }
58
-
59
- /** Inputs to {@link createParentReferenceVerifier}. */
60
- export interface ParentReferenceVerifierDeps {
61
- /** The synchronous local existence view the verifier consults (no network I/O). */
62
- readonly parentTopicView: BootstrapParentTopicView;
63
- }
64
-
65
- /**
66
- * The real, all-tiers `verifyParentReference(reg) => boolean`. Total and synchronous:
67
- *
68
- * ```
69
- * env = parseBootstrapEvidenceEnvelope(reg); if (!env?.parentRef) return false
70
- * if (parentRef.parentTopicId === reg.topicId) return false // a topic cannot vouch for itself
71
- * if (!verifyPeerSig(participantCoord, parentRefSigningImage(reg, parentTopicId), parentRef.sig)) return false
72
- * return parentTopicView.exists(parentTopicId, reg.tier)
73
- * ```
74
- *
75
- * - Absent `parentRef`, a malformed envelope, a bad/non-base64url signature, or an unknown parent → `false`
76
- * (fails closed → `unwilling_cohort`).
77
- * - The self-referential guard rejects `parentTopicId === reg.topicId`: a topic cannot reference itself to
78
- * prove its own existence (a circular bootstrap).
79
- */
80
- export function createParentReferenceVerifier(deps: ParentReferenceVerifierDeps): (reg: RegisterV1) => boolean {
81
- const { parentTopicView } = deps;
82
- return (reg: RegisterV1): boolean => {
83
- try {
84
- const env = parseBootstrapEvidenceEnvelope(reg);
85
- if (env?.parentRef === undefined) {
86
- return false; // not offered / not a parent-reference envelope
87
- }
88
- const { parentTopicId, sig } = env.parentRef;
89
- // A topic cannot vouch for its own existence (self-referential / circular bootstrap).
90
- if (parentTopicId === reg.topicId) {
91
- return false;
92
- }
93
- // 1. Signed reference (anti-replay): the participant binds THIS parent to THIS register.
94
- const image = parentRefSigningImage(reg, parentTopicId);
95
- if (!verifyPeerSig(b64urlToBytes(reg.participantCoord), image, b64urlToBytes(sig))) {
96
- return false;
97
- }
98
- // 2. Existence: the parent topic must exist in locally-available committed / membership state.
99
- return parentTopicView.exists(b64urlToBytes(parentTopicId), reg.tier);
100
- } catch {
101
- return false; // any decode / verify failure on attacker input → fail closed
102
- }
103
- };
104
- }
105
-
106
- /** A synchronous local "is a cohort cached for this coord?" read — the {@link FretMembershipSource} cache shape. */
107
- export interface LocalCohortExistence {
108
- /** True iff a `MembershipCertV1` is locally cached for `coord`. */
109
- has(coord: RingCoord): boolean;
110
- }
111
-
112
- /** Inputs to {@link createDefaultParentTopicView}. */
113
- export interface DefaultParentTopicViewDeps {
114
- /** The FRET membership cache (T2/T3 backing): a cached cert means a cohort genuinely serves the parent. */
115
- readonly membershipSource: LocalCohortExistence;
116
- /** Tier addressing — supplies `coord0(parentTopicId)`, the parent topic's root cohort coordinate. */
117
- readonly addressing: { coord0(topicId: Uint8Array): RingCoord };
118
- /**
119
- * Optional committed-state backing for the committed tiers (T0/T1). Reads `coord_0(parentTopicId)`. When
120
- * **omitted the committed tiers fail closed** (return `false`) — a FRET-cached cert (T2/T3 membership)
121
- * must NOT vouch for committed-tier parent existence (committed-tier integrity). A node supplies a real
122
- * committed-by-coord reader (e.g. a future tx-log / commit-cert index) here when one is available.
123
- */
124
- readonly committedReader?: (coord: RingCoord) => boolean;
125
- /** Highest tier exempt from PoW / served by committed work (T0/T1 → 1). Default {@link DEFAULT_MAX_NO_POW_TIER}. */
126
- readonly maxNoPowTier?: number;
127
- }
128
-
129
- /**
130
- * The host-default {@link BootstrapParentTopicView}, tier-routed exactly like the membership source
131
- * (`docs/cohort-topic.md` §Membership source):
132
- *
133
- * - **T0/T1 (committed tiers):** consult `committedReader` — the committed-state backing. Absent → `false`
134
- * (fail closed). A FRET-cached cert never satisfies a committed-tier existence check, so committed-tier
135
- * integrity holds: a parent known only as a FRET (T2/T3) cohort cannot back a committed parent reference.
136
- * - **T2/T3:** `membershipSource.has(coord_0(parentTopicId))` — a cached `MembershipCertV1` means a cohort
137
- * is genuinely serving the parent topic.
138
- *
139
- * **Known limitation (interim).** No coord-keyed committed-membership index exists yet (the transaction-log
140
- * commit certificate is keyed by action, not by `coord_0`), so a node typically wires no `committedReader`
141
- * and T0/T1 parent-ref existence fails closed. T2/T3 parent-ref is fully real today. The dedicated committed
142
- * backing is the follow-on `cohort-topic-parent-ref-tx-log-content`.
143
- */
144
- export function createDefaultParentTopicView(deps: DefaultParentTopicViewDeps): BootstrapParentTopicView {
145
- const { membershipSource, addressing, committedReader } = deps;
146
- const maxNoPowTier = deps.maxNoPowTier ?? DEFAULT_MAX_NO_POW_TIER;
147
- return {
148
- exists(parentTopicId: Uint8Array, tier: number): boolean {
149
- const coord = addressing.coord0(parentTopicId);
150
- if (tier <= maxNoPowTier) {
151
- // Committed tiers (T0/T1): require a committed backing; fail closed without one. The FRET cache
152
- // must not vouch for committed-tier existence (committed-tier integrity).
153
- return committedReader !== undefined && committedReader(coord);
154
- }
155
- // T2/T3: the FRET membership cache reflects a cohort serving the parent topic.
156
- return membershipSource.has(coord);
157
- },
158
- };
159
- }
1
+ /**
2
+ * Cohort-topic substrate — real signed-parent-reference bootstrap-evidence verifier (db-p2p side).
3
+ *
4
+ * The committed-work proxy for the no-proof-of-work path: a `bootstrap: true` register carries a **signed
5
+ * reference to a parent topic that actually exists**, and the cohort admits it only after confirming the
6
+ * parent topic exists in locally-available committed / membership state. This is the *only* accepted
7
+ * evidence for the committed tiers (T0/T1) and the third option for T2/T3 (`PoW || reputation || parent-ref`).
8
+ *
9
+ * It replaces the interim reputation stand-in for `verifyParentReference` that
10
+ * `cohort-topic-bootstrap-evidence-verifiers` left in `host.ts`.
11
+ *
12
+ * The verifier is **synchronous** (it runs in `member-engine.ts`'s `runGuards` on every register) and
13
+ * **total** (any parse / decode / verify failure on attacker input yields `false`, never a throw). The
14
+ * existence check therefore consults a **synchronous local view** — never a network fetch, which inside an
15
+ * admission gate would itself be a DoS amplifier.
16
+ *
17
+ * Two independent checks, both must pass (`docs/cohort-topic.md` §Anti-DoS bullet 4):
18
+ *
19
+ * 1. **Signed reference (anti-replay).** The participant peer-key-signs the {@link parentRefSigningImage}
20
+ * — the bound tuple extended with `parentTopicId` — so a reference minted for one
21
+ * `(topic, tier, peer, time, parent)` cannot be lifted onto another register. Verified against the
22
+ * participant's own peer key, so it stands alone even in key-less mode (where the outer register
23
+ * signature is absent).
24
+ * 2. **Existence.** The referenced parent topic must exist in locally-available committed / membership
25
+ * state, via the injectable synchronous {@link BootstrapParentTopicView}.
26
+ *
27
+ * A node only admits a parent-ref bootstrap for a parent topic it has *locally cached* a cert/commit for
28
+ * (fail-closed when unknown). That is acceptable for an admission gate: a participant whose parent the node
29
+ * does not know retries / uses PoW (T2/T3); a genuinely-new committed (T0/T1) topic is bootstrapped by
30
+ * nodes that already serve the parent's committed work, which hold its cert. A *richer* check — that the
31
+ * parent's commit certificate names *this* child topic — is the follow-on
32
+ * `cohort-topic-parent-ref-tx-log-content`, not this module.
33
+ */
34
+
35
+ import {
36
+ parseBootstrapEvidenceEnvelope,
37
+ parentRefSigningImage,
38
+ DEFAULT_MAX_NO_POW_TIER,
39
+ b64urlToBytes,
40
+ type RegisterV1,
41
+ type RingCoord,
42
+ } from "@optimystic/db-core";
43
+ import { verifyPeerSig } from "./peer-sig.js";
44
+
45
+ /**
46
+ * A synchronous local view answering "does this node locally know parent topic `parentTopicId` exists?".
47
+ * Backed (in the host default) by the committed / FRET membership state the node already holds — never a
48
+ * network lookup (a round-trip inside an admission gate is a DoS amplifier).
49
+ */
50
+ export interface BootstrapParentTopicView {
51
+ /**
52
+ * True iff the node locally knows parent topic `parentTopicId` exists (a committed cohort serves it /
53
+ * its membership cert is cached) for a cohort at `tier`. `tier` is the registering child's tier and
54
+ * routes the lookup to the committed (T0/T1) or FRET (T2/T3) backing, mirroring the membership source.
55
+ */
56
+ exists(parentTopicId: Uint8Array, tier: number): boolean;
57
+ }
58
+
59
+ /** Inputs to {@link createParentReferenceVerifier}. */
60
+ export interface ParentReferenceVerifierDeps {
61
+ /** The synchronous local existence view the verifier consults (no network I/O). */
62
+ readonly parentTopicView: BootstrapParentTopicView;
63
+ }
64
+
65
+ /**
66
+ * The real, all-tiers `verifyParentReference(reg) => boolean`. Total and synchronous:
67
+ *
68
+ * ```
69
+ * env = parseBootstrapEvidenceEnvelope(reg); if (!env?.parentRef) return false
70
+ * if (parentRef.parentTopicId === reg.topicId) return false // a topic cannot vouch for itself
71
+ * if (!verifyPeerSig(participantCoord, parentRefSigningImage(reg, parentTopicId), parentRef.sig)) return false
72
+ * return parentTopicView.exists(parentTopicId, reg.tier)
73
+ * ```
74
+ *
75
+ * - Absent `parentRef`, a malformed envelope, a bad/non-base64url signature, or an unknown parent → `false`
76
+ * (fails closed → `unwilling_cohort`).
77
+ * - The self-referential guard rejects `parentTopicId === reg.topicId`: a topic cannot reference itself to
78
+ * prove its own existence (a circular bootstrap).
79
+ */
80
+ export function createParentReferenceVerifier(deps: ParentReferenceVerifierDeps): (reg: RegisterV1) => boolean {
81
+ const { parentTopicView } = deps;
82
+ return (reg: RegisterV1): boolean => {
83
+ try {
84
+ const env = parseBootstrapEvidenceEnvelope(reg);
85
+ if (env?.parentRef === undefined) {
86
+ return false; // not offered / not a parent-reference envelope
87
+ }
88
+ const { parentTopicId, sig } = env.parentRef;
89
+ // A topic cannot vouch for its own existence (self-referential / circular bootstrap).
90
+ if (parentTopicId === reg.topicId) {
91
+ return false;
92
+ }
93
+ // 1. Signed reference (anti-replay): the participant binds THIS parent to THIS register.
94
+ const image = parentRefSigningImage(reg, parentTopicId);
95
+ if (!verifyPeerSig(b64urlToBytes(reg.participantCoord), image, b64urlToBytes(sig))) {
96
+ return false;
97
+ }
98
+ // 2. Existence: the parent topic must exist in locally-available committed / membership state.
99
+ return parentTopicView.exists(b64urlToBytes(parentTopicId), reg.tier);
100
+ } catch {
101
+ return false; // any decode / verify failure on attacker input → fail closed
102
+ }
103
+ };
104
+ }
105
+
106
+ /** A synchronous local "is a cohort cached for this coord?" read — the {@link FretMembershipSource} cache shape. */
107
+ export interface LocalCohortExistence {
108
+ /** True iff a `MembershipCertV1` is locally cached for `coord`. */
109
+ has(coord: RingCoord): boolean;
110
+ }
111
+
112
+ /** Inputs to {@link createDefaultParentTopicView}. */
113
+ export interface DefaultParentTopicViewDeps {
114
+ /** The FRET membership cache (T2/T3 backing): a cached cert means a cohort genuinely serves the parent. */
115
+ readonly membershipSource: LocalCohortExistence;
116
+ /** Tier addressing — supplies `coord0(parentTopicId)`, the parent topic's root cohort coordinate. */
117
+ readonly addressing: { coord0(topicId: Uint8Array): RingCoord };
118
+ /**
119
+ * Optional committed-state backing for the committed tiers (T0/T1). Reads `coord_0(parentTopicId)`. When
120
+ * **omitted the committed tiers fail closed** (return `false`) — a FRET-cached cert (T2/T3 membership)
121
+ * must NOT vouch for committed-tier parent existence (committed-tier integrity). A node supplies a real
122
+ * committed-by-coord reader (e.g. a future tx-log / commit-cert index) here when one is available.
123
+ */
124
+ readonly committedReader?: (coord: RingCoord) => boolean;
125
+ /** Highest tier exempt from PoW / served by committed work (T0/T1 → 1). Default {@link DEFAULT_MAX_NO_POW_TIER}. */
126
+ readonly maxNoPowTier?: number;
127
+ }
128
+
129
+ /**
130
+ * The host-default {@link BootstrapParentTopicView}, tier-routed exactly like the membership source
131
+ * (`docs/cohort-topic.md` §Membership source):
132
+ *
133
+ * - **T0/T1 (committed tiers):** consult `committedReader` — the committed-state backing. Absent → `false`
134
+ * (fail closed). A FRET-cached cert never satisfies a committed-tier existence check, so committed-tier
135
+ * integrity holds: a parent known only as a FRET (T2/T3) cohort cannot back a committed parent reference.
136
+ * - **T2/T3:** `membershipSource.has(coord_0(parentTopicId))` — a cached `MembershipCertV1` means a cohort
137
+ * is genuinely serving the parent topic.
138
+ *
139
+ * **Known limitation (interim).** No coord-keyed committed-membership index exists yet (the transaction-log
140
+ * commit certificate is keyed by action, not by `coord_0`), so a node typically wires no `committedReader`
141
+ * and T0/T1 parent-ref existence fails closed. T2/T3 parent-ref is fully real today. The dedicated committed
142
+ * backing is the follow-on `cohort-topic-parent-ref-tx-log-content`.
143
+ */
144
+ export function createDefaultParentTopicView(deps: DefaultParentTopicViewDeps): BootstrapParentTopicView {
145
+ const { membershipSource, addressing, committedReader } = deps;
146
+ const maxNoPowTier = deps.maxNoPowTier ?? DEFAULT_MAX_NO_POW_TIER;
147
+ return {
148
+ exists(parentTopicId: Uint8Array, tier: number): boolean {
149
+ const coord = addressing.coord0(parentTopicId);
150
+ if (tier <= maxNoPowTier) {
151
+ // Committed tiers (T0/T1): require a committed backing; fail closed without one. The FRET cache
152
+ // must not vouch for committed-tier existence (committed-tier integrity).
153
+ return committedReader !== undefined && committedReader(coord);
154
+ }
155
+ // T2/T3: the FRET membership cache reflects a cohort serving the parent topic.
156
+ return membershipSource.has(coord);
157
+ },
158
+ };
159
+ }
@@ -1,109 +1,109 @@
1
- import type { CohortTopicService, CollectionChangeEvent, CollectionChangeListener, CommitCert, IBlockChangeNotifier } from "@optimystic/db-core";
2
- import { createLogger } from "../logger.js";
3
-
4
- const log = createLogger('cohort-change-bridge');
5
-
6
- /**
7
- * The local commit feed the bridge observes: a {@link IBlockChangeNotifier} (the node's
8
- * `StorageRepo`) extended with the catch-all {@link onAnyCollectionChange} the bridge needs to see
9
- * EVERY commit — it cannot enumerate collection ids ahead of time to subscribe per-collection, and
10
- * origination must fire whether or not anyone subscribed to that collection.
11
- */
12
- export interface ChangeBridgeSource extends IBlockChangeNotifier {
13
- onAnyCollectionChange(listener: CollectionChangeListener): () => void;
14
- }
15
-
16
- export interface CohortTopicChangeNotifierDeps {
17
- /** The local node's commit feed (its `StorageRepo`). */
18
- readonly source: ChangeBridgeSource;
19
- /** The cohort-topic substrate whose `onLocalCommit` origination hook receives member commits. */
20
- readonly service: CohortTopicService;
21
- /**
22
- * True iff this node is a cohort member responsible for this change event's reactivity-topic fan-out.
23
- * Receives the whole {@link CollectionChangeEvent} (not just the collection id) because the reactivity
24
- * topic is tail-anchored — `H(event.tailId ‖ "reactivity")` — so the gate needs `event.tailId` to
25
- * derive the topic's `coord_0` cohort. A tail-less event (a read-driven promotion) is never a member.
26
- */
27
- readonly selfIsCohortMember: (event: CollectionChangeEvent) => boolean;
28
- /** Resolve the pass-through commit cert for a change event (e.g. the cluster commit-cert store). */
29
- readonly extractCommitCert: (event: CollectionChangeEvent) => CommitCert | undefined;
30
- }
31
-
32
- /**
33
- * The reactivity/matchmaking **origination point**: bridge the local single-node change-notifier
34
- * primitive into the networked cohort-topic substrate.
35
- *
36
- * On EVERY commit landing on this node (via the catch-all {@link ChangeBridgeSource.onAnyCollectionChange}
37
- * feed), if this node is a cohort member for the collection's reactivity topic, the bridge hands the
38
- * `CollectionChangeEvent` plus the pass-through {@link CommitCert} to `service.onLocalCommit` —
39
- * reactivity reuses the commit cert's threshold signature directly and never re-signs. A commit on a
40
- * non-member node (no fan-out responsibility) or one for which no cert is retained (nothing
41
- * authoritative to forward) is a no-op. A throwing downstream hook is isolated + logged so
42
- * origination can never break the commit (matching the {@link IBlockChangeNotifier} listener contract).
43
- *
44
- * The returned value IS an {@link IBlockChangeNotifier}: it is what `network-transactor` takes as its
45
- * `localChangeNotifier`, so per-collection {@link IBlockChangeNotifier.onCollectionChange} subscribers
46
- * (e.g. the Quereus reactive-watch vtab) keep working — those subscriptions delegate straight to
47
- * `source`. Origination runs independently on the catch-all feed.
48
- *
49
- * `makeCohortTopicChangeNotifier` discards the catch-all unsubscribe (node-lifetime by intent) for the
50
- * unit tests; the node assembly uses {@link attachCohortChangeBridge}, which returns the teardown.
51
- */
52
- export function makeCohortTopicChangeNotifier(deps: CohortTopicChangeNotifierDeps): IBlockChangeNotifier {
53
- return buildCohortTopicChangeBridge(deps).notifier;
54
- }
55
-
56
- /**
57
- * Shared wiring behind both entry points: subscribe the origination handler to the source's catch-all
58
- * commit feed and build the decorating {@link IBlockChangeNotifier}. Returns the notifier plus the
59
- * idempotent teardown for that catch-all subscription (the underlying `onAnyCollectionChange` unsub is
60
- * itself idempotent), so the node assembly can release it on node stop alongside `host.stop()`.
61
- */
62
- function buildCohortTopicChangeBridge(deps: CohortTopicChangeNotifierDeps): { notifier: IBlockChangeNotifier; unsubscribe: () => void } {
63
- const unsubscribe = deps.source.onAnyCollectionChange((event) => originate(deps, event));
64
- const notifier: IBlockChangeNotifier = {
65
- onCollectionChange: (collectionId, listener): (() => void) => deps.source.onCollectionChange(collectionId, listener),
66
- };
67
- return { notifier, unsubscribe };
68
- }
69
-
70
- /** Run the membership gate, cert extraction, and origination hook for one change event, isolating throws. */
71
- function originate(deps: CohortTopicChangeNotifierDeps, event: CollectionChangeEvent): void {
72
- try {
73
- if (!deps.selfIsCohortMember(event)) {
74
- return; // not responsible for this topic's fan-out
75
- }
76
- const hook = deps.service.onLocalCommit;
77
- if (!hook) {
78
- return; // no reactivity/matchmaking consumer has attached an origination handler yet
79
- }
80
- const commitCert = deps.extractCommitCert(event);
81
- if (!commitCert) {
82
- return; // nothing authoritative to forward; never fabricate an unsigned cert (extractor logs why)
83
- }
84
- hook(event, commitCert);
85
- } catch (err) {
86
- log('origination hook threw for collection=%s rev=%d: %o', event.collectionId, event.rev, err);
87
- }
88
- }
89
-
90
- /**
91
- * Wire the cohort-topic origination bridge as `node`'s `blockChangeNotifier` (the value the
92
- * `NetworkTransactor` consumes as its `localChangeNotifier`). Call this from the node assembly once a
93
- * {@link CohortTopicService} is running on the node, passing the node's `StorageRepo` as `source` and
94
- * the membership + cert-extraction seams.
95
- *
96
- * Returns the installed `notifier` plus an idempotent `unsubscribe` that tears down the catch-all
97
- * origination subscription — the node assembly calls it on node stop (alongside `host.stop()`) so the
98
- * origination feed is released before the node's transports close. Tearing it down stops further
99
- * origination but leaves the per-collection `onCollectionChange` delegation intact (those subscriptions
100
- * live on the `source` `StorageRepo`, not on the bridge).
101
- */
102
- export function attachCohortChangeBridge(
103
- node: { blockChangeNotifier?: IBlockChangeNotifier },
104
- deps: CohortTopicChangeNotifierDeps,
105
- ): { notifier: IBlockChangeNotifier; unsubscribe: () => void } {
106
- const bridge = buildCohortTopicChangeBridge(deps);
107
- node.blockChangeNotifier = bridge.notifier;
108
- return bridge;
109
- }
1
+ import type { CohortTopicService, CollectionChangeEvent, CollectionChangeListener, CommitCert, IBlockChangeNotifier } from "@optimystic/db-core";
2
+ import { createLogger } from "../logger.js";
3
+
4
+ const log = createLogger('cohort-change-bridge');
5
+
6
+ /**
7
+ * The local commit feed the bridge observes: a {@link IBlockChangeNotifier} (the node's
8
+ * `StorageRepo`) extended with the catch-all {@link onAnyCollectionChange} the bridge needs to see
9
+ * EVERY commit — it cannot enumerate collection ids ahead of time to subscribe per-collection, and
10
+ * origination must fire whether or not anyone subscribed to that collection.
11
+ */
12
+ export interface ChangeBridgeSource extends IBlockChangeNotifier {
13
+ onAnyCollectionChange(listener: CollectionChangeListener): () => void;
14
+ }
15
+
16
+ export interface CohortTopicChangeNotifierDeps {
17
+ /** The local node's commit feed (its `StorageRepo`). */
18
+ readonly source: ChangeBridgeSource;
19
+ /** The cohort-topic substrate whose `onLocalCommit` origination hook receives member commits. */
20
+ readonly service: CohortTopicService;
21
+ /**
22
+ * True iff this node is a cohort member responsible for this change event's reactivity-topic fan-out.
23
+ * Receives the whole {@link CollectionChangeEvent} (not just the collection id) because the reactivity
24
+ * topic is tail-anchored — `H(event.tailId ‖ "reactivity")` — so the gate needs `event.tailId` to
25
+ * derive the topic's `coord_0` cohort. A tail-less event (a read-driven promotion) is never a member.
26
+ */
27
+ readonly selfIsCohortMember: (event: CollectionChangeEvent) => boolean;
28
+ /** Resolve the pass-through commit cert for a change event (e.g. the cluster commit-cert store). */
29
+ readonly extractCommitCert: (event: CollectionChangeEvent) => CommitCert | undefined;
30
+ }
31
+
32
+ /**
33
+ * The reactivity/matchmaking **origination point**: bridge the local single-node change-notifier
34
+ * primitive into the networked cohort-topic substrate.
35
+ *
36
+ * On EVERY commit landing on this node (via the catch-all {@link ChangeBridgeSource.onAnyCollectionChange}
37
+ * feed), if this node is a cohort member for the collection's reactivity topic, the bridge hands the
38
+ * `CollectionChangeEvent` plus the pass-through {@link CommitCert} to `service.onLocalCommit` —
39
+ * reactivity reuses the commit cert's threshold signature directly and never re-signs. A commit on a
40
+ * non-member node (no fan-out responsibility) or one for which no cert is retained (nothing
41
+ * authoritative to forward) is a no-op. A throwing downstream hook is isolated + logged so
42
+ * origination can never break the commit (matching the {@link IBlockChangeNotifier} listener contract).
43
+ *
44
+ * The returned value IS an {@link IBlockChangeNotifier}: it is what `network-transactor` takes as its
45
+ * `localChangeNotifier`, so per-collection {@link IBlockChangeNotifier.onCollectionChange} subscribers
46
+ * (e.g. the Quereus reactive-watch vtab) keep working — those subscriptions delegate straight to
47
+ * `source`. Origination runs independently on the catch-all feed.
48
+ *
49
+ * `makeCohortTopicChangeNotifier` discards the catch-all unsubscribe (node-lifetime by intent) for the
50
+ * unit tests; the node assembly uses {@link attachCohortChangeBridge}, which returns the teardown.
51
+ */
52
+ export function makeCohortTopicChangeNotifier(deps: CohortTopicChangeNotifierDeps): IBlockChangeNotifier {
53
+ return buildCohortTopicChangeBridge(deps).notifier;
54
+ }
55
+
56
+ /**
57
+ * Shared wiring behind both entry points: subscribe the origination handler to the source's catch-all
58
+ * commit feed and build the decorating {@link IBlockChangeNotifier}. Returns the notifier plus the
59
+ * idempotent teardown for that catch-all subscription (the underlying `onAnyCollectionChange` unsub is
60
+ * itself idempotent), so the node assembly can release it on node stop alongside `host.stop()`.
61
+ */
62
+ function buildCohortTopicChangeBridge(deps: CohortTopicChangeNotifierDeps): { notifier: IBlockChangeNotifier; unsubscribe: () => void } {
63
+ const unsubscribe = deps.source.onAnyCollectionChange((event) => originate(deps, event));
64
+ const notifier: IBlockChangeNotifier = {
65
+ onCollectionChange: (collectionId, listener): (() => void) => deps.source.onCollectionChange(collectionId, listener),
66
+ };
67
+ return { notifier, unsubscribe };
68
+ }
69
+
70
+ /** Run the membership gate, cert extraction, and origination hook for one change event, isolating throws. */
71
+ function originate(deps: CohortTopicChangeNotifierDeps, event: CollectionChangeEvent): void {
72
+ try {
73
+ if (!deps.selfIsCohortMember(event)) {
74
+ return; // not responsible for this topic's fan-out
75
+ }
76
+ const hook = deps.service.onLocalCommit;
77
+ if (!hook) {
78
+ return; // no reactivity/matchmaking consumer has attached an origination handler yet
79
+ }
80
+ const commitCert = deps.extractCommitCert(event);
81
+ if (!commitCert) {
82
+ return; // nothing authoritative to forward; never fabricate an unsigned cert (extractor logs why)
83
+ }
84
+ hook(event, commitCert);
85
+ } catch (err) {
86
+ log('origination hook threw for collection=%s rev=%d: %o', event.collectionId, event.rev, err);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Wire the cohort-topic origination bridge as `node`'s `blockChangeNotifier` (the value the
92
+ * `NetworkTransactor` consumes as its `localChangeNotifier`). Call this from the node assembly once a
93
+ * {@link CohortTopicService} is running on the node, passing the node's `StorageRepo` as `source` and
94
+ * the membership + cert-extraction seams.
95
+ *
96
+ * Returns the installed `notifier` plus an idempotent `unsubscribe` that tears down the catch-all
97
+ * origination subscription — the node assembly calls it on node stop (alongside `host.stop()`) so the
98
+ * origination feed is released before the node's transports close. Tearing it down stops further
99
+ * origination but leaves the per-collection `onCollectionChange` delegation intact (those subscriptions
100
+ * live on the `source` `StorageRepo`, not on the bridge).
101
+ */
102
+ export function attachCohortChangeBridge(
103
+ node: { blockChangeNotifier?: IBlockChangeNotifier },
104
+ deps: CohortTopicChangeNotifierDeps,
105
+ ): { notifier: IBlockChangeNotifier; unsubscribe: () => void } {
106
+ const bridge = buildCohortTopicChangeBridge(deps);
107
+ node.blockChangeNotifier = bridge.notifier;
108
+ return bridge;
109
+ }