@optimystic/db-p2p 0.14.1 → 0.16.2

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 (275) hide show
  1. package/README.md +616 -609
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +4 -3
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/block-transfer.d.ts +52 -3
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  7. package/dist/src/cluster/block-transfer.js +129 -6
  8. package/dist/src/cluster/block-transfer.js.map +1 -1
  9. package/dist/src/cluster/client.d.ts.map +1 -1
  10. package/dist/src/cluster/client.js +3 -1
  11. package/dist/src/cluster/client.js.map +1 -1
  12. package/dist/src/cluster/cluster-repo.d.ts +182 -11
  13. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  14. package/dist/src/cluster/cluster-repo.js +429 -69
  15. package/dist/src/cluster/cluster-repo.js.map +1 -1
  16. package/dist/src/cluster/peer-key-binding.d.ts +25 -0
  17. package/dist/src/cluster/peer-key-binding.d.ts.map +1 -0
  18. package/dist/src/cluster/peer-key-binding.js +39 -0
  19. package/dist/src/cluster/peer-key-binding.js.map +1 -0
  20. package/dist/src/cluster/quorum-restore.d.ts +72 -0
  21. package/dist/src/cluster/quorum-restore.d.ts.map +1 -0
  22. package/dist/src/cluster/quorum-restore.js +0 -0
  23. package/dist/src/cluster/quorum-restore.js.map +1 -0
  24. package/dist/src/cluster/rebalance-monitor.d.ts +23 -1
  25. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  26. package/dist/src/cluster/rebalance-monitor.js +17 -2
  27. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  28. package/dist/src/cluster/service.d.ts.map +1 -1
  29. package/dist/src/cluster/service.js +7 -1
  30. package/dist/src/cluster/service.js.map +1 -1
  31. package/dist/src/cluster/spread-on-churn.d.ts +7 -0
  32. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  33. package/dist/src/cluster/spread-on-churn.js +4 -1
  34. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  35. package/dist/src/cluster/supermajority-coupling.d.ts +22 -0
  36. package/dist/src/cluster/supermajority-coupling.d.ts.map +1 -0
  37. package/dist/src/cluster/supermajority-coupling.js +27 -0
  38. package/dist/src/cluster/supermajority-coupling.js.map +1 -0
  39. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +46 -4
  40. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -1
  41. package/dist/src/cohort-topic/cohort-gossip-driver.js +62 -7
  42. package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -1
  43. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +7 -0
  44. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -1
  45. package/dist/src/cohort-topic/cohort-gossip-transport.js +9 -0
  46. package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -1
  47. package/dist/src/cohort-topic/host.d.ts +197 -27
  48. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  49. package/dist/src/cohort-topic/host.js +643 -87
  50. package/dist/src/cohort-topic/host.js.map +1 -1
  51. package/dist/src/dispute/arbitrator-selection.d.ts +68 -7
  52. package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -1
  53. package/dist/src/dispute/arbitrator-selection.js +105 -16
  54. package/dist/src/dispute/arbitrator-selection.js.map +1 -1
  55. package/dist/src/dispute/cascade.d.ts +7 -0
  56. package/dist/src/dispute/cascade.d.ts.map +1 -1
  57. package/dist/src/dispute/cascade.js +1 -1
  58. package/dist/src/dispute/cascade.js.map +1 -1
  59. package/dist/src/dispute/client.d.ts.map +1 -1
  60. package/dist/src/dispute/client.js +3 -2
  61. package/dist/src/dispute/client.js.map +1 -1
  62. package/dist/src/dispute/dispute-service.d.ts +32 -3
  63. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  64. package/dist/src/dispute/dispute-service.js +82 -9
  65. package/dist/src/dispute/dispute-service.js.map +1 -1
  66. package/dist/src/dispute/index.d.ts +1 -1
  67. package/dist/src/dispute/index.d.ts.map +1 -1
  68. package/dist/src/dispute/index.js +1 -1
  69. package/dist/src/dispute/index.js.map +1 -1
  70. package/dist/src/dispute/invalidation.d.ts +25 -0
  71. package/dist/src/dispute/invalidation.d.ts.map +1 -1
  72. package/dist/src/dispute/invalidation.js +14 -2
  73. package/dist/src/dispute/invalidation.js.map +1 -1
  74. package/dist/src/dispute/service.d.ts.map +1 -1
  75. package/dist/src/dispute/service.js +7 -1
  76. package/dist/src/dispute/service.js.map +1 -1
  77. package/dist/src/index.d.ts +4 -1
  78. package/dist/src/index.d.ts.map +1 -1
  79. package/dist/src/index.js +4 -1
  80. package/dist/src/index.js.map +1 -1
  81. package/dist/src/it-utility.d.ts +1 -1
  82. package/dist/src/it-utility.d.ts.map +1 -1
  83. package/dist/src/it-utility.js +4 -13
  84. package/dist/src/it-utility.js.map +1 -1
  85. package/dist/src/libp2p-key-network.d.ts +98 -1
  86. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  87. package/dist/src/libp2p-key-network.js +272 -17
  88. package/dist/src/libp2p-key-network.js.map +1 -1
  89. package/dist/src/libp2p-node-base.d.ts +12 -0
  90. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  91. package/dist/src/libp2p-node-base.js +415 -87
  92. package/dist/src/libp2p-node-base.js.map +1 -1
  93. package/dist/src/matchmaking/query-transport.d.ts +2 -0
  94. package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
  95. package/dist/src/matchmaking/query-transport.js +6 -0
  96. package/dist/src/matchmaking/query-transport.js.map +1 -1
  97. package/dist/src/matchmaking/traffic-validation.js +1 -0
  98. package/dist/src/matchmaking/traffic-validation.js.map +1 -1
  99. package/dist/src/network/network-manager-service.d.ts +2 -2
  100. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  101. package/dist/src/network/network-manager-service.js +8 -4
  102. package/dist/src/network/network-manager-service.js.map +1 -1
  103. package/dist/src/owned-block-seed.d.ts +27 -0
  104. package/dist/src/owned-block-seed.d.ts.map +1 -0
  105. package/dist/src/owned-block-seed.js +37 -0
  106. package/dist/src/owned-block-seed.js.map +1 -0
  107. package/dist/src/protocol-client.d.ts +1 -0
  108. package/dist/src/protocol-client.d.ts.map +1 -1
  109. package/dist/src/protocol-client.js +9 -2
  110. package/dist/src/protocol-client.js.map +1 -1
  111. package/dist/src/protocol-limits.d.ts +32 -0
  112. package/dist/src/protocol-limits.d.ts.map +1 -0
  113. package/dist/src/protocol-limits.js +32 -0
  114. package/dist/src/protocol-limits.js.map +1 -0
  115. package/dist/src/repo/client.d.ts.map +1 -1
  116. package/dist/src/repo/client.js +3 -0
  117. package/dist/src/repo/client.js.map +1 -1
  118. package/dist/src/repo/cluster-coordinator.d.ts +41 -4
  119. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  120. package/dist/src/repo/cluster-coordinator.js +128 -80
  121. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  122. package/dist/src/repo/coordinator-repo.d.ts +38 -1
  123. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  124. package/dist/src/repo/coordinator-repo.js +92 -13
  125. package/dist/src/repo/coordinator-repo.js.map +1 -1
  126. package/dist/src/repo/service.d.ts.map +1 -1
  127. package/dist/src/repo/service.js +7 -1
  128. package/dist/src/repo/service.js.map +1 -1
  129. package/dist/src/reputation/peer-reputation.js +1 -1
  130. package/dist/src/reputation/peer-reputation.js.map +1 -1
  131. package/dist/src/reputation/types.d.ts +3 -1
  132. package/dist/src/reputation/types.d.ts.map +1 -1
  133. package/dist/src/reputation/types.js +3 -0
  134. package/dist/src/reputation/types.js.map +1 -1
  135. package/dist/src/rn.d.ts +5 -1
  136. package/dist/src/rn.d.ts.map +1 -1
  137. package/dist/src/rn.js +7 -1
  138. package/dist/src/rn.js.map +1 -1
  139. package/dist/src/storage/arachnode-fret-adapter.d.ts +30 -1
  140. package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
  141. package/dist/src/storage/arachnode-fret-adapter.js +15 -2
  142. package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
  143. package/dist/src/storage/arachnode-partition.d.ts +49 -0
  144. package/dist/src/storage/arachnode-partition.d.ts.map +1 -0
  145. package/dist/src/storage/arachnode-partition.js +61 -0
  146. package/dist/src/storage/arachnode-partition.js.map +1 -0
  147. package/dist/src/storage/block-storage.d.ts +40 -1
  148. package/dist/src/storage/block-storage.d.ts.map +1 -1
  149. package/dist/src/storage/block-storage.js +150 -57
  150. package/dist/src/storage/block-storage.js.map +1 -1
  151. package/dist/src/storage/i-block-storage.d.ts +8 -0
  152. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  153. package/dist/src/storage/i-raw-storage.d.ts +13 -0
  154. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  155. package/dist/src/storage/kv-raw-storage.d.ts +45 -0
  156. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -0
  157. package/dist/src/storage/kv-raw-storage.js +113 -0
  158. package/dist/src/storage/kv-raw-storage.js.map +1 -0
  159. package/dist/src/storage/memory-storage.d.ts +20 -58
  160. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  161. package/dist/src/storage/memory-storage.js +22 -148
  162. package/dist/src/storage/memory-storage.js.map +1 -1
  163. package/dist/src/storage/memory-store-driver.d.ts +39 -0
  164. package/dist/src/storage/memory-store-driver.d.ts.map +1 -0
  165. package/dist/src/storage/memory-store-driver.js +129 -0
  166. package/dist/src/storage/memory-store-driver.js.map +1 -0
  167. package/dist/src/storage/raw-store-codec.d.ts +10 -0
  168. package/dist/src/storage/raw-store-codec.d.ts.map +1 -0
  169. package/dist/src/storage/raw-store-codec.js +36 -0
  170. package/dist/src/storage/raw-store-codec.js.map +1 -0
  171. package/dist/src/storage/raw-store-driver.d.ts +69 -0
  172. package/dist/src/storage/raw-store-driver.d.ts.map +1 -0
  173. package/dist/src/storage/raw-store-driver.js +2 -0
  174. package/dist/src/storage/raw-store-driver.js.map +1 -0
  175. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -1
  176. package/dist/src/storage/restoration-coordinator-v2.js +1 -1
  177. package/dist/src/storage/restoration-coordinator-v2.js.map +1 -1
  178. package/dist/src/storage/restoration-coordinator.d.ts +73 -0
  179. package/dist/src/storage/restoration-coordinator.d.ts.map +1 -0
  180. package/dist/src/storage/restoration-coordinator.js +181 -0
  181. package/dist/src/storage/restoration-coordinator.js.map +1 -0
  182. package/dist/src/storage/ring-selector.d.ts +92 -5
  183. package/dist/src/storage/ring-selector.d.ts.map +1 -1
  184. package/dist/src/storage/ring-selector.js +153 -51
  185. package/dist/src/storage/ring-selector.js.map +1 -1
  186. package/dist/src/storage/ring-shift-coordinator.d.ts +128 -0
  187. package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -0
  188. package/dist/src/storage/ring-shift-coordinator.js +196 -0
  189. package/dist/src/storage/ring-shift-coordinator.js.map +1 -0
  190. package/dist/src/storage/storage-monitor.d.ts +28 -0
  191. package/dist/src/storage/storage-monitor.d.ts.map +1 -1
  192. package/dist/src/storage/storage-monitor.js +42 -2
  193. package/dist/src/storage/storage-monitor.js.map +1 -1
  194. package/dist/src/storage/storage-repo.d.ts +19 -0
  195. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  196. package/dist/src/storage/storage-repo.js +172 -22
  197. package/dist/src/storage/storage-repo.js.map +1 -1
  198. package/dist/src/sync/client.d.ts.map +1 -1
  199. package/dist/src/sync/client.js +5 -1
  200. package/dist/src/sync/client.js.map +1 -1
  201. package/dist/src/sync/service.d.ts.map +1 -1
  202. package/dist/src/sync/service.js +2 -1
  203. package/dist/src/sync/service.js.map +1 -1
  204. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +28 -4
  205. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  206. package/dist/src/testing/cohort-topic-mesh-harness.js +51 -10
  207. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  208. package/dist/src/testing/index.d.ts +1 -0
  209. package/dist/src/testing/index.d.ts.map +1 -1
  210. package/dist/src/testing/index.js +1 -0
  211. package/dist/src/testing/index.js.map +1 -1
  212. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  213. package/dist/src/testing/mesh-harness.js +15 -7
  214. package/dist/src/testing/mesh-harness.js.map +1 -1
  215. package/dist/src/testing/raw-storage-conformance.d.ts +24 -0
  216. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -0
  217. package/dist/src/testing/raw-storage-conformance.js +313 -0
  218. package/dist/src/testing/raw-storage-conformance.js.map +1 -0
  219. package/package.json +4 -4
  220. package/src/cluster/block-transfer-service.ts +4 -3
  221. package/src/cluster/block-transfer.ts +424 -265
  222. package/src/cluster/client.ts +3 -1
  223. package/src/cluster/cluster-repo.ts +481 -71
  224. package/src/cluster/peer-key-binding.ts +37 -0
  225. package/src/cluster/quorum-restore.ts +0 -0
  226. package/src/cluster/rebalance-monitor.ts +34 -3
  227. package/src/cluster/service.ts +7 -1
  228. package/src/cluster/spread-on-churn.ts +11 -1
  229. package/src/cluster/supermajority-coupling.ts +37 -0
  230. package/src/cohort-topic/cohort-gossip-driver.ts +96 -9
  231. package/src/cohort-topic/cohort-gossip-transport.ts +10 -0
  232. package/src/cohort-topic/host.ts +831 -105
  233. package/src/dispute/arbitrator-selection.ts +138 -28
  234. package/src/dispute/cascade.ts +8 -1
  235. package/src/dispute/client.ts +3 -2
  236. package/src/dispute/dispute-service.ts +94 -9
  237. package/src/dispute/index.ts +7 -1
  238. package/src/dispute/invalidation.ts +40 -2
  239. package/src/dispute/service.ts +7 -1
  240. package/src/index.ts +46 -43
  241. package/src/it-utility.ts +5 -13
  242. package/src/libp2p-key-network.ts +281 -18
  243. package/src/libp2p-node-base.ts +485 -79
  244. package/src/matchmaking/query-transport.ts +9 -0
  245. package/src/network/network-manager-service.ts +8 -2
  246. package/src/owned-block-seed.ts +40 -0
  247. package/src/protocol-client.ts +10 -3
  248. package/src/protocol-limits.ts +33 -0
  249. package/src/repo/client.ts +3 -0
  250. package/src/repo/cluster-coordinator.ts +145 -82
  251. package/src/repo/coordinator-repo.ts +96 -16
  252. package/src/repo/service.ts +7 -1
  253. package/src/reputation/types.ts +3 -0
  254. package/src/rn.ts +7 -1
  255. package/src/storage/arachnode-fret-adapter.ts +33 -2
  256. package/src/storage/arachnode-partition.ts +74 -0
  257. package/src/storage/block-storage.ts +169 -61
  258. package/src/storage/i-block-storage.ts +9 -0
  259. package/src/storage/i-raw-storage.ts +14 -0
  260. package/src/storage/kv-raw-storage.ts +135 -0
  261. package/src/storage/memory-storage.ts +25 -173
  262. package/src/storage/memory-store-driver.ts +157 -0
  263. package/src/storage/raw-store-codec.ts +42 -0
  264. package/src/storage/raw-store-driver.ts +80 -0
  265. package/src/storage/{restoration-coordinator-v2.ts → restoration-coordinator.ts} +43 -37
  266. package/src/storage/ring-selector.ts +317 -176
  267. package/src/storage/ring-shift-coordinator.ts +271 -0
  268. package/src/storage/storage-monitor.ts +70 -3
  269. package/src/storage/storage-repo.ts +710 -558
  270. package/src/sync/client.ts +5 -1
  271. package/src/sync/service.ts +2 -1
  272. package/src/testing/cohort-topic-mesh-harness.ts +61 -11
  273. package/src/testing/index.ts +1 -0
  274. package/src/testing/mesh-harness.ts +14 -6
  275. package/src/testing/raw-storage-conformance.ts +397 -0
