@optimystic/db-p2p 0.22.0 → 0.24.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 (177) 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/stream-util.d.ts +22 -6
  14. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  15. package/dist/src/cohort-topic/stream-util.js +56 -10
  16. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  17. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  18. package/dist/src/dispute/dispute-service.js +9 -3
  19. package/dist/src/dispute/dispute-service.js.map +1 -1
  20. package/dist/src/index.d.ts +3 -0
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +3 -0
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/libp2p-key-network.d.ts +88 -2
  25. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  26. package/dist/src/libp2p-key-network.js +134 -28
  27. package/dist/src/libp2p-key-network.js.map +1 -1
  28. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  29. package/dist/src/libp2p-node-base.js +25 -1
  30. package/dist/src/libp2p-node-base.js.map +1 -1
  31. package/dist/src/logger.d.ts +17 -1
  32. package/dist/src/logger.d.ts.map +1 -1
  33. package/dist/src/logger.js +19 -2
  34. package/dist/src/logger.js.map +1 -1
  35. package/dist/src/owned-block-seed.d.ts +6 -3
  36. package/dist/src/owned-block-seed.d.ts.map +1 -1
  37. package/dist/src/owned-block-seed.js +16 -3
  38. package/dist/src/owned-block-seed.js.map +1 -1
  39. package/dist/src/peer-address-book.d.ts +72 -0
  40. package/dist/src/peer-address-book.d.ts.map +1 -0
  41. package/dist/src/peer-address-book.js +123 -0
  42. package/dist/src/peer-address-book.js.map +1 -0
  43. package/dist/src/repo/client.d.ts.map +1 -1
  44. package/dist/src/repo/client.js +11 -2
  45. package/dist/src/repo/client.js.map +1 -1
  46. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  47. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  48. package/dist/src/repo/cluster-coordinator.js +95 -3
  49. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  50. package/dist/src/repo/coordinator-repo.d.ts +62 -9
  51. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  52. package/dist/src/repo/coordinator-repo.js +242 -73
  53. package/dist/src/repo/coordinator-repo.js.map +1 -1
  54. package/dist/src/rn.d.ts +3 -0
  55. package/dist/src/rn.d.ts.map +1 -1
  56. package/dist/src/rn.js +3 -0
  57. package/dist/src/rn.js.map +1 -1
  58. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  59. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  60. package/dist/src/storage/cached-raw-storage.js +152 -0
  61. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  62. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  63. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  64. package/dist/src/storage/cached-store-driver.js +775 -0
  65. package/dist/src/storage/cached-store-driver.js.map +1 -0
  66. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  67. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  68. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  69. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  70. package/dist/src/storage/shared-cache-pool.js +354 -0
  71. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  72. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  73. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  74. package/dist/src/testing/raw-storage-conformance.js +35 -2
  75. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  76. package/package.json +3 -3
  77. package/readme.md +668 -668
  78. package/src/cluster/block-transfer.ts +424 -424
  79. package/src/cluster/client.ts +119 -88
  80. package/src/cluster/cluster-error.ts +64 -64
  81. package/src/cluster/cluster-policy.ts +203 -203
  82. package/src/cluster/cluster-repo.ts +242 -122
  83. package/src/cluster/cluster-size-coupling.ts +45 -45
  84. package/src/cluster/commit-cert.ts +139 -139
  85. package/src/cluster/i-transaction-state-store.ts +43 -43
  86. package/src/cluster/memory-transaction-state-store.ts +56 -56
  87. package/src/cluster/peer-key-binding.ts +37 -37
  88. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  89. package/src/cluster/quorum-restore.ts +223 -223
  90. package/src/cluster/reconcile-block.ts +203 -203
  91. package/src/cluster/service.ts +293 -241
  92. package/src/cluster/supermajority-coupling.ts +37 -37
  93. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  94. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  95. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  96. package/src/cohort-topic/change-bridge.ts +109 -109
  97. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  98. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  99. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  100. package/src/cohort-topic/host.ts +2901 -2901
  101. package/src/cohort-topic/index.ts +13 -13
  102. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  103. package/src/cohort-topic/membership-source.ts +68 -68
  104. package/src/cohort-topic/peer-codec.ts +31 -31
  105. package/src/cohort-topic/peer-sig.ts +86 -86
  106. package/src/cohort-topic/protocols.ts +71 -71
  107. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  108. package/src/cohort-topic/size-estimator.ts +16 -16
  109. package/src/cohort-topic/stream-util.ts +135 -87
  110. package/src/cohort-topic/threshold-crypto.ts +239 -239
  111. package/src/cohort-topic/topic-router.ts +77 -77
  112. package/src/dispute/arbitrator-selection.ts +138 -138
  113. package/src/dispute/cascade.ts +524 -524
  114. package/src/dispute/dispute-service.ts +11 -5
  115. package/src/dispute/invalidation.ts +625 -625
  116. package/src/inbound-authorization.ts +190 -190
  117. package/src/index.ts +52 -49
  118. package/src/libp2p-key-network.ts +1120 -990
  119. package/src/libp2p-node-base.ts +1675 -1651
  120. package/src/libp2p-node-rn.ts +30 -30
  121. package/src/libp2p-node.ts +36 -36
  122. package/src/logger.ts +19 -2
  123. package/src/matchmaking/aggregate-counts.ts +104 -104
  124. package/src/matchmaking/index.ts +20 -20
  125. package/src/matchmaking/module.ts +363 -363
  126. package/src/matchmaking/protocols.ts +51 -51
  127. package/src/matchmaking/provider-manager.ts +95 -95
  128. package/src/matchmaking/query-handler.ts +88 -88
  129. package/src/matchmaking/query-transport.ts +492 -492
  130. package/src/matchmaking/seeker-manager.ts +64 -64
  131. package/src/matchmaking/seeker-walk-client.ts +293 -293
  132. package/src/matchmaking/traffic-validation.ts +195 -195
  133. package/src/optimystic-node.ts +36 -36
  134. package/src/owned-block-seed.ts +53 -40
  135. package/src/peer-address-book.ts +149 -0
  136. package/src/protocol-limits.ts +33 -33
  137. package/src/reactivity/forwarder-host.ts +438 -438
  138. package/src/reactivity/index.ts +19 -19
  139. package/src/reactivity/notify-transport.ts +144 -144
  140. package/src/reactivity/origination-manager.ts +192 -192
  141. package/src/reactivity/protocols.ts +61 -61
  142. package/src/reactivity/push-state-gossip.ts +291 -291
  143. package/src/reactivity/recover-transport.ts +408 -408
  144. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  145. package/src/reactivity/subscriber-registry.ts +96 -96
  146. package/src/reactivity/subscription-manager.ts +450 -450
  147. package/src/reactivity/topic-bytes.ts +37 -37
  148. package/src/repo/client.ts +12 -2
  149. package/src/repo/cluster-coordinator.ts +99 -3
  150. package/src/repo/coordinator-repo.ts +281 -74
  151. package/src/repo/types.ts +7 -7
  152. package/src/rn.ts +39 -36
  153. package/src/rpc-deadline.ts +45 -45
  154. package/src/storage/arachnode-partition.ts +74 -74
  155. package/src/storage/cached-raw-storage.ts +180 -0
  156. package/src/storage/cached-store-driver.ts +859 -0
  157. package/src/storage/i-kv-store.ts +8 -8
  158. package/src/storage/i-raw-storage.ts +12 -5
  159. package/src/storage/kv-raw-storage.ts +135 -135
  160. package/src/storage/memory-kv-store.ts +28 -28
  161. package/src/storage/memory-storage.ts +25 -25
  162. package/src/storage/memory-store-driver.ts +157 -157
  163. package/src/storage/raw-store-codec.ts +42 -42
  164. package/src/storage/raw-store-driver.ts +80 -80
  165. package/src/storage/ring-selector.ts +317 -317
  166. package/src/storage/ring-shift-coordinator.ts +271 -271
  167. package/src/storage/shared-cache-pool.ts +452 -0
  168. package/src/storage/storage-repo.ts +1014 -1014
  169. package/src/testing/cohort-topic-mesh-harness.ts +663 -663
  170. package/src/testing/index.ts +8 -8
  171. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  172. package/src/testing/raw-storage-conformance.ts +453 -417
  173. package/src/testing/reactivity-mesh-harness.ts +922 -922
  174. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  175. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  176. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  177. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
