@optimystic/db-p2p 0.21.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 (219) 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-policy.d.ts +13 -2
  6. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-policy.js +51 -4
  8. package/dist/src/cluster/cluster-policy.js.map +1 -1
  9. package/dist/src/cluster/cluster-repo.d.ts +42 -17
  10. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  11. package/dist/src/cluster/cluster-repo.js +229 -122
  12. package/dist/src/cluster/cluster-repo.js.map +1 -1
  13. package/dist/src/cluster/cluster-size-coupling.d.ts +28 -0
  14. package/dist/src/cluster/cluster-size-coupling.d.ts.map +1 -0
  15. package/dist/src/cluster/cluster-size-coupling.js +35 -0
  16. package/dist/src/cluster/cluster-size-coupling.js.map +1 -0
  17. package/dist/src/cluster/quorum-restore.d.ts +6 -0
  18. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  19. package/dist/src/cluster/quorum-restore.js +1 -1
  20. package/dist/src/cluster/quorum-restore.js.map +1 -1
  21. package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
  22. package/dist/src/cluster/reconcile-block.js +15 -3
  23. package/dist/src/cluster/reconcile-block.js.map +1 -1
  24. package/dist/src/cluster/service.d.ts +32 -1
  25. package/dist/src/cluster/service.d.ts.map +1 -1
  26. package/dist/src/cluster/service.js +43 -2
  27. package/dist/src/cluster/service.js.map +1 -1
  28. package/dist/src/cohort-topic/stream-util.d.ts +22 -6
  29. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  30. package/dist/src/cohort-topic/stream-util.js +56 -10
  31. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  32. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  33. package/dist/src/dispute/dispute-service.js +9 -3
  34. package/dist/src/dispute/dispute-service.js.map +1 -1
  35. package/dist/src/index.d.ts +5 -0
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -0
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/libp2p-key-network.d.ts +134 -7
  40. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  41. package/dist/src/libp2p-key-network.js +174 -37
  42. package/dist/src/libp2p-key-network.js.map +1 -1
  43. package/dist/src/libp2p-node-base.d.ts +3 -2
  44. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  45. package/dist/src/libp2p-node-base.js +859 -778
  46. package/dist/src/libp2p-node-base.js.map +1 -1
  47. package/dist/src/libp2p-node-rn.d.ts +2 -2
  48. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  49. package/dist/src/libp2p-node-rn.js.map +1 -1
  50. package/dist/src/libp2p-node.d.ts +2 -2
  51. package/dist/src/libp2p-node.d.ts.map +1 -1
  52. package/dist/src/libp2p-node.js.map +1 -1
  53. package/dist/src/logger.d.ts +17 -1
  54. package/dist/src/logger.d.ts.map +1 -1
  55. package/dist/src/logger.js +19 -2
  56. package/dist/src/logger.js.map +1 -1
  57. package/dist/src/network/network-manager-service.d.ts +2 -0
  58. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  59. package/dist/src/network/network-manager-service.js +4 -0
  60. package/dist/src/network/network-manager-service.js.map +1 -1
  61. package/dist/src/optimystic-node.d.ts +35 -0
  62. package/dist/src/optimystic-node.d.ts.map +1 -0
  63. package/dist/src/optimystic-node.js +2 -0
  64. package/dist/src/optimystic-node.js.map +1 -0
  65. package/dist/src/owned-block-seed.d.ts +6 -3
  66. package/dist/src/owned-block-seed.d.ts.map +1 -1
  67. package/dist/src/owned-block-seed.js +16 -3
  68. package/dist/src/owned-block-seed.js.map +1 -1
  69. package/dist/src/peer-address-book.d.ts +72 -0
  70. package/dist/src/peer-address-book.d.ts.map +1 -0
  71. package/dist/src/peer-address-book.js +123 -0
  72. package/dist/src/peer-address-book.js.map +1 -0
  73. package/dist/src/repo/client.d.ts.map +1 -1
  74. package/dist/src/repo/client.js +11 -2
  75. package/dist/src/repo/client.js.map +1 -1
  76. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  77. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  78. package/dist/src/repo/cluster-coordinator.js +95 -3
  79. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  80. package/dist/src/repo/coordinator-repo.d.ts +78 -14
  81. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  82. package/dist/src/repo/coordinator-repo.js +266 -81
  83. package/dist/src/repo/coordinator-repo.js.map +1 -1
  84. package/dist/src/rn.d.ts +5 -0
  85. package/dist/src/rn.d.ts.map +1 -1
  86. package/dist/src/rn.js +5 -0
  87. package/dist/src/rn.js.map +1 -1
  88. package/dist/src/storage/block-storage.d.ts.map +1 -1
  89. package/dist/src/storage/block-storage.js +57 -5
  90. package/dist/src/storage/block-storage.js.map +1 -1
  91. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  92. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  93. package/dist/src/storage/cached-raw-storage.js +152 -0
  94. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  95. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  96. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  97. package/dist/src/storage/cached-store-driver.js +775 -0
  98. package/dist/src/storage/cached-store-driver.js.map +1 -0
  99. package/dist/src/storage/i-block-storage.d.ts +20 -1
  100. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  101. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  102. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  103. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  104. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  105. package/dist/src/storage/shared-cache-pool.js +354 -0
  106. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  107. package/dist/src/storage/storage-repo.d.ts +56 -3
  108. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  109. package/dist/src/storage/storage-repo.js +124 -18
  110. package/dist/src/storage/storage-repo.js.map +1 -1
  111. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  112. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  113. package/dist/src/testing/raw-storage-conformance.js +52 -2
  114. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  115. package/package.json +3 -3
  116. package/readme.md +668 -653
  117. package/src/cluster/block-transfer.ts +424 -424
  118. package/src/cluster/client.ts +119 -88
  119. package/src/cluster/cluster-error.ts +64 -64
  120. package/src/cluster/cluster-policy.ts +203 -152
  121. package/src/cluster/cluster-repo.ts +245 -125
  122. package/src/cluster/cluster-size-coupling.ts +45 -0
  123. package/src/cluster/commit-cert.ts +139 -139
  124. package/src/cluster/i-transaction-state-store.ts +43 -43
  125. package/src/cluster/memory-transaction-state-store.ts +56 -56
  126. package/src/cluster/peer-key-binding.ts +37 -37
  127. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  128. package/src/cluster/quorum-restore.ts +223 -223
  129. package/src/cluster/reconcile-block.ts +203 -191
  130. package/src/cluster/service.ts +293 -241
  131. package/src/cluster/supermajority-coupling.ts +37 -37
  132. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  133. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  134. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  135. package/src/cohort-topic/change-bridge.ts +109 -109
  136. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  137. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  138. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  139. package/src/cohort-topic/host.ts +2901 -2901
  140. package/src/cohort-topic/index.ts +13 -13
  141. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  142. package/src/cohort-topic/membership-source.ts +68 -68
  143. package/src/cohort-topic/peer-codec.ts +31 -31
  144. package/src/cohort-topic/peer-sig.ts +86 -86
  145. package/src/cohort-topic/protocols.ts +71 -71
  146. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  147. package/src/cohort-topic/size-estimator.ts +16 -16
  148. package/src/cohort-topic/stream-util.ts +135 -87
  149. package/src/cohort-topic/threshold-crypto.ts +239 -239
  150. package/src/cohort-topic/topic-router.ts +77 -77
  151. package/src/dispute/arbitrator-selection.ts +138 -138
  152. package/src/dispute/cascade.ts +524 -524
  153. package/src/dispute/dispute-service.ts +11 -5
  154. package/src/dispute/invalidation.ts +625 -625
  155. package/src/inbound-authorization.ts +190 -190
  156. package/src/index.ts +52 -47
  157. package/src/libp2p-key-network.ts +1120 -958
  158. package/src/libp2p-node-base.ts +1675 -1591
  159. package/src/libp2p-node-rn.ts +30 -30
  160. package/src/libp2p-node.ts +36 -36
  161. package/src/logger.ts +19 -2
  162. package/src/matchmaking/aggregate-counts.ts +104 -104
  163. package/src/matchmaking/index.ts +20 -20
  164. package/src/matchmaking/module.ts +363 -363
  165. package/src/matchmaking/protocols.ts +51 -51
  166. package/src/matchmaking/provider-manager.ts +95 -95
  167. package/src/matchmaking/query-handler.ts +88 -88
  168. package/src/matchmaking/query-transport.ts +492 -492
  169. package/src/matchmaking/seeker-manager.ts +64 -64
  170. package/src/matchmaking/seeker-walk-client.ts +293 -293
  171. package/src/matchmaking/traffic-validation.ts +195 -195
  172. package/src/network/network-manager-service.ts +5 -0
  173. package/src/optimystic-node.ts +36 -0
  174. package/src/owned-block-seed.ts +53 -40
  175. package/src/peer-address-book.ts +149 -0
  176. package/src/protocol-limits.ts +33 -33
  177. package/src/reactivity/forwarder-host.ts +438 -438
  178. package/src/reactivity/index.ts +19 -19
  179. package/src/reactivity/notify-transport.ts +144 -144
  180. package/src/reactivity/origination-manager.ts +192 -192
  181. package/src/reactivity/protocols.ts +61 -61
  182. package/src/reactivity/push-state-gossip.ts +291 -291
  183. package/src/reactivity/recover-transport.ts +408 -408
  184. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  185. package/src/reactivity/subscriber-registry.ts +96 -96
  186. package/src/reactivity/subscription-manager.ts +450 -450
  187. package/src/reactivity/topic-bytes.ts +37 -37
  188. package/src/repo/client.ts +12 -2
  189. package/src/repo/cluster-coordinator.ts +99 -3
  190. package/src/repo/coordinator-repo.ts +305 -82
  191. package/src/repo/types.ts +7 -7
  192. package/src/rn.ts +39 -34
  193. package/src/rpc-deadline.ts +45 -45
  194. package/src/storage/arachnode-partition.ts +74 -74
  195. package/src/storage/block-storage.ts +59 -6
  196. package/src/storage/cached-raw-storage.ts +180 -0
  197. package/src/storage/cached-store-driver.ts +859 -0
  198. package/src/storage/i-block-storage.ts +20 -1
  199. package/src/storage/i-kv-store.ts +8 -8
  200. package/src/storage/i-raw-storage.ts +12 -5
  201. package/src/storage/kv-raw-storage.ts +135 -135
  202. package/src/storage/memory-kv-store.ts +28 -28
  203. package/src/storage/memory-storage.ts +25 -25
  204. package/src/storage/memory-store-driver.ts +157 -157
  205. package/src/storage/raw-store-codec.ts +42 -42
  206. package/src/storage/raw-store-driver.ts +80 -80
  207. package/src/storage/ring-selector.ts +317 -317
  208. package/src/storage/ring-shift-coordinator.ts +271 -271
  209. package/src/storage/shared-cache-pool.ts +452 -0
  210. package/src/storage/storage-repo.ts +1014 -903
  211. package/src/testing/cohort-topic-mesh-harness.ts +663 -663
  212. package/src/testing/index.ts +8 -8
  213. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  214. package/src/testing/raw-storage-conformance.ts +453 -397
  215. package/src/testing/reactivity-mesh-harness.ts +922 -922
  216. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  217. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  218. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  219. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
@@ -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
+ }