@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,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
+ }