@@ -1,28 +1,138 @@
1
- import type { PeerId } from '@libp2p/interface';
2
- import { sortPeersByDistance, type KnownPeer } from '../routing/responsibility.js';
3
-
4
- /**
5
- * Select arbitrators for a dispute using XOR-distance from the block ID.
6
- * Selects the next K peers beyond the original cluster (positions K+1 through 2K).
7
- * This ensures independence (arbitrators are not in the original cluster)
8
- * and determinism (all parties agree on who arbitrates).
9
- */
10
- export function selectArbitrators(
11
- allPeers: KnownPeer[],
12
- blockIdBytes: Uint8Array,
13
- excludePeerIds: Set<string>,
14
- count: number,
15
- ): PeerId[] {
16
- // Sort all peers by XOR distance to the block ID
17
- const sorted = sortPeersByDistance(allPeers, blockIdBytes);
18
-
19
- // Skip peers in the original cluster (and self), select the next K
20
- const arbitrators: PeerId[] = [];
21
- for (const peer of sorted) {
22
- if (arbitrators.length >= count) break;
23
- if (excludePeerIds.has(peer.id.toString())) continue;
24
- arbitrators.push(peer.id);
25
- }
26
-
27
- return arbitrators;
28
- }
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
+ }
@@ -40,6 +40,13 @@ export type CollectionEnv = {
40
40
  readonly collectionId: CollectionId;
41
41
  readonly log: Log<unknown>;
42
42
  readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
43
+ /**
44
+ * Optional per-block commit-latch runner threaded into each cascade child's {@link applyInvalidation}
45
+ * so the child's compensating write serializes against a concurrent commit on the same block — the
46
+ * same mutual-exclusion the root apply gets (see `InvalidationContext.withBlockCommitLatch`). Omitted
47
+ * → child writes run unlatched (today's behavior).
48
+ */
49
+ readonly withBlockCommitLatch?: <T>(blockId: BlockId, fn: () => Promise<T>) => Promise<T>;
43
50
  };