@@ -1,203 +1,203 @@
1
- import { DEFAULT_SUPER_MAJORITY_THRESHOLD, type ClusterConsensusConfig } from "@optimystic/db-core";
2
- import { createLogger } from "../logger.js";
3
- import { CORROBORATION_FLOOR } from "./quorum-restore.js";
4
-
5
- const log = createLogger('cluster-policy');
6
-
7
- /**
8
- * Resolves the operator-facing cluster knobs (`clusterSize`, `clusterPolicy.*`) into the concrete
9
- * numbers the consensus and block-restoration paths run on.
10
- *
11
- * Extracted from `createLibp2pNodeBase` rather than left inline so the composition root's defaults
12
- * are assertable without booting a libp2p node — the layer a real deployment actually uses, and
13
- * therefore the layer where a default that relaxed the repair corroboration floor to a single voter
14
- * survived unnoticed (see `test/cluster-policy.spec.ts`).
15
- *
16
- * ## Why two size yardsticks, not one
17
- *
18
- * One operator field — `clusterPolicy.assumedClusterSize`, "the smallest cohort this deployment can
19
- * genuinely field" — feeds two consumers whose failure modes point in opposite directions, so its
20
- * *default* cannot serve both:
21
- *
22
- * - **Membership admission gate** (`cluster/cluster-repo.ts`, `admitMembership`) reads it only on its
23
- * fallback path, when this node has no confident network-size estimate. Too small: a
24
- * partition-induced downsize slips past while the node is unconfident. Too large: the node refuses
25
- * legitimate writes — unavailability. It wants a *permissive* default, because an unconfigured
26
- * two-node mesh must still be able to transact. It gets {@link minAbsoluteClusterSize} (2).
27
- * - **Repair corroboration floor** (`corroboratorCapacity` in `cluster/quorum-restore.ts`, called by
28
- * `CoordinatorRepo.queryClusterForLatest` and `createReconcileBlock`) reads it on *every* repair,
29
- * unconditionally. Too small: a shrunken — and always unauthenticated — cohort view buys a lone
30
- * peer full trust. Too large: a block stays unrepaired, degraded rather than dead. It wants a
31
- * *strict* default. It gets {@link ResolvedClusterPolicy.repairCorroborationClusterSize}, which
32
- * falls back to `clusterSize` (the configured replication factor).
33
- *
34
- * A single explicit `clusterPolicy.assumedClusterSize` still sets BOTH — an operator declaring their
35
- * real cohort size means it for both consumers. Only the unconfigured case diverges.
36
- *
37
- * So a genuine two-node mesh needs exactly one setting to self-repair: either
38
- * `clusterPolicy.assumedClusterSize: 2` (which does not lower the replication factor) or an honest
39
- * `clusterSize: 2`. Writes and voting still work with zero configuration.
40
- *
41
- * ## Future
42
- *
43
- * Deriving the yardstick from observation (the largest peer group this node has ever seen for the
44
- * key) would remove the trade entirely and subsume both values. Filed as backlog
45
- * `feat-admission-floor-from-observed-cohort-high-water-mark`; do not build it here.
46
- */
47
-
48
- /**
49
- * Absolute floor below which no cohort is safe, whatever the size references say. Named rather than
50
- * inlined because the admission gate's `assumedClusterSize` defaults to exactly this value — the two
51
- * must not drift.
52
- */
53
- export const minAbsoluteClusterSize = 2;
54
-
55
- /**
56
- * Default replication factor / target cohort breadth when the operator declares no `clusterSize`.
57
- *
58
- * Exported (and re-exported from the package root) rather than left inline because a caller that
59
- * must construct a `Libp2pKeyPeerNetwork` for a node it did not build has to state a cluster size —
60
- * the constructor no longer supplies one — and the only defensible answer is "whatever a node built
61
- * here would have resolved to". Repeating the literal is how the two drifted last time.
62
- */
63
- export const DEFAULT_CLUSTER_SIZE = 10;
64
-
65
- /**
66
- * The operator-facing cluster knobs. `NodeOptions` (`libp2p-node-base.ts`) intersects this rather
67
- * than restating it, so a knob added here is one `resolveClusterPolicy` is guaranteed to see — a
68
- * second declaration would compile fine and be silently dropped.
69
- */
70
- export interface ClusterPolicyOptions {
71
- /**
72
- * Desired cluster size per key (default 10) — the replication factor / target cohort breadth
73
- * the coordinator aims for. NOT a statement about how many peers actually exist, so the
74
- * membership admission gate is never measured against it (see `cluster/cluster-repo.ts`).
75
- *
76
- * The read-repair/reconcile corroboration floor DOES fall back to it when
77
- * `clusterPolicy.assumedClusterSize` is absent — the strict direction, so an unconfigured node
78
- * cannot have its floor talked down by a shrunken cohort view. A deployment that genuinely runs
79
- * fewer peers than this should declare `clusterPolicy.assumedClusterSize`.
80
- */
81
- clusterSize?: number;
82
- clusterPolicy?: {
83
- allowDownsize?: boolean;
84
- /** Acceptable relative difference (e.g. 0.5 = +/-50%). */
85
- sizeTolerance?: number;
86
- /** Fraction of peers needed for super-majority (default {@link DEFAULT_SUPER_MAJORITY_THRESHOLD}). */
87
- superMajorityThreshold?: number;
88
- /**
89
- * Opt in to transacting below the safe cluster-size floor when FRET has no confident
90
- * network-size estimate — the membership-admission and coordinator small-cluster gates both
91
- * fail closed without it. Default false. Turn on only for single-node / local dev meshes that
92
- * knowingly run undersized.
93
- */
94
- allowUnvalidatedSmallCluster?: boolean;
95
- /**
96
- * The smallest cohort this deployment can genuinely field — normally the number of nodes you
97
- * actually run, capped at `clusterSize`. Two consumers read it: the membership admission gate,
98
- * on its fallback path when the node has no confident network-size estimate; and the
99
- * read-repair/reconcile corroboration floor (`corroboratorCapacity`), unconditionally.
100
- *
101
- * Declaring it sets BOTH. Leaving it unset does NOT — see the module doc for why the two
102
- * defaults point in opposite directions. A large deployment should still set this to its real
103
- * cohort size, otherwise the admission gate cannot police a partition-induced downsize while
104
- * its size estimate is unconfident; a genuine two-node mesh needs it (or an honest
105
- * `clusterSize: 2`) to self-repair.
106
- */
107
- assumedClusterSize?: number;
108
- };
109
- }
110
-
111
- /** Everything a node's consensus + restoration paths need, with every default already applied. */
112
- export type ResolvedClusterPolicy = ClusterConsensusConfig & {
113
- /** Replication factor / target cohort breadth. Always concrete after resolution. */
114
- clusterSize: number;
115
- /**
116
- * Yardstick the repair corroboration floor measures a (possibly shrunken, always unauthenticated)
117
- * cohort view against — see `corroboratorCapacity` in `cluster/quorum-restore.ts`.
118
- *
119
- * Deliberately distinct from {@link ClusterConsensusConfig.assumedClusterSize}, which the
120
- * membership admission gate reads: the two share an operator field but not a default, because
121
- * over- and under-stating them cost opposite things. See the module doc.
122
- */
123
- repairCorroborationClusterSize: number;
124
- };
125
-
126
- /**
127
- * Apply every cluster-policy default a node needs. Same options in, same numbers out, so the
128
- * composition root's behavior is unit-testable (`test/cluster-policy.spec.ts`). Its one side effect
129
- * is the `assumed-cluster-size-unset` advisory below, which lives here because this is the only place
130
- * that knows the resolution produced a self-defeating combination.
131
- */
132
- export function resolveClusterPolicy(options: ClusterPolicyOptions): ResolvedClusterPolicy {
133
- // undefined here means "the operator said nothing", which is the only case where the two
134
- // yardsticks below diverge.
135
- //
136
- // NOTE: a declared value is passed through unvalidated. The admission gate floors a degenerate one
137
- // (0, negative, NaN, Infinity) itself — see `cluster-repo.admissionFloor` and its specs — but
138
- // `corroboratorCapacity` does not: NaN there makes every quorum comparison false, so repair
139
- // silently declines forever. Fail-safe, and unreachable through the reference-peer CLI (which
140
- // rejects non-positive integers). If another composition root starts accepting unvalidated config,
141
- // clamp here rather than in each consumer.
142
- const declaredCohortSize = options.clusterPolicy?.assumedClusterSize;
143
- const clusterSize = options.clusterSize ?? DEFAULT_CLUSTER_SIZE;
144
- const repairCorroborationClusterSize = declaredCohortSize ?? clusterSize;
145
-
146
- // Called once per node (resolveClusterPolicy runs once at construction), so this fires once per
147
- // node startup, not per repair — a per-attempt warn on a busy node would be noise that gets
148
- // filtered, defeating the point. Fires purely off configuration (not an observed cohort), so a
149
- // deployment that genuinely runs `clusterSize` machines sees it too; worded as a conditional
150
- // ("if you run fewer than N machines") rather than a fault for exactly that reason.
151
- //
152
- // The machine count in the message is CORROBORATION_FLOOR + 1, NOT repairCorroborationClusterSize:
153
- // `corroboratorCapacity` caps only the FLOOR of two (`quorum-restore.ts`), so a cohort with two
154
- // peers besides the reader meets it whatever the declared size. What an undeclared size costs is
155
- // the relaxation below two, which is only reachable when repairCorroborationClusterSize <= 2.
156
- // NOTE: only the UNDECLARED case warns. An operator who declares an assumedClusterSize larger than
157
- // the cohort they actually run is equally unable to repair and gets no warning — deliberate, since
158
- // a declaration is an explicit assertion and this function has no observed cohort to contradict it
159
- // with. If `feat-admission-floor-from-observed-cohort-high-water-mark` ever lands (deriving the
160
- // yardstick from observation), that warning becomes cheap and worth adding here.
161
- if (declaredCohortSize === undefined && clusterSize > minAbsoluteClusterSize) {
162
- const minimumSelfHealingDeployment = CORROBORATION_FLOOR + 1;
163
- log('assumed-cluster-size-unset', {
164
- clusterSize,
165
- repairCorroborationClusterSize,
166
- corroborationFloor: CORROBORATION_FLOOR,
167
- minimumSelfHealingDeployment,
168
- message:
169
- `No clusterPolicy.assumedClusterSize declared: block repair (read-repair and reconcile) requires ` +
170
- `${CORROBORATION_FLOOR} distinct corroborating peers other than the reader, and that floor is ` +
171
- `relaxed only for a cohort that DECLARES it is smaller — with ` +
172
- `repairCorroborationClusterSize=${repairCorroborationClusterSize} it never relaxes. So a deployment ` +
173
- `that actually runs fewer than ${minimumSelfHealingDeployment} machines can never supply the floor ` +
174
- `and every repair declines, permanently. If you run fewer than ${minimumSelfHealingDeployment} ` +
175
- `machines, set clusterPolicy.assumedClusterSize to your real cohort size; it does not lower ` +
176
- `clusterSize=${clusterSize} (the replication factor). Larger deployments can ignore this.`
177
- });
178
- }
179
-
180
- return {
181
- superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
182
- simpleMajorityThreshold: 0.51,
183
- minAbsoluteClusterSize,
184
- allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
185
- clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
186
- // Fail closed by default (an undersized cluster with no confident network-size estimate is
187
- // rejected); embedders running knowingly-small meshes opt in through clusterPolicy.
188
- allowUnvalidatedSmallCluster: options.clusterPolicy?.allowUnvalidatedSmallCluster ?? false,
189
- partitionDetectionWindow: 60000,
190
- // Replication factor / target cohort breadth — what the coordinator aims for when selecting a
191
- // cohort. Deliberately NOT the membership admission gate's yardstick: it says nothing about how
192
- // many peers actually exist, so an unconfigured small mesh would refuse every write.
193
- clusterSize,
194
- // Membership admission gate, fallback path only (no confident network-size estimate). Defaults
195
- // permissive so a two- or three-node mesh transacts unconfigured; the cost of that default is
196
- // bounded to the gate, since the repair floor no longer reads this field.
197
- assumedClusterSize: declaredCohortSize ?? minAbsoluteClusterSize,
198
- // Repair corroboration floor, every repair. Defaults strict — to the replication factor — so an
199
- // unconfigured node cannot have its floor talked down to a single voter by a shrunken cohort
200
- // view. A genuinely small mesh declares its size (either field) to regain self-repair.
201
- repairCorroborationClusterSize
202
- };
203
- }
1
+ import { DEFAULT_SUPER_MAJORITY_THRESHOLD, type ClusterConsensusConfig } from "@optimystic/db-core";
2
+ import { createLogger } from "../logger.js";
3
+ import { CORROBORATION_FLOOR } from "./quorum-restore.js";
4
+
5
+ const log = createLogger('cluster-policy');
6
+
7
+ /**
8
+ * Resolves the operator-facing cluster knobs (`clusterSize`, `clusterPolicy.*`) into the concrete
9
+ * numbers the consensus and block-restoration paths run on.
10
+ *
11
+ * Extracted from `createLibp2pNodeBase` rather than left inline so the composition root's defaults
12
+ * are assertable without booting a libp2p node — the layer a real deployment actually uses, and
13
+ * therefore the layer where a default that relaxed the repair corroboration floor to a single voter
14
+ * survived unnoticed (see `test/cluster-policy.spec.ts`).
15
+ *
16
+ * ## Why two size yardsticks, not one
17
+ *
18
+ * One operator field — `clusterPolicy.assumedClusterSize`, "the smallest cohort this deployment can
19
+ * genuinely field" — feeds two consumers whose failure modes point in opposite directions, so its
20
+ * *default* cannot serve both:
21
+ *
22
+ * - **Membership admission gate** (`cluster/cluster-repo.ts`, `admitMembership`) reads it only on its
23
+ * fallback path, when this node has no confident network-size estimate. Too small: a
24
+ * partition-induced downsize slips past while the node is unconfident. Too large: the node refuses
25
+ * legitimate writes — unavailability. It wants a *permissive* default, because an unconfigured
26
+ * two-node mesh must still be able to transact. It gets {@link minAbsoluteClusterSize} (2).
27
+ * - **Repair corroboration floor** (`corroboratorCapacity` in `cluster/quorum-restore.ts`, called by
28
+ * `CoordinatorRepo.queryClusterForLatest` and `createReconcileBlock`) reads it on *every* repair,
29
+ * unconditionally. Too small: a shrunken — and always unauthenticated — cohort view buys a lone
30
+ * peer full trust. Too large: a block stays unrepaired, degraded rather than dead. It wants a
31
+ * *strict* default. It gets {@link ResolvedClusterPolicy.repairCorroborationClusterSize}, which
32
+ * falls back to `clusterSize` (the configured replication factor).
33
+ *
34
+ * A single explicit `clusterPolicy.assumedClusterSize` still sets BOTH — an operator declaring their
35
+ * real cohort size means it for both consumers. Only the unconfigured case diverges.
36
+ *
37
+ * So a genuine two-node mesh needs exactly one setting to self-repair: either
38
+ * `clusterPolicy.assumedClusterSize: 2` (which does not lower the replication factor) or an honest
39
+ * `clusterSize: 2`. Writes and voting still work with zero configuration.
40
+ *
41
+ * ## Future
42
+ *
43
+ * Deriving the yardstick from observation (the largest peer group this node has ever seen for the
44
+ * key) would remove the trade entirely and subsume both values. Filed as backlog
45
+ * `feat-admission-floor-from-observed-cohort-high-water-mark`; do not build it here.
46
+ */
47
+
48
+ /**
49
+ * Absolute floor below which no cohort is safe, whatever the size references say. Named rather than
50
+ * inlined because the admission gate's `assumedClusterSize` defaults to exactly this value — the two
51
+ * must not drift.
52
+ */
53
+ export const minAbsoluteClusterSize = 2;
54
+
55
+ /**
56
+ * Default replication factor / target cohort breadth when the operator declares no `clusterSize`.
57
+ *
58
+ * Exported (and re-exported from the package root) rather than left inline because a caller that
59
+ * must construct a `Libp2pKeyPeerNetwork` for a node it did not build has to state a cluster size —
60
+ * the constructor no longer supplies one — and the only defensible answer is "whatever a node built
61
+ * here would have resolved to". Repeating the literal is how the two drifted last time.
62
+ */
63
+ export const DEFAULT_CLUSTER_SIZE = 10;
64
+
65
+ /**
66
+ * The operator-facing cluster knobs. `NodeOptions` (`libp2p-node-base.ts`) intersects this rather
67
+ * than restating it, so a knob added here is one `resolveClusterPolicy` is guaranteed to see — a
68
+ * second declaration would compile fine and be silently dropped.
69
+ */
70
+ export interface ClusterPolicyOptions {
71
+ /**
72
+ * Desired cluster size per key (default 10) — the replication factor / target cohort breadth
73
+ * the coordinator aims for. NOT a statement about how many peers actually exist, so the
74
+ * membership admission gate is never measured against it (see `cluster/cluster-repo.ts`).
75
+ *
76
+ * The read-repair/reconcile corroboration floor DOES fall back to it when
77
+ * `clusterPolicy.assumedClusterSize` is absent — the strict direction, so an unconfigured node
78
+ * cannot have its floor talked down by a shrunken cohort view. A deployment that genuinely runs
79
+ * fewer peers than this should declare `clusterPolicy.assumedClusterSize`.
80
+ */
81
+ clusterSize?: number;
82
+ clusterPolicy?: {
83
+ allowDownsize?: boolean;
84
+ /** Acceptable relative difference (e.g. 0.5 = +/-50%). */
85
+ sizeTolerance?: number;
86
+ /** Fraction of peers needed for super-majority (default {@link DEFAULT_SUPER_MAJORITY_THRESHOLD}). */
87
+ superMajorityThreshold?: number;
88
+ /**
89
+ * Opt in to transacting below the safe cluster-size floor when FRET has no confident
90
+ * network-size estimate — the membership-admission and coordinator small-cluster gates both
91
+ * fail closed without it. Default false. Turn on only for single-node / local dev meshes that
92
+ * knowingly run undersized.
93
+ */
94
+ allowUnvalidatedSmallCluster?: boolean;
95
+ /**
96
+ * The smallest cohort this deployment can genuinely field — normally the number of nodes you
97
+ * actually run, capped at `clusterSize`. Two consumers read it: the membership admission gate,
98
+ * on its fallback path when the node has no confident network-size estimate; and the
99
+ * read-repair/reconcile corroboration floor (`corroboratorCapacity`), unconditionally.
100
+ *
101
+ * Declaring it sets BOTH. Leaving it unset does NOT — see the module doc for why the two
102
+ * defaults point in opposite directions. A large deployment should still set this to its real
103
+ * cohort size, otherwise the admission gate cannot police a partition-induced downsize while
104
+ * its size estimate is unconfident; a genuine two-node mesh needs it (or an honest
105
+ * `clusterSize: 2`) to self-repair.
106
+ */
107
+ assumedClusterSize?: number;
108
+ };
109
+ }
110
+
111
+ /** Everything a node's consensus + restoration paths need, with every default already applied. */
112
+ export type ResolvedClusterPolicy = ClusterConsensusConfig & {
113
+ /** Replication factor / target cohort breadth. Always concrete after resolution. */
114
+ clusterSize: number;
115
+ /**
116
+ * Yardstick the repair corroboration floor measures a (possibly shrunken, always unauthenticated)
117
+ * cohort view against — see `corroboratorCapacity` in `cluster/quorum-restore.ts`.
118
+ *
119
+ * Deliberately distinct from {@link ClusterConsensusConfig.assumedClusterSize}, which the
120
+ * membership admission gate reads: the two share an operator field but not a default, because
121
+ * over- and under-stating them cost opposite things. See the module doc.
122
+ */
123
+ repairCorroborationClusterSize: number;
124
+ };
125
+
126
+ /**
127
+ * Apply every cluster-policy default a node needs. Same options in, same numbers out, so the
128
+ * composition root's behavior is unit-testable (`test/cluster-policy.spec.ts`). Its one side effect
129
+ * is the `assumed-cluster-size-unset` advisory below, which lives here because this is the only place
130
+ * that knows the resolution produced a self-defeating combination.
131
+ */
132
+ export function resolveClusterPolicy(options: ClusterPolicyOptions): ResolvedClusterPolicy {
133
+ // undefined here means "the operator said nothing", which is the only case where the two
134
+ // yardsticks below diverge.
135
+ //
136
+ // NOTE: a declared value is passed through unvalidated. The admission gate floors a degenerate one
137
+ // (0, negative, NaN, Infinity) itself — see `cluster-repo.admissionFloor` and its specs — but
138
+ // `corroboratorCapacity` does not: NaN there makes every quorum comparison false, so repair
139
+ // silently declines forever. Fail-safe, and unreachable through the reference-peer CLI (which
140
+ // rejects non-positive integers). If another composition root starts accepting unvalidated config,
141
+ // clamp here rather than in each consumer.
142
+ const declaredCohortSize = options.clusterPolicy?.assumedClusterSize;
143
+ const clusterSize = options.clusterSize ?? DEFAULT_CLUSTER_SIZE;
144
+ const repairCorroborationClusterSize = declaredCohortSize ?? clusterSize;
145
+
146
+ // Called once per node (resolveClusterPolicy runs once at construction), so this fires once per
147
+ // node startup, not per repair — a per-attempt warn on a busy node would be noise that gets
148
+ // filtered, defeating the point. Fires purely off configuration (not an observed cohort), so a
149
+ // deployment that genuinely runs `clusterSize` machines sees it too; worded as a conditional
150
+ // ("if you run fewer than N machines") rather than a fault for exactly that reason.
151
+ //
152
+ // The machine count in the message is CORROBORATION_FLOOR + 1, NOT repairCorroborationClusterSize:
153
+ // `corroboratorCapacity` caps only the FLOOR of two (`quorum-restore.ts`), so a cohort with two
154
+ // peers besides the reader meets it whatever the declared size. What an undeclared size costs is
155
+ // the relaxation below two, which is only reachable when repairCorroborationClusterSize <= 2.
156
+ // NOTE: only the UNDECLARED case warns. An operator who declares an assumedClusterSize larger than
157
+ // the cohort they actually run is equally unable to repair and gets no warning — deliberate, since
158
+ // a declaration is an explicit assertion and this function has no observed cohort to contradict it
159
+ // with. If `feat-admission-floor-from-observed-cohort-high-water-mark` ever lands (deriving the
160
+ // yardstick from observation), that warning becomes cheap and worth adding here.
161
+ if (declaredCohortSize === undefined && clusterSize > minAbsoluteClusterSize) {
162
+ const minimumSelfHealingDeployment = CORROBORATION_FLOOR + 1;
163
+ log('assumed-cluster-size-unset', {
164
+ clusterSize,
165
+ repairCorroborationClusterSize,
166
+ corroborationFloor: CORROBORATION_FLOOR,
167
+ minimumSelfHealingDeployment,
168
+ message:
169
+ `No clusterPolicy.assumedClusterSize declared: block repair (read-repair and reconcile) requires ` +
170
+ `${CORROBORATION_FLOOR} distinct corroborating peers other than the reader, and that floor is ` +
171
+ `relaxed only for a cohort that DECLARES it is smaller — with ` +
172
+ `repairCorroborationClusterSize=${repairCorroborationClusterSize} it never relaxes. So a deployment ` +
173
+ `that actually runs fewer than ${minimumSelfHealingDeployment} machines can never supply the floor ` +
174
+ `and every repair declines, permanently. If you run fewer than ${minimumSelfHealingDeployment} ` +
175
+ `machines, set clusterPolicy.assumedClusterSize to your real cohort size; it does not lower ` +
176
+ `clusterSize=${clusterSize} (the replication factor). Larger deployments can ignore this.`
177
+ });
178
+ }
179
+
180
+ return {
181
+ superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
182
+ simpleMajorityThreshold: 0.51,
183
+ minAbsoluteClusterSize,
184
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
185
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
186
+ // Fail closed by default (an undersized cluster with no confident network-size estimate is
187
+ // rejected); embedders running knowingly-small meshes opt in through clusterPolicy.
188
+ allowUnvalidatedSmallCluster: options.clusterPolicy?.allowUnvalidatedSmallCluster ?? false,
189
+ partitionDetectionWindow: 60000,
190
+ // Replication factor / target cohort breadth — what the coordinator aims for when selecting a
191
+ // cohort. Deliberately NOT the membership admission gate's yardstick: it says nothing about how
192
+ // many peers actually exist, so an unconfigured small mesh would refuse every write.
193
+ clusterSize,
194
+ // Membership admission gate, fallback path only (no confident network-size estimate). Defaults
195
+ // permissive so a two- or three-node mesh transacts unconfigured; the cost of that default is
196
+ // bounded to the gate, since the repair floor no longer reads this field.
197
+ assumedClusterSize: declaredCohortSize ?? minAbsoluteClusterSize,
198
+ // Repair corroboration floor, every repair. Defaults strict — to the replication factor — so an
199
+ // unconfigured node cannot have its floor talked down to a single voter by a shrunken cohort
200
+ // view. A genuinely small mesh declares its size (either field) to regain self-repair.
201
+ repairCorroborationClusterSize
202
+ };
203
+ }