@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,77 +1,77 @@
1
- import type { ITopicRouter, PeerRef, RingCoord } from "@optimystic/db-core";
2
- import { bytesToB64url, b64urlToBytes, encodeCohortMessage } from "@optimystic/db-core";
3
- import type { Libp2p } from "libp2p";
4
- import type { FretService, RouteAndMaybeActV1, NearAnchorV1 } from "p2p-fret";
5
- import { bytesToPeerId } from "./peer-codec.js";
6
- import { requestResponse, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
7
- import { PROTOCOL_COHORT_REGISTER } from "./protocols.js";
8
-
9
- /** A FRET `routeAct` result carrying a cohort reply. */
10
- function isCommit(res: NearAnchorV1 | { commitCertificate: string }): res is { commitCertificate: string } {
11
- return typeof res === "object" && res !== null && "commitCertificate" in res;
12
- }
13
-
14
- export interface FretTopicRouterOptions {
15
- /** The `/register` protocol id (defaults to the canonical one). */
16
- readonly registerProtocol?: string;
17
- /** RPC TTL hops for `RouteAndMaybeAct`. Default 16. */
18
- readonly ttl?: number;
19
- /** Per-frame ceiling. Default {@link DEFAULT_STREAM_MAX_BYTES}. */
20
- readonly maxBytes?: number;
21
- /** Monotonic clock (unix ms); injectable for tests. Default `Date.now`. */
22
- readonly clock?: () => number;
23
- }
24
-
25
- /**
26
- * FRET-backed {@link ITopicRouter}.
27
- *
28
- * `routeAndAct` maps onto FRET's `RouteAndMaybeAct` (`FretService.routeAct`): the `RegisterV1` frame
29
- * rides the `activity` field (base64url), routed to the cohort owning `key = coord_d(self, topicId)`,
30
- * collecting `want_k = k` participants and `min_sigs = k − x`. The cohort's activity callback (set by
31
- * the host) runs the willingness / cold-start / admission decision and returns the encoded
32
- * `RegisterReplyV1` as the `commitCertificate`; this adapter decodes it back to bytes. A bare
33
- * `NearAnchorV1` (no in-cluster activity ran) is surfaced to the walk as `no_state`.
34
- *
35
- * `dialMember` is the post-registration direct path: a libp2p dial of the `/register` protocol to the
36
- * cached primary, used by the renewal ping (`docs/cohort-topic.md` §FRET integration L457-460).
37
- */
38
- export class FretTopicRouter implements ITopicRouter {
39
- private readonly registerProtocol: string;
40
- private readonly ttl: number;
41
- private readonly maxBytes: number;
42
- private readonly clock: () => number;
43
-
44
- constructor(private readonly node: Libp2p, private readonly fret: FretService, options: FretTopicRouterOptions = {}) {
45
- this.registerProtocol = options.registerProtocol ?? PROTOCOL_COHORT_REGISTER;
46
- this.ttl = options.ttl ?? 16;
47
- this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
48
- this.clock = options.clock ?? ((): number => Date.now());
49
- }
50
-
51
- async routeAndAct(key: RingCoord, activity: Uint8Array, opts: { wantK: number; minSigs: number }): Promise<Uint8Array> {
52
- const now = this.clock();
53
- const msg: RouteAndMaybeActV1 = {
54
- v: 1,
55
- key: bytesToB64url(key),
56
- want_k: opts.wantK,
57
- min_sigs: opts.minSigs,
58
- ttl: this.ttl,
59
- activity: bytesToB64url(activity),
60
- correlation_id: bytesToB64url(key) + ":" + now,
61
- timestamp: now,
62
- signature: "",
63
- };
64
- const res = await this.fret.routeAct(msg);
65
- if (isCommit(res)) {
66
- return b64urlToBytes(res.commitCertificate);
67
- }
68
- // No in-cluster activity ran (we only reached an anchor hint): the walk treats this as NoState
69
- // and steps toward the root.
70
- return encodeCohortMessage({ v: 1, result: "no_state" }, this.maxBytes);
71
- }
72
-
73
- async dialMember(member: PeerRef, activity: Uint8Array): Promise<Uint8Array> {
74
- const peer = bytesToPeerId(member.id);
75
- return requestResponse(this.node, peer, this.registerProtocol, activity, this.maxBytes);
76
- }
77
- }
1
+ import type { ITopicRouter, PeerRef, RingCoord } from "@optimystic/db-core";
2
+ import { bytesToB64url, b64urlToBytes, encodeCohortMessage } from "@optimystic/db-core";
3
+ import type { Libp2p } from "libp2p";
4
+ import type { FretService, RouteAndMaybeActV1, NearAnchorV1 } from "p2p-fret";
5
+ import { bytesToPeerId } from "./peer-codec.js";
6
+ import { requestResponse, DEFAULT_STREAM_MAX_BYTES } from "./stream-util.js";
7
+ import { PROTOCOL_COHORT_REGISTER } from "./protocols.js";
8
+
9
+ /** A FRET `routeAct` result carrying a cohort reply. */
10
+ function isCommit(res: NearAnchorV1 | { commitCertificate: string }): res is { commitCertificate: string } {
11
+ return typeof res === "object" && res !== null && "commitCertificate" in res;
12
+ }
13
+
14
+ export interface FretTopicRouterOptions {
15
+ /** The `/register` protocol id (defaults to the canonical one). */
16
+ readonly registerProtocol?: string;
17
+ /** RPC TTL hops for `RouteAndMaybeAct`. Default 16. */
18
+ readonly ttl?: number;
19
+ /** Per-frame ceiling. Default {@link DEFAULT_STREAM_MAX_BYTES}. */
20
+ readonly maxBytes?: number;
21
+ /** Monotonic clock (unix ms); injectable for tests. Default `Date.now`. */
22
+ readonly clock?: () => number;
23
+ }
24
+
25
+ /**
26
+ * FRET-backed {@link ITopicRouter}.
27
+ *
28
+ * `routeAndAct` maps onto FRET's `RouteAndMaybeAct` (`FretService.routeAct`): the `RegisterV1` frame
29
+ * rides the `activity` field (base64url), routed to the cohort owning `key = coord_d(self, topicId)`,
30
+ * collecting `want_k = k` participants and `min_sigs = k − x`. The cohort's activity callback (set by
31
+ * the host) runs the willingness / cold-start / admission decision and returns the encoded
32
+ * `RegisterReplyV1` as the `commitCertificate`; this adapter decodes it back to bytes. A bare
33
+ * `NearAnchorV1` (no in-cluster activity ran) is surfaced to the walk as `no_state`.
34
+ *
35
+ * `dialMember` is the post-registration direct path: a libp2p dial of the `/register` protocol to the
36
+ * cached primary, used by the renewal ping (`docs/cohort-topic.md` §FRET integration L457-460).
37
+ */
38
+ export class FretTopicRouter implements ITopicRouter {
39
+ private readonly registerProtocol: string;
40
+ private readonly ttl: number;
41
+ private readonly maxBytes: number;
42
+ private readonly clock: () => number;
43
+
44
+ constructor(private readonly node: Libp2p, private readonly fret: FretService, options: FretTopicRouterOptions = {}) {
45
+ this.registerProtocol = options.registerProtocol ?? PROTOCOL_COHORT_REGISTER;
46
+ this.ttl = options.ttl ?? 16;
47
+ this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
48
+ this.clock = options.clock ?? ((): number => Date.now());
49
+ }
50
+
51
+ async routeAndAct(key: RingCoord, activity: Uint8Array, opts: { wantK: number; minSigs: number }): Promise<Uint8Array> {
52
+ const now = this.clock();
53
+ const msg: RouteAndMaybeActV1 = {
54
+ v: 1,
55
+ key: bytesToB64url(key),
56
+ want_k: opts.wantK,
57
+ min_sigs: opts.minSigs,
58
+ ttl: this.ttl,
59
+ activity: bytesToB64url(activity),
60
+ correlation_id: bytesToB64url(key) + ":" + now,
61
+ timestamp: now,
62
+ signature: "",
63
+ };
64
+ const res = await this.fret.routeAct(msg);
65
+ if (isCommit(res)) {
66
+ return b64urlToBytes(res.commitCertificate);
67
+ }
68
+ // No in-cluster activity ran (we only reached an anchor hint): the walk treats this as NoState
69
+ // and steps toward the root.
70
+ return encodeCohortMessage({ v: 1, result: "no_state" }, this.maxBytes);
71
+ }
72
+
73
+ async dialMember(member: PeerRef, activity: Uint8Array): Promise<Uint8Array> {
74
+ const peer = bytesToPeerId(member.id);
75
+ return requestResponse(this.node, peer, this.registerProtocol, activity, this.maxBytes);
76
+ }
77
+ }
@@ -1,138 +1,138 @@
1
- /**
2
- * Verifiable dispersed arbitrator sampling.
3
- *
4
- * The old selection walked the ring positions *immediately adjacent* to the disputed block (sort every
5
- * peer by XOR distance to `hash(blockId)`, skip the original cluster, take the next K). That recruits
6
- * from exactly the neighborhood an attacker already had to own to capture the block's cluster — so
7
- * "independent" arbitration drew from the *least* independent population (see `docs/correctness.md` §7.1
8
- * Sybil, Theorems 8 & 10).
9
- *
10
- * Instead we derive `count` pseudo-random ring coordinates from `hash(blockId ‖ round ‖ epoch ‖ i)` and
11
- * pick the peer nearest each coordinate. SHA-256 output is uniform over the ring, so the coordinates land
12
- * spread across the whole keyspace and the sampled arbitrators are drawn from the whole population, not
13
- * the block's neighborhood. To capture them an attacker needs IDs near many independent random points —
14
- * a fraction of the *entire* network, not one locale.
15
- *
16
- * Two properties both hold:
17
- * - **Deterministic & independently verifiable** — every honest node, given the same
18
- * `(blockId, round, epoch)` and the same agreed membership, computes the identical set. This is what
19
- * lets the dispute verify path re-derive the eligible set instead of trusting a declared one.
20
- * - **Unpredictable / not pre-positionable** — the coordinates for round r are pinned only once
21
- * `(blockId, round, epoch)` are all fixed. `round` advances in real time during the dispute; `epoch`
22
- * is the agreed membership epoch, which rotates with membership and cannot be freely advanced by the
23
- * attacker. So the attacker cannot know far enough ahead which coordinates to migrate IDs toward.
24
- */
25
-
26
- /**
27
- * Resolve the peer-id strings nearest a ring coordinate, in ascending distance order.
28
- * Production: FRET `assembleCohort(coord, wants)`. Tests: sort a fixed `KnownPeer[]` by XOR distance.
29
- * May return fewer than `wants` — that signals the whole eligible membership fit in the slice.
30
- */
31
- export type NearestResolver = (coord: Uint8Array, wants: number) => string[] | Promise<string[]>;
32
-
33
- /** FRET-compatible ring hash of arbitrary bytes → coordinate (SHA-256; see db-core `RingHash.H` / FRET `hashKey`). */
34
- export type RingHashFn = (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
35
-
36
- export interface ArbitratorSamplingParams {
37
- /** Disputed block id bytes (messageHash fallback), as bound into the dispute. */
38
- readonly blockId: Uint8Array;
39
- /** Escalation round, 0-based. Round 0 is the first arbitration. */
40
- readonly round: number;
41
- /**
42
- * Agreed membership epoch bytes. Pins the draw to an epoch the attacker cannot freely advance.
43
- * Interim source (until `design-cluster-membership-agreement` lands): hash of the agreed responsible
44
- * set the admission gate already converges on (`cluster-membership-admission-gate`).
45
- */
46
- readonly epoch: Uint8Array;
47
- /** Number of fresh, distinct arbitrators to draw this round. */
48
- readonly count: number;
49
- /** Peer-id strings to exclude: original cluster + self + arbitrators already drawn in prior rounds. */
50
- readonly exclude: ReadonlySet<string>;
51
- }
52
-
53
- /** Little-endian u32 encoding of `n` — the canonical wire encoding for `round` and the coordinate index. */
54
- function u32le(n: number): Uint8Array {
55
- const out = new Uint8Array(4);
56
- new DataView(out.buffer).setUint32(0, n >>> 0, true);
57
- return out;
58
- }
59
-
60
- /** Concatenate byte spans left-to-right. */
61
- function concatBytes(parts: Uint8Array[]): Uint8Array {
62
- let total = 0;
63
- for (const p of parts) total += p.length;
64
- const out = new Uint8Array(total);
65
- let off = 0;
66
- for (const p of parts) { out.set(p, off); off += p.length; }
67
- return out;
68
- }
69
-
70
- /**
71
- * The exact preimage the i-th coordinate of a round hashes: `blockId ‖ u32le(round) ‖ epoch ‖ u32le(i)`.
72
- * Folding `round`, `epoch`, and `i` in gives: (a) each round samples a distinct population (round changes
73
- * every coordinate); (b) each of the `count` coordinates is an independent uniform draw (dispersion);
74
- * (c) a replacement for an offline/duplicate pick is the *next* peer in the same coordinate's ordering,
75
- * never a fresh challenger-chosen peer. The canonical little-endian encoding is asserted by a golden
76
- * vector so two implementations hash identical bytes.
77
- */
78
- export function coordinatePreimage(blockId: Uint8Array, round: number, epoch: Uint8Array, i: number): Uint8Array {
79
- return concatBytes([blockId, u32le(round), epoch, u32le(i)]);
80
- }
81
-
82
- /**
83
- * Deterministic dispersed arbitrator draw. Returns up to `count` distinct peer-id strings; fewer only
84
- * when the network is too small to yield that many (small-network fallback) — never duplicates, never
85
- * loops. In the degenerate all-peers-in-cluster case, returns `[]`.
86
- *
87
- * For each coordinate `i` the nearest unseen peer is chosen. When a coordinate's nearest slice is
88
- * entirely `seen` (excluded, or already picked for an earlier coordinate) the slice is widened
89
- * (`wants` grows) and retried; if widening exposes the whole eligible membership with nobody fresh, the
90
- * entire membership is exhausted and the (short) picks are returned. Replacement of an offline/duplicate
91
- * pick is thus the deterministic next peer in the same coordinate's ordering, identical on every honest
92
- * node, so disputing parties cannot steer it.
93
- */
94
- export async function sampleArbitrators(
95
- params: ArbitratorSamplingParams,
96
- nearest: NearestResolver,
97
- hash: RingHashFn,
98
- ): Promise<string[]> {
99
- const { blockId, round, epoch, count, exclude } = params;
100
- const picks: string[] = [];
101
- if (count <= 0) return picks;
102
-
103
- const seen = new Set<string>(exclude);
104
-
105
- for (let i = 0; picks.length < count; i++) {
106
- const coord = await hash(coordinatePreimage(blockId, round, epoch, i));
107
-
108
- // Walk this coordinate's ascending-distance ordering for the first peer we have not yet seen,
109
- // widening the slice until we find one or have proven the whole eligible membership is exhausted.
110
- // NOTE: `wants` starts at `seen.size + 1` (conservative — guarantees exhaustion is provable in one
111
- // widen). Starting at 1 and widening only on a seen-collision is also correct and asks the resolver
112
- // for far fewer peers per coordinate; if `assembleCohort` ever shows up as hot here, start smaller.
113
- let wants = seen.size + 1;
114
- let prevLen = -1;
115
- let picked: string | undefined;
116
- let membershipExhausted = false;
117
- for (;;) {
118
- const cands = await nearest(coord, wants);
119
- const fresh = cands.find(c => !seen.has(c));
120
- if (fresh !== undefined) { picked = fresh; break; }
121
- // No fresh peer in this slice. If the resolver returned fewer than we asked (or the slice
122
- // stopped growing), we have seen the whole eligible membership from this coordinate — and
123
- // since every one of them is already `seen`, no future coordinate can yield anything new.
124
- if (cands.length < wants || cands.length <= prevLen) { membershipExhausted = true; break; }
125
- prevLen = cands.length;
126
- wants *= 2;
127
- }
128
-
129
- if (picked !== undefined) {
130
- picks.push(picked);
131
- seen.add(picked);
132
- } else if (membershipExhausted) {
133
- break;
134
- }
135
- }
136
-
137
- return picks;
138
- }
1
+ /**
2
+ * Verifiable dispersed arbitrator sampling.
3
+ *
4
+ * The old selection walked the ring positions *immediately adjacent* to the disputed block (sort every
5
+ * peer by XOR distance to `hash(blockId)`, skip the original cluster, take the next K). That recruits
6
+ * from exactly the neighborhood an attacker already had to own to capture the block's cluster — so
7
+ * "independent" arbitration drew from the *least* independent population (see `docs/correctness.md` §7.1
8
+ * Sybil, Theorems 8 & 10).
9
+ *
10
+ * Instead we derive `count` pseudo-random ring coordinates from `hash(blockId ‖ round ‖ epoch ‖ i)` and
11
+ * pick the peer nearest each coordinate. SHA-256 output is uniform over the ring, so the coordinates land
12
+ * spread across the whole keyspace and the sampled arbitrators are drawn from the whole population, not
13
+ * the block's neighborhood. To capture them an attacker needs IDs near many independent random points —
14
+ * a fraction of the *entire* network, not one locale.
15
+ *
16
+ * Two properties both hold:
17
+ * - **Deterministic & independently verifiable** — every honest node, given the same
18
+ * `(blockId, round, epoch)` and the same agreed membership, computes the identical set. This is what
19
+ * lets the dispute verify path re-derive the eligible set instead of trusting a declared one.
20
+ * - **Unpredictable / not pre-positionable** — the coordinates for round r are pinned only once
21
+ * `(blockId, round, epoch)` are all fixed. `round` advances in real time during the dispute; `epoch`
22
+ * is the agreed membership epoch, which rotates with membership and cannot be freely advanced by the
23
+ * attacker. So the attacker cannot know far enough ahead which coordinates to migrate IDs toward.
24
+ */
25
+
26
+ /**
27
+ * Resolve the peer-id strings nearest a ring coordinate, in ascending distance order.
28
+ * Production: FRET `assembleCohort(coord, wants)`. Tests: sort a fixed `KnownPeer[]` by XOR distance.
29
+ * May return fewer than `wants` — that signals the whole eligible membership fit in the slice.
30
+ */
31
+ export type NearestResolver = (coord: Uint8Array, wants: number) => string[] | Promise<string[]>;
32
+
33
+ /** FRET-compatible ring hash of arbitrary bytes → coordinate (SHA-256; see db-core `RingHash.H` / FRET `hashKey`). */
34
+ export type RingHashFn = (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
35
+
36
+ export interface ArbitratorSamplingParams {
37
+ /** Disputed block id bytes (messageHash fallback), as bound into the dispute. */
38
+ readonly blockId: Uint8Array;
39
+ /** Escalation round, 0-based. Round 0 is the first arbitration. */
40
+ readonly round: number;
41
+ /**
42
+ * Agreed membership epoch bytes. Pins the draw to an epoch the attacker cannot freely advance.
43
+ * Interim source (until `design-cluster-membership-agreement` lands): hash of the agreed responsible
44
+ * set the admission gate already converges on (`cluster-membership-admission-gate`).
45
+ */
46
+ readonly epoch: Uint8Array;
47
+ /** Number of fresh, distinct arbitrators to draw this round. */
48
+ readonly count: number;
49
+ /** Peer-id strings to exclude: original cluster + self + arbitrators already drawn in prior rounds. */
50
+ readonly exclude: ReadonlySet<string>;
51
+ }
52
+
53
+ /** Little-endian u32 encoding of `n` — the canonical wire encoding for `round` and the coordinate index. */
54
+ function u32le(n: number): Uint8Array {
55
+ const out = new Uint8Array(4);
56
+ new DataView(out.buffer).setUint32(0, n >>> 0, true);
57
+ return out;
58
+ }
59
+
60
+ /** Concatenate byte spans left-to-right. */
61
+ function concatBytes(parts: Uint8Array[]): Uint8Array {
62
+ let total = 0;
63
+ for (const p of parts) total += p.length;
64
+ const out = new Uint8Array(total);
65
+ let off = 0;
66
+ for (const p of parts) { out.set(p, off); off += p.length; }
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * The exact preimage the i-th coordinate of a round hashes: `blockId ‖ u32le(round) ‖ epoch ‖ u32le(i)`.
72
+ * Folding `round`, `epoch`, and `i` in gives: (a) each round samples a distinct population (round changes
73
+ * every coordinate); (b) each of the `count` coordinates is an independent uniform draw (dispersion);
74
+ * (c) a replacement for an offline/duplicate pick is the *next* peer in the same coordinate's ordering,
75
+ * never a fresh challenger-chosen peer. The canonical little-endian encoding is asserted by a golden
76
+ * vector so two implementations hash identical bytes.
77
+ */
78
+ export function coordinatePreimage(blockId: Uint8Array, round: number, epoch: Uint8Array, i: number): Uint8Array {
79
+ return concatBytes([blockId, u32le(round), epoch, u32le(i)]);
80
+ }
81
+
82
+ /**
83
+ * Deterministic dispersed arbitrator draw. Returns up to `count` distinct peer-id strings; fewer only
84
+ * when the network is too small to yield that many (small-network fallback) — never duplicates, never
85
+ * loops. In the degenerate all-peers-in-cluster case, returns `[]`.
86
+ *
87
+ * For each coordinate `i` the nearest unseen peer is chosen. When a coordinate's nearest slice is
88
+ * entirely `seen` (excluded, or already picked for an earlier coordinate) the slice is widened
89
+ * (`wants` grows) and retried; if widening exposes the whole eligible membership with nobody fresh, the
90
+ * entire membership is exhausted and the (short) picks are returned. Replacement of an offline/duplicate
91
+ * pick is thus the deterministic next peer in the same coordinate's ordering, identical on every honest
92
+ * node, so disputing parties cannot steer it.
93
+ */
94
+ export async function sampleArbitrators(
95
+ params: ArbitratorSamplingParams,
96
+ nearest: NearestResolver,
97
+ hash: RingHashFn,
98
+ ): Promise<string[]> {
99
+ const { blockId, round, epoch, count, exclude } = params;
100
+ const picks: string[] = [];
101
+ if (count <= 0) return picks;
102
+
103
+ const seen = new Set<string>(exclude);
104
+
105
+ for (let i = 0; picks.length < count; i++) {
106
+ const coord = await hash(coordinatePreimage(blockId, round, epoch, i));
107
+
108
+ // Walk this coordinate's ascending-distance ordering for the first peer we have not yet seen,
109
+ // widening the slice until we find one or have proven the whole eligible membership is exhausted.
110
+ // NOTE: `wants` starts at `seen.size + 1` (conservative — guarantees exhaustion is provable in one
111
+ // widen). Starting at 1 and widening only on a seen-collision is also correct and asks the resolver
112
+ // for far fewer peers per coordinate; if `assembleCohort` ever shows up as hot here, start smaller.
113
+ let wants = seen.size + 1;
114
+ let prevLen = -1;
115
+ let picked: string | undefined;
116
+ let membershipExhausted = false;
117
+ for (;;) {
118
+ const cands = await nearest(coord, wants);
119
+ const fresh = cands.find(c => !seen.has(c));
120
+ if (fresh !== undefined) { picked = fresh; break; }
121
+ // No fresh peer in this slice. If the resolver returned fewer than we asked (or the slice
122
+ // stopped growing), we have seen the whole eligible membership from this coordinate — and
123
+ // since every one of them is already `seen`, no future coordinate can yield anything new.
124
+ if (cands.length < wants || cands.length <= prevLen) { membershipExhausted = true; break; }
125
+ prevLen = cands.length;
126
+ wants *= 2;
127
+ }
128
+
129
+ if (picked !== undefined) {
130
+ picks.push(picked);
131
+ seen.add(picked);
132
+ } else if (membershipExhausted) {
133
+ break;
134
+ }
135
+ }
136
+
137
+ return picks;
138
+ }