44
51
 
45
52
  // ─── Invalidated (blockId, revision) pairs ───
@@ -347,7 +354,7 @@ export async function cascadeInvalidate(input: CascadeInput): Promise<CascadeRes
347
354
 
348
355
  const env = cand.env;
349
356
  const result = await applyInvalidation(
350
- { log: env.log, createBlockStorage: env.createBlockStorage },
357
+ { log: env.log, createBlockStorage: env.createBlockStorage, withBlockCommitLatch: env.withBlockCommitLatch },
351
358
  {
352
359
  invalidatedActionId: cand.actionId,
353
360
  invalidatedRev: cand.rev,
@@ -1,6 +1,7 @@
1
1
  import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
2
2
  import { ProtocolClient } from '../protocol-client.js';
3
3
  import { DEFAULT_DIAL_TIMEOUT_MS, withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
4
+ import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
4
5
  import type { DisputeChallenge, DisputeResolution, ArbitrationVote, DisputeMessage } from './types.js';
5
6
 
6
7
  /**
@@ -32,7 +33,7 @@ export class DisputeClient extends ProtocolClient {
32
33
  const response = await this.processMessage<{ type: 'vote'; vote: ArbitrationVote }>(
33
34
  message,
34
35
  this.protocol,
35
- { signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS }
36
+ { signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS, maxDataLength: MAX_CONTROL_MESSAGE_BYTES }
36
37
  );
37
38
  return response.vote;
38
39
  }
@@ -45,7 +46,7 @@ export class DisputeClient extends ProtocolClient {
45
46
  await this.processMessage<{ type: 'ack' }>(
46
47
  message,
47
48
  this.protocol,
48
- withRpcDeadlineDefaults(options),
49
+ { ...withRpcDeadlineDefaults(options), maxDataLength: MAX_CONTROL_MESSAGE_BYTES },
49
50
  );
50
51
  }
51
52
  }
@@ -1,11 +1,12 @@
1
1
  import type { ClusterRecord, ITransactionValidator, InvalidateRequest, CommitRequest, PendRequest, CollectionId } from '@optimystic/db-core';
2
- import { blockIdsForTransforms } from '@optimystic/db-core';
2
+ import { blockIdsForTransforms, computeClusterPromiseHash, recordMembershipDigest } from '@optimystic/db-core';
3
3
  import { buildDisputeResolutionProof, computeTargetHash, computeArbitratorSetHash, voteSigningPayload, arbitratorSetSigningPayload, VOTE_VERSION, type CertificateTarget } from './invalidation.js';
4
4
  import type { PeerId, PrivateKey } from '@libp2p/interface';
5
5
  import { sha256 } from 'multiformats/hashes/sha2';
6
6
  import { base58btc } from 'multiformats/bases/base58';
7
7
  import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
8
8
  import { publicKeyFromRaw } from '@libp2p/crypto/keys';
9
+ import { peerIdBindsPublicKey } from '../cluster/peer-key-binding.js';
9
10
  import type {
10
11
  ValidationEvidence,
11
12
  DisputeChallenge,
@@ -39,8 +40,14 @@ export interface DisputeServiceInit {
39
40
  validator?: ITransactionValidator;
40
41
  revalidate?: RevalidateTransaction;
41
42
  config?: Partial<DisputeConfig>;
42
- /** Select arbitrators for a dispute (next K peers beyond the original cluster) */
43
- selectArbitrators: (blockId: string, excludePeers: string[], count: number) => Promise<PeerId[]>;
43
+ /**
44
+ * Select arbitrators for a dispute via verifiable dispersed sampling (see `sampleArbitrators`): draw
45
+ * `count` distinct peers from coordinates spread across the whole keyspace, not the block's neighborhood.
46
+ * `round` (0-based escalation round) and `epoch` (agreed membership epoch bytes) are folded into every
47
+ * coordinate so the draw is deterministic yet not pre-positionable — an honest node re-derives the
48
+ * identical set from the same `(blockId, round, epoch)` + agreed membership.
49
+ */
50
+ selectArbitrators: (blockId: string, excludePeers: string[], count: number, round: number, epoch: Uint8Array) => Promise<PeerId[]>;
44
51
  /**
45
52
  * Originates the durable reversal when a dispute resolves `challenger-wins`. The dissent
46
53
  * coordinator (the node that initiated the dispute, holding the original record) builds the
@@ -163,9 +170,18 @@ export class DisputeService {
163
170
  const originalPeers = Object.keys(record.peers);
164
171
  const arbitratorCount = this.config.arbitratorCount ?? originalPeers.length;
165
172
 
173
+ // Round 0 is the only round today (single-round arbitration); `design-dispute-synchronous-escalation`
174
+ // drives real-time round progression. `epoch` pins the dispersed draw to a membership the attacker
175
+ // cannot freely advance.
176
+ // NOTE: interim agreed-membership epoch — hash of the admission-gate-agreed responsible set
177
+ // (`Object.keys(record.peers)`). When `design-cluster-membership-agreement` lands, `epoch` becomes
178
+ // the agreed membership epoch rather than this locally-hashed stand-in.
179
+ const round = 0;
180
+ const epoch = await this.computeEpochBytes(originalPeers);
181
+
166
182
  let arbitrators: PeerId[];
167
183
  try {
168
- arbitrators = await this.selectArbitrators(blockId, originalPeers, arbitratorCount);
184
+ arbitrators = await this.selectArbitrators(blockId, originalPeers, arbitratorCount, round, epoch);
169
185
  } catch (err) {
170
186
  log('dispute-arbitrator-selection-failed', { disputeId, error: err instanceof Error ? err.message : String(err) });
171
187
  return undefined;
@@ -195,7 +211,7 @@ export class DisputeService {
195
211
 
196
212
  // Send challenge to all arbitrators and collect votes
197
213
  const votes = await this.collectVotes(challenge, arbitrators);
198
- const resolution = this.resolveDispute(challenge, votes);
214
+ const resolution = await this.resolveDispute(challenge, votes);
199
215
 
200
216
  this.resolvedChallenges.set(disputeId, challenge);
201
217
  this.activeDisputes.delete(disputeId);
@@ -234,10 +250,12 @@ export class DisputeService {
234
250
  const targetHash = await this.computeChallengeTargetHash(challenge);
235
251
  const setHash = await computeArbitratorSetHash(challenge.arbitratorSet ?? []);
236
252
 
237
- // Verify the challenge signature
253
+ // Verify the challenge signature — bound to the challenger's id so a relay cannot attach a key it
254
+ // controls under some other peer's id and pass a forged challenge as that peer.
238
255
  const validSignature = await this.verifyDisputeSignature(
239
256
  challenge.disputeId,
240
257
  challenge.signature,
258
+ challenge.challengerPeerId,
241
259
  challenge.originalRecord.peers[challenge.challengerPeerId]?.publicKey
242
260
  );
243
261
 
@@ -351,7 +369,7 @@ export class DisputeService {
351
369
  }
352
370
 
353
371
  /** Determine dispute resolution from collected votes */
354
- resolveDispute(challenge: DisputeChallenge, votes: ArbitrationVote[]): DisputeResolution {
372
+ async resolveDispute(challenge: DisputeChallenge, votes: ArbitrationVote[]): Promise<DisputeResolution> {
355
373
  const challengerVotes = votes.filter(v => v.vote === 'agree-with-challenger').length;
356
374
  const majorityVotes = votes.filter(v => v.vote === 'agree-with-majority').length;
357
375
  const totalDecisive = challengerVotes + majorityVotes;
@@ -366,11 +384,21 @@ export class DisputeService {
366
384
  outcome = 'inconclusive';
367
385
  } else if (challengerVotes >= superMajorityThreshold) {
368
386
  outcome = 'challenger-wins';
369
- // Penalize majority peers who approved the transaction
387
+ // Penalize majority peers who approved the transaction. The promise signatures in a challenge's
388
+ // originalRecord are NOT otherwise verified on the dispute path, so an attacker who crafts a
389
+ // challenge carrying a fabricated originalRecord could attach a forged approval under an honest
390
+ // peer's id to get that peer a FalseApproval penalty. Gate each false-approval on the approval
391
+ // being binding-valid (key bound to the id AND signature verifies); skip — never penalize —
392
+ // any approval that is unbound or invalid.
370
393
  const originalRecord = challenge.originalRecord;
394
+ const promiseHash = await this.computePromiseHash(originalRecord);
371
395
  for (const [peerId, signature] of Object.entries(originalRecord.promises)) {
372
396
  if (signature.type === 'approve' && peerId !== challenge.challengerPeerId) {
373
- affectedPeers.push({ peerId, reason: 'false-approval' });
397
+ if (await this.verifyPromiseSignature(originalRecord, peerId, promiseHash, signature)) {
398
+ affectedPeers.push({ peerId, reason: 'false-approval' });
399
+ } else {
400
+ log('dispute-skip-unverified-approval', { disputeId: challenge.disputeId, peerId });
401
+ }
374
402
  }
375
403
  }
376
404
  } else if (majorityVotes >= superMajorityThreshold) {
@@ -557,6 +585,19 @@ export class DisputeService {
557
585
  };
558
586
  }
559
587
 
588
+ /**
589
+ * Interim agreed-membership epoch bytes for the dispersed arbitrator draw: SHA-256 of the sorted,
590
+ * comma-joined responsible peer-id set (the same set the admission gate agrees on). Sorted so the
591
+ * digest is independent of enumeration order, so every honest node computes the identical epoch — the
592
+ * property that lets the verify path re-derive the same arbitrator set. Replaced by the real agreed
593
+ * membership epoch when `design-cluster-membership-agreement` lands.
594
+ */
595
+ private async computeEpochBytes(peers: string[]): Promise<Uint8Array> {
596
+ const joined = [...peers].sort().join(',');
597
+ const digest = await sha256.digest(new TextEncoder().encode(joined));
598
+ return digest.digest;
599
+ }
600
+
560
601
  private async computeDisputeId(messageHash: string, peerId: string, timestamp: number): Promise<string> {
561
602
  const input = `${messageHash}+${peerId}+${timestamp}`;
562
603
  const inputBytes = new TextEncoder().encode(input);
@@ -583,6 +624,7 @@ export class DisputeService {
583
624
  private async verifyDisputeSignature(
584
625
  disputeId: string,
585
626
  signature: string,
627
+ challengerPeerId: string,
586
628
  publicKey?: string | Uint8Array
587
629
  ): Promise<boolean> {
588
630
  if (!publicKey?.length) return false;
@@ -590,6 +632,9 @@ export class DisputeService {
590
632
  const keyBytes = typeof publicKey === 'string'
591
633
  ? uint8ArrayFromString(publicKey, 'base64url')
592
634
  : publicKey;
635
+ // The key must be the one the challenger's id provably names, else a forged challenge could be
636
+ // attributed to any peer id while signed by a key the relay controls.
637
+ if (!peerIdBindsPublicKey(challengerPeerId, keyBytes)) return false;
593
638
  const pubKey = publicKeyFromRaw(keyBytes);
594
639
  const payload = new TextEncoder().encode(disputeId);
595
640
  const sigBytes = uint8ArrayFromString(signature, 'base64url');
@@ -599,6 +644,46 @@ export class DisputeService {
599
644
  }
600
645
  }
601
646
 
647
+ /**
648
+ * Reconstruct the cluster promise-vote hash for `record` — identical to `ClusterMember.computePromiseHash`,
649
+ * and version-dispatched the same way: a v2 record folds its membership digest into the preimage, a v1 /
650
+ * unversioned record hashes byte-identically to before. The promise signatures in a disputed record were
651
+ * produced over this by the cluster path, so re-verifying an approval requires reproducing it here — a v2
652
+ * originalRecord whose digest we omitted would fail to verify its (honest) approvals.
653
+ */
654
+ private async computePromiseHash(record: ClusterRecord): Promise<string> {
655
+ return computeClusterPromiseHash(record.messageHash, record.message, recordMembershipDigest(record));
656
+ }
657
+
658
+ /**
659
+ * True iff `signature` on `record` is a binding-valid promise vote from `peerId`: the record's key for
660
+ * `peerId` must be the one that id provably names AND the signature must verify over the reconstructed
661
+ * promise-vote preimage. Total: returns `false` (never throws) on a missing/unbound/malformed key or an
662
+ * undecodable signature. Mirrors `ClusterMember.verifySignature`'s binding-then-verify order.
663
+ */
664
+ private async verifyPromiseSignature(
665
+ record: ClusterRecord,
666
+ peerId: string,
667
+ promiseHash: string,
668
+ signature: { type: string; signature: string; rejectReason?: string }
669
+ ): Promise<boolean> {
670
+ const publicKey = record.peers[peerId]?.publicKey;
671
+ if (!publicKey?.length) return false;
672
+ try {
673
+ const keyBytes = typeof publicKey === 'string'
674
+ ? uint8ArrayFromString(publicKey, 'base64url')
675
+ : publicKey;
676
+ if (!peerIdBindsPublicKey(peerId, keyBytes)) return false;
677
+ const pubKey = publicKeyFromRaw(keyBytes);
678
+ const payloadStr = promiseHash + ':' + signature.type + (signature.rejectReason ? ':' + signature.rejectReason : '');
679
+ const payload = new TextEncoder().encode(payloadStr);
680
+ const sigBytes = uint8ArrayFromString(signature.signature, 'base64url');
681
+ return pubKey.verify(payload, sigBytes);
682
+ } catch {
683
+ return false;
684
+ }
685
+ }
686
+
602
687
  private findChallengeForDispute(disputeId: string): DisputeChallenge | undefined {
603
688
  return this.activeDisputes.get(disputeId) ?? this.resolvedChallenges.get(disputeId);
604
689
  }
@@ -14,7 +14,13 @@ export {
14
14
  type EngineHealthState,
15
15
  DEFAULT_DISPUTE_CONFIG,
16
16
  } from './types.js';
17
- export { selectArbitrators } from './arbitrator-selection.js';
17
+ export {
18
+ sampleArbitrators,
19
+ coordinatePreimage,
20
+ type ArbitratorSamplingParams,
21
+ type NearestResolver,
22
+ type RingHashFn,
23
+ } from './arbitrator-selection.js';
18
24
  export {
19
25
  buildDisputeResolutionProof,
20
26
  verifyInvalidationCertificate,
@@ -82,6 +82,21 @@ export type ArbitratorSetRecomputeContext = {
82
82
  readonly blockIds: ReadonlyArray<BlockId>;
83
83
  /** The carried set being judged (peer-id strings). */
84
84
  readonly arbitratorSet: ReadonlyArray<string>;
85
+ /**
86
+ * Escalation round the carried set was drawn at (0-based). A recompute capability MUST feed this to
87
+ * `sampleArbitrators` so it re-derives the SAME dispersed draw. Today every dispute is round 0
88
+ * (single-round arbitration), so the verify path pins it to 0; `design-dispute-synchronous-escalation`
89
+ * threads the real round through the proof and updates this seam.
90
+ */
91
+ readonly round: number;
92
+ /**
93
+ * Agreed membership epoch the carried set was drawn against — the bytes `sampleArbitrators` folds into
94
+ * every coordinate. A recompute capability MUST feed this so its re-derivation matches the original
95
+ * draw. Optional until `design-dispute-synchronous-escalation` carries/derives it on the proof; while
96
+ * absent a recompute cannot reconstruct the draw and should return `{ feasible: false }` (degradation),
97
+ * which matches today's behavior — no recompute capability is wired in production.
98
+ */
99
+ readonly epoch?: Uint8Array;
85
100
  };
86
101
 
87
102
  /**
@@ -291,6 +306,11 @@ export async function verifyInvalidationCertificate(proof: DisputeResolutionProo
291
306
  invalidatedActionId: target.invalidatedActionId,
292
307
  blockIds: target.blockIds,
293
308
  arbitratorSet: proof.arbitratorSet,
309
+ // NOTE: `round` pinned to 0 (single-round arbitration today) and `epoch` omitted until
310
+ // `design-dispute-synchronous-escalation` threads the real (round, epoch) through the proof, so a
311
+ // `sampleArbitrators`-based recompute re-derives the identical dispersed set. Until then no recompute
312
+ // capability is wired in production; the test recomputes ignore these fields.
313
+ round: 0,
294
314
  });
295
315
  if (verdict.feasible) {
296
316
  if (!verdict.legitimate) {
@@ -458,6 +478,16 @@ export type InvalidationContext = {
458
478
  readonly log: Log<unknown>;
459
479
  /** Resolves a block's storage so the compensating revision can be written. */
460
480
  readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
481
+ /**
482
+ * Runs `fn` while holding the same per-block commit latch `StorageRepo.commit` and
483
+ * `saveReplicatedBlock` hold, so the compensating `saveReplica`/`saveDeletion` read-modify-write of
484
+ * `meta.latest` is mutually exclusive with a concurrent local commit on that block. Without it, an
485
+ * invalidation advancing `latest` outside that latch is invisible to commit's staleness guard, so a
486
+ * commit that read a stale `latest` can clobber it back down — a lost-update / non-monotonic
487
+ * regression. Optional: when omitted (unit tests / non-`StorageRepo` hosts) the write runs unlatched,
488
+ * preserving today's behavior. The host supplies `StorageRepo`'s `withBlockCommitLatch`.
489
+ */
490
+ readonly withBlockCommitLatch?: <T>(blockId: BlockId, fn: () => Promise<T>) => Promise<T>;
461
491
  };
462
492
 
463
493
  export type ApplyInvalidationParams = {
@@ -552,11 +582,19 @@ export async function applyInvalidation(ctx: InvalidationContext, params: ApplyI
552
582
  // Deterministic compensating-revision actionId — identical on every member, so all converge on
553
583
  // the same (rev, actionId) for both the restore and the tombstone path.
554
584
  const revertActionId = await hashString(`inv:${invalidatedActionId}:${proof.disputeId}:${blockId}:${rev}`);
585
+ // Hold the per-block commit latch around ONLY the compensating write (matching saveReplicatedBlock's
586
+ // scope): the monotonic guard inside saveReplica/saveDeletion then runs under the same latch a
587
+ // concurrent commit holds, so the two RMW of meta.latest serialize and latest stays monotonic.
588
+ // Acquire per block, one at a time — invalidation never holds two block latches, so it cannot
589
+ // deadlock against commit's sorted multi-latch acquisition. When no runner is injected, the write
590
+ // runs unlatched (today's behavior).
591
+ const runLatched = <T>(fn: () => Promise<T>): Promise<T> =>
592
+ ctx.withBlockCommitLatch ? ctx.withBlockCommitLatch(blockId, fn) : fn();
555
593
  if (computation.kind === 'delete') {
556
594
  // Block-creation reversal: physically remove the created block by writing a forward tombstone
557
595
  // revision. The `restoredContentHash` is the DELETED_BLOCK_RESTORE sentinel — a deleted block
558
596
  // has no content hash, and the sentinel tells dependents "observed content is gone → invalidate".
559
- await storage.saveDeletion({ rev, actionId: revertActionId });
597
+ await runLatched(() => storage.saveDeletion({ rev, actionId: revertActionId }));
560
598
  log('apply-delete-restore blockId=%s invalidatedRev=%d rev=%d', blockId, invalidatedRev, rev);
561
599
  reverted.push({ blockId, fromRev: computation.fromRev, restoredContentHash: DELETED_BLOCK_RESTORE });
562
600
  continue;
@@ -565,7 +603,7 @@ export async function applyInvalidation(ctx: InvalidationContext, params: ApplyI
565
603
  // Surviving later actions were replayed verbatim; true read-dependents are out of scope here.
566
604
  log('apply-replayed-later-actions blockId=%s count=%d', blockId, computation.laterActions);
567
605
  }
568
- await storage.saveReplica(computation.block, { rev, actionId: revertActionId });
606
+ await runLatched(() => storage.saveReplica(computation.block, { rev, actionId: revertActionId }));
569
607
  reverted.push({ blockId, fromRev: computation.fromRev, restoredContentHash: computation.restoredContentHash });
570
608
  }
571
609
 
@@ -2,6 +2,7 @@ import { pipe } from 'it-pipe';
2
2
  import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed';
3
3
  import type { Startable, Logger, Stream, Connection, StreamHandler } from '@libp2p/interface';
4
4
  import type { Uint8ArrayList } from 'uint8arraylist';
5
+ import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
5
6
  import type { DisputeMessage } from './types.js';
6
7
  import type { DisputeService } from './dispute-service.js';
7
8
 
@@ -95,6 +96,11 @@ export class DisputeProtocolService implements Startable {
95
96
  }
96
97
 
97
98
  yield new TextEncoder().encode(JSON.stringify(response));
99
+ // One request per stream: every real DisputeClient sends exactly one
100
+ // request per dial (see ProtocolClient.processMessage), so complete the
101
+ // generator after the first response. A second frame a peer queued is then
102
+ // never read or parsed. Mirrors sync/block-transfer.
103
+ return;
98
104
  }
99
105
  };
100
106
 
@@ -102,7 +108,7 @@ export class DisputeProtocolService implements Startable {
102
108
  try {
103
109
  const responses = pipe(
104
110
  stream,
105
- (source) => lpDecode(source),
111
+ (source) => lpDecode(source, { maxDataLength: MAX_CONTROL_MESSAGE_BYTES }),
106
112
  processStream.bind(this),
107
113
  (source) => lpEncode(source)
108
114
  );
package/src/index.ts CHANGED
@@ -1,43 +1,46 @@
1
- export * from "./cluster/client.js";
2
- export * from "./cluster/cluster-repo.js";
3
- export * from "./cluster/commit-cert.js";
4
- export * from "./cluster/service.js";
5
- export * from "./cluster/rebalance-monitor.js";
6
- export * from "./cluster/spread-on-churn.js";
7
- export * from "./cluster/block-transfer.js";
8
- export * from "./cluster/block-transfer-service.js";
9
- export * from "./protocol-client.js";
10
- export * from "./repo/client.js";
11
- export * from "./repo/cluster-coordinator.js";
12
- export * from "./repo/coordinator-repo.js";
13
- export * from "./repo/service.js";
14
- export * from "./storage/block-storage.js";
15
- export * from "./storage/memory-storage.js";
16
- export * from "./storage/i-block-storage.js";
17
- export * from "./storage/i-raw-storage.js";
18
- export * from "./storage/struct.js";
19
- export * from "./storage/storage-repo.js";
20
- export * from "./storage/restoration-coordinator-v2.js";
21
- export * from "./storage/ring-selector.js";
22
- export * from "./storage/storage-monitor.js";
23
- export * from "./storage/arachnode-fret-adapter.js";
24
- export * from "./sync/protocol.js";
25
- export * from "./sync/client.js";
26
- export * from "./sync/service.js";
27
- export * from "./it-utility.js";
28
- export * from "./libp2p-key-network.js";
29
- export * from "./libp2p-node.js";
30
- export * from "./routing/responsibility.js";
31
- export * from "./routing/libp2p-known-peers.js";
32
- export * from "./network/network-manager-service.js";
33
- export * from "./network/get-network-manager.js";
34
- export * from "./reputation/index.js";
35
- export * from "./dispute/index.js";
36
- export * from "./cohort-topic/index.js";
37
- export * from "./matchmaking/index.js";
38
- export * from "./reactivity/index.js";
39
- export * from "./cluster/i-transaction-state-store.js";
40
- export * from "./cluster/memory-transaction-state-store.js";
41
- export * from "./cluster/persistent-transaction-state-store.js";
42
- export * from "./storage/i-kv-store.js";
43
- export * from "./storage/memory-kv-store.js";
1
+ export * from "./cluster/client.js";
2
+ export * from "./cluster/cluster-repo.js";
3
+ export * from "./cluster/commit-cert.js";
4
+ export * from "./cluster/service.js";
5
+ export * from "./cluster/rebalance-monitor.js";
6
+ export * from "./cluster/spread-on-churn.js";
7
+ export * from "./cluster/block-transfer.js";
8
+ export * from "./cluster/block-transfer-service.js";
9
+ export * from "./protocol-client.js";
10
+ export * from "./repo/client.js";
11
+ export * from "./repo/cluster-coordinator.js";
12
+ export * from "./repo/coordinator-repo.js";
13
+ export * from "./repo/service.js";
14
+ export * from "./storage/block-storage.js";
15
+ export * from "./storage/raw-store-driver.js";
16
+ export * from "./storage/kv-raw-storage.js";
17
+ export * from "./storage/memory-store-driver.js";
18
+ export * from "./storage/memory-storage.js";
19
+ export * from "./storage/i-block-storage.js";
20
+ export * from "./storage/i-raw-storage.js";
21
+ export * from "./storage/struct.js";
22
+ export * from "./storage/storage-repo.js";
23
+ export * from "./storage/restoration-coordinator.js";
24
+ export * from "./storage/ring-selector.js";
25
+ export * from "./storage/storage-monitor.js";
26
+ export * from "./storage/arachnode-fret-adapter.js";
27
+ export * from "./sync/protocol.js";
28
+ export * from "./sync/client.js";
29
+ export * from "./sync/service.js";
30
+ export * from "./it-utility.js";
31
+ export * from "./libp2p-key-network.js";
32
+ export * from "./libp2p-node.js";
33
+ export * from "./routing/responsibility.js";
34
+ export * from "./routing/libp2p-known-peers.js";
35
+ export * from "./network/network-manager-service.js";
36
+ export * from "./network/get-network-manager.js";
37
+ export * from "./reputation/index.js";
38
+ export * from "./dispute/index.js";
39
+ export * from "./cohort-topic/index.js";
40
+ export * from "./matchmaking/index.js";
41
+ export * from "./reactivity/index.js";
42
+ export * from "./cluster/i-transaction-state-store.js";
43
+ export * from "./cluster/memory-transaction-state-store.js";
44
+ export * from "./cluster/persistent-transaction-state-store.js";
45
+ export * from "./storage/i-kv-store.js";
46
+ export * from "./storage/memory-kv-store.js";
package/src/it-utility.ts CHANGED
@@ -1,19 +1,11 @@
1
1
  export async function first<T>(
2
- createIterable: (signal: AbortSignal) => AsyncIterable<T>,
3
- onEmpty: () => T = () => { throw new Error('No items found') },
4
- timeoutMs?: number
2
+ createIterable: () => AsyncIterable<T>,
3
+ onEmpty: () => T = () => { throw new Error('No items found') }
5
4
  ): Promise<T> {
6
- const controller = new AbortController();
7
- const timer = typeof timeoutMs === 'number' ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
8
- try {
9
- for await (const item of createIterable(controller.signal)) {
10
- return item;
11
- }
12
- return onEmpty();
13
- } finally {
14
- if (timer !== undefined) clearTimeout(timer);
15
- controller.abort();
5
+ for await (const item of createIterable()) {
6
+ return item;
16
7
  }
8
+ return onEmpty();
17
9
  }
18
10
 
19
11
  export async function asyncIteratorToArray<T>(iterator: AsyncIterable<T>): Promise<T[]> {