@optimystic/db-p2p 0.14.0 → 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,5 +1,5 @@
1
1
  import type { PendRequest, ActionBlocks, IRepo, MessageOptions, CommitResult, GetBlockResults, PendResult, BlockGets, CommitRequest, RepoMessage, IKeyNetwork, ICluster, ClusterConsensusConfig, BlockId, ActionRev, ActionContext, ClusterRecord } from "@optimystic/db-core";
2
- import { LruMap, blockIdsForTransforms } from "@optimystic/db-core";
2
+ import { LruMap, blockIdsForTransforms, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
3
3
  import { ClusterCoordinator } from "./cluster-coordinator.js";
4
4
  import type { ClusterClient } from "../cluster/client.js";
5
5
  import type { PeerId } from "@libp2p/interface";
@@ -7,7 +7,9 @@ import { peerIdFromString } from "@libp2p/peer-id";
7
7
  import type { FretService } from "p2p-fret";
8
8
  import { createLogger } from '../logger.js';
9
9
  import type { IPeerReputation } from "../reputation/types.js";
10
+ import { PenaltyReason } from "../reputation/types.js";
10
11
  import type { ITransactionStateStore } from "../cluster/i-transaction-state-store.js";
12
+ import { selectQuorumRev, type RevClaim, type QuorumRev } from "../cluster/quorum-restore.js";
11
13
 
12
14
  const log = createLogger('coordinator-repo');
13
15
 
@@ -69,6 +71,11 @@ export class CoordinatorRepo implements IRepo {
69
71
  private readonly readRepairMode: 'off' | 'lazy' | 'paranoid';
70
72
  private readonly readRepairWindowMs: number;
71
73
  private readonly readRepairSampleRate: number;
74
+ /** Simple-majority threshold from the consensus policy; drives the read-repair corroboration quorum. */
75
+ private readonly simpleMajorityThreshold: number;
76
+ /** Resolved super-majority threshold the coordinator commits on (mirrors the value handed to ClusterCoordinator). */
77
+ private readonly superMajorityThreshold: number;
78
+ private readonly reputation?: IPeerReputation;
72
79
  /** Test seam: overridable clock for window-based read-repair gating. */
73
80
  now: () => number = () => Date.now();
74
81
  /** Test seam: overridable RNG (0..1) for sample-rate gating. */
@@ -89,7 +96,7 @@ export class CoordinatorRepo implements IRepo {
89
96
  this.localPeerId = localPeerId;
90
97
  const policy: ClusterConsensusConfig & { clusterSize: number } = {
91
98
  clusterSize: cfg?.clusterSize ?? 10,
92
- superMajorityThreshold: cfg?.superMajorityThreshold ?? 0.75,
99
+ superMajorityThreshold: cfg?.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
93
100
  simpleMajorityThreshold: cfg?.simpleMajorityThreshold ?? 0.51,
94
101
  minAbsoluteClusterSize: cfg?.minAbsoluteClusterSize ?? 3,
95
102
  allowClusterDownsize: cfg?.allowClusterDownsize ?? true,
@@ -100,13 +107,20 @@ export class CoordinatorRepo implements IRepo {
100
107
  commitBroadcastRetryMaxIntervalMs: cfg?.commitBroadcastRetryMaxIntervalMs ?? 8000,
101
108
  commitBroadcastRetryMaxAttempts: cfg?.commitBroadcastRetryMaxAttempts ?? 5,
102
109
  commitBroadcastImmediateRetries: cfg?.commitBroadcastImmediateRetries ?? 1,
110
+ promiseImmediateRetries: cfg?.promiseImmediateRetries ?? 1,
103
111
  readRepairMode: cfg?.readRepairMode ?? 'lazy',
104
112
  readRepairWindowMs: cfg?.readRepairWindowMs ?? 10000,
105
- readRepairSampleRate: cfg?.readRepairSampleRate ?? 0
113
+ readRepairSampleRate: cfg?.readRepairSampleRate ?? 0,
114
+ // Default false: an undersized cluster with no confident network-size estimate
115
+ // is REJECTED (fail closed). Callers only opt in for single-node/local/test meshes.
116
+ allowUnvalidatedSmallCluster: cfg?.allowUnvalidatedSmallCluster ?? false
106
117
  };
107
118
  this.readRepairMode = policy.readRepairMode!;
108
119
  this.readRepairWindowMs = policy.readRepairWindowMs!;
109
120
  this.readRepairSampleRate = policy.readRepairSampleRate!;
121
+ this.simpleMajorityThreshold = policy.simpleMajorityThreshold;
122
+ this.superMajorityThreshold = policy.superMajorityThreshold;
123
+ this.reputation = reputation;
110
124
  const localClusterRef = localCluster && localPeerId ? {
111
125
  update: localCluster.update.bind(localCluster),
112
126
  peerId: localPeerId,
@@ -115,6 +129,15 @@ export class CoordinatorRepo implements IRepo {
115
129
  this.coordinator = new ClusterCoordinator(keyNetwork, createClusterClient, policy, localClusterRef, fretService, reputation, stateStore);
116
130
  }
117
131
 
132
+ /**
133
+ * The resolved super-majority threshold this coordinator commits on. Exposed so the composition root
134
+ * can fail-fast if the coordinator and the cluster member would run different thresholds (see the
135
+ * coupling assertion in `libp2p-node-base.ts`).
136
+ */
137
+ get effectiveSuperMajorityThreshold(): number {
138
+ return this.superMajorityThreshold;
139
+ }
140
+
118
141
  /** Recover coordinator transactions from persistent store after a restart. */
119
142
  async recoverTransactions(): Promise<void> {
120
143
  await this.coordinator.recoverTransactions();
@@ -183,6 +206,13 @@ export class CoordinatorRepo implements IRepo {
183
206
  // (b) Stale-by-policy — block is present but read-repair policy says verify.
184
207
  // Skip cluster fetch if this is already a sync request (to prevent recursive queries).
185
208
  const skipClusterFetch = (options as any)?.skipClusterFetch;
209
+ // NOTE: NetworkTransactor.get treats an authoritative "absent" ({ state: {} })
210
+ // as final and no longer retries it (ticket txn-perf-authoritative-notfound),
211
+ // relying on this cluster reconciliation to have already run. If a coordinator
212
+ // is ever configured WITHOUT clusterLatestCallback, a missing block is answered
213
+ // from local state alone with no transactor-level retry to compensate. That is
214
+ // fine today (such a coordinator has no cluster to reconcile against), but keep
215
+ // this coupling in mind if a partial-cluster read path is added.
186
216
  if (this.clusterLatestCallback && !skipClusterFetch) {
187
217
  for (const blockId of blockGets.blockIds) {
188
218
  const localEntry = localResult[blockId];
@@ -292,11 +322,23 @@ export class CoordinatorRepo implements IRepo {
292
322
  }
293
323
 
294
324
  /**
295
- * Query cluster peers to find the maximum latest revision for a block.
325
+ * Query cluster peers for their latest revision and return the highest
326
+ * revision corroborated by a quorum of distinct peers.
327
+ *
328
+ * Replaces the old "max rev any single peer reports" — which let one lying
329
+ * peer over-reporting its revision steer restoration — with quorum
330
+ * corroboration on the exact `(rev, actionId)` pair (see
331
+ * {@link selectQuorumRev}). The local node's own latest is included as a
332
+ * corroborating vote because `clusterLatestCallback` self-short-circuits to
333
+ * local storage. Returns `undefined` (keep local, do not restore) when no
334
+ * revision is corroborated.
335
+ *
336
+ * NOTE: the quorum is corroboration-of-a-claim, NOT Sybil-resistant cohort
337
+ * membership — a peer minting fresh keypairs still casts a vote. Commit-cert
338
+ * + membership anchoring is deferred to backlog
339
+ * `debt-read-repair-commit-cert-verification`.
296
340
  */
297
341
  private async queryClusterForLatest(peerIds: string[], blockId: BlockId, context?: ActionContext): Promise<ActionRev | undefined> {
298
- let maxLatest: ActionRev | undefined;
299
-
300
342
  // Add timeout wrapper to prevent hanging on unresponsive peers
301
343
  const withTimeout = <T>(promise: Promise<T>, timeoutMs: number): Promise<T | undefined> =>
302
344
  Promise.race([
@@ -304,30 +346,68 @@ export class CoordinatorRepo implements IRepo {
304
346
  new Promise<undefined>(resolve => setTimeout(() => resolve(undefined), timeoutMs))
305
347
  ]);
306
348
 
307
- // Query peers in parallel for their latest revision (with 1s timeout per peer)
349
+ // Query peers in parallel for their latest revision (with 1s timeout per peer),
350
+ // tagging each response with the peer that made it so votes stay distinct.
308
351
  const latestResults = await Promise.allSettled(
309
- peerIds.map(peerIdStr => {
352
+ peerIds.map(async peerIdStr => {
310
353
  const peerId = peerIdFromString(peerIdStr);
311
- return withTimeout(this.clusterLatestCallback!(peerId, blockId, context), 1000);
354
+ const value = await withTimeout(this.clusterLatestCallback!(peerId, blockId, context), 1000);
355
+ return { peerIdStr, value };
312
356
  })
313
357
  );
314
358
 
359
+ const claims: RevClaim[] = [];
315
360
  for (const result of latestResults) {
316
- if (result.status === 'fulfilled' && result.value) {
317
- const peerLatest = result.value;
318
- if (!maxLatest || peerLatest.rev > maxLatest.rev) {
319
- maxLatest = peerLatest;
320
- }
361
+ if (result.status === 'fulfilled' && result.value.value) {
362
+ const { peerIdStr, value } = result.value;
363
+ claims.push({ peerId: peerIdStr, rev: value.rev, actionId: value.actionId });
321
364
  }
322
365
  }
323
366
 
324
- return maxLatest;
367
+ const selected = selectQuorumRev(claims, this.simpleMajorityThreshold);
368
+ if (!selected) {
369
+ log('cluster-fetch:no-quorum', { blockId, responders: claims.length });
370
+ return undefined;
371
+ }
372
+
373
+ // Best-effort: penalize peers whose claim contradicts the corroborated pair
374
+ // (an inflated rev the quorum outvoted, or conflicting content at the agreed
375
+ // rev). A lower rev is just lag, never penalized. Never let this throw.
376
+ this.penalizeContradictingRevClaims(claims, selected, blockId);
377
+
378
+ return { actionId: selected.actionId, rev: selected.rev };
379
+ }
380
+
381
+ /**
382
+ * Report peers whose reported latest contradicts the quorum-corroborated pair. Best-effort.
383
+ *
384
+ * NOTE: the `rev > selected.rev` branch is NOT provable misbehavior — an honest
385
+ * peer legitimately ahead of the sampled quorum (an in-flight commit it durably
386
+ * stored, or other honest holders dropped from the sample by the 1s per-peer
387
+ * timeout) reports a higher rev and gets penalized (weight 30 → immediate
388
+ * deprioritize at threshold 20). Distinguishing a liar from an honest leader
389
+ * needs the commit-cert verification tracked in backlog
390
+ * `debt-read-repair-penalty-provable-only` / `debt-read-repair-commit-cert-verification`.
391
+ */
392
+ private penalizeContradictingRevClaims(claims: RevClaim[], selected: QuorumRev, blockId: BlockId): void {
393
+ if (!this.reputation) return;
394
+ try {
395
+ for (const c of claims) {
396
+ const contradicts = c.rev > selected.rev
397
+ || (c.rev === selected.rev && c.actionId !== selected.actionId);
398
+ if (contradicts) {
399
+ this.reputation.reportPeer(c.peerId, PenaltyReason.InvalidRestoration, `read-repair:${blockId}`);
400
+ }
401
+ }
402
+ } catch (err) {
403
+ log('cluster-fetch:penalize-error', { blockId, error: (err as Error).message });
404
+ }
325
405
  }
326
406
 
327
407
  async pend(request: PendRequest, options?: MessageOptions): Promise<PendResult> {
328
408
  const allBlockIds = blockIdsForTransforms(request.transforms);
329
409
  await this.verifyResponsibility(allBlockIds);
330
- const coordinatingBlockIds = (options as any)?.coordinatingBlockIds ?? allBlockIds;
410
+ const coordinatingBlockIds = options?.coordinatingBlockIds ?? allBlockIds;
331
411
 
332
412
  const peerCount = await this.coordinator.getClusterSize(coordinatingBlockIds[0]!);
333
413
  if (peerCount <= 1) {
@@ -5,6 +5,7 @@ import type { IRepo, RepoMessage } from '@optimystic/db-core'
5
5
  import { blockIdsForTransforms } from '@optimystic/db-core'
6
6
  import { peersEqual } from '../peer-utils.js'
7
7
  import { encodePeers, type RedirectPayload } from './redirect.js'
8
+ import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js'
8
9
  import type { Uint8ArrayList } from 'uint8arraylist'
9
10
  import { createLogger } from '../logger.js'
10
11
 
@@ -267,6 +268,11 @@ export class RepoService implements Startable {
267
268
 
268
269
  // Encode and yield the response
269
270
  yield new TextEncoder().encode(JSON.stringify(response))
271
+ // One request per stream: every real RepoClient sends exactly one request
272
+ // per dial (see ProtocolClient.processMessage), so complete the generator
273
+ // after the first response. A second frame a peer queued is then never read
274
+ // or parsed. Mirrors sync/block-transfer.
275
+ return
270
276
  }
271
277
  }
272
278
 
@@ -274,7 +280,7 @@ export class RepoService implements Startable {
274
280
  try {
275
281
  const responses = pipe(
276
282
  stream,
277
- (source) => lpDecode(source),
283
+ (source) => lpDecode(source, { maxDataLength: MAX_BLOCK_MESSAGE_BYTES }),
278
284
  processStream.bind(this),
279
285
  (source) => lpEncode(source)
280
286
  )
@@ -20,6 +20,8 @@ export enum PenaltyReason {
20
20
  FalseApproval = 'false-approval',
21
21
  /** Challenger lost a dispute (their rejection was wrong) */
22
22
  DisputeLost = 'dispute-lost',
23
+ /** Peer reported a block latest (rev/actionId) or served content contradicted by a cluster quorum during restoration */
24
+ InvalidRestoration = 'invalid-restoration',
23
25
  }
24
26
 
25
27
  /** Default penalty weights by reason */
@@ -34,6 +36,7 @@ export const DEFAULT_PENALTY_WEIGHTS: Record<PenaltyReason, number> = {
34
36
  [PenaltyReason.ConnectionFailure]: 2,
35
37
  [PenaltyReason.FalseApproval]: 40,
36
38
  [PenaltyReason.DisputeLost]: 30,
39
+ [PenaltyReason.InvalidRestoration]: 30,
37
40
  };
38
41
 
39
42
  /** Thresholds controlling graduated reputation responses */
package/src/rn.ts CHANGED
@@ -7,12 +7,15 @@ export * from './repo/cluster-coordinator.js';
7
7
  export * from './repo/coordinator-repo.js';
8
8
  export * from './repo/service.js';
9
9
  export * from './storage/block-storage.js';
10
+ export * from './storage/raw-store-driver.js';
11
+ export * from './storage/kv-raw-storage.js';
12
+ export * from './storage/memory-store-driver.js';
10
13
  export * from './storage/memory-storage.js';
11
14
  export * from './storage/i-block-storage.js';
12
15
  export * from './storage/i-raw-storage.js';
13
16
  export * from './storage/struct.js';
14
17
  export * from './storage/storage-repo.js';
15
- export * from './storage/restoration-coordinator-v2.js';
18
+ export * from './storage/restoration-coordinator.js';
16
19
  export * from './storage/ring-selector.js';
17
20
  export * from './storage/storage-monitor.js';
18
21
  export * from './storage/arachnode-fret-adapter.js';
@@ -26,3 +29,6 @@ export * from './routing/responsibility.js';
26
29
  export * from './routing/libp2p-known-peers.js';
27
30
  export * from './network/network-manager-service.js';
28
31
  export * from './network/get-network-manager.js';
32
+ // Browser-safe peer signing seam. The rest of ./cohort-topic pulls node-heavy host.js,
33
+ // so only peer-sig (@noble/curves + peer-id) is surfaced through the RN/browser entry.
34
+ export * from './cohort-topic/peer-sig.js';
@@ -23,6 +23,24 @@ export interface ArachnodeInfo {
23
23
 
24
24
  /** Ring membership status */
25
25
  status: 'joining' | 'active' | 'moving' | 'leaving';
26
+
27
+ /**
28
+ * The ring/partition this node is moving *from*, present only while `status === 'moving'`.
29
+ *
30
+ * During a ring shift the node advertises its **target** `ringDepth`/`partition` (Phase A) but
31
+ * keeps serving its old range until it has confirmed the shed range is replicated elsewhere
32
+ * (Phase B) and releases it (Phase C). `moveFrom` records that old range so peers can treat a
33
+ * `moving` node as *still responsible for its old range* (fail-toward-old-holder), and so a
34
+ * crash between advertise and release is recoverable. Cleared on release/rollback (→ `active`).
35
+ * See `docs/arachnode-ring-handoff.md` § Part 2 & Part 3.
36
+ */
37
+ moveFrom?: {
38
+ ringDepth: number;
39
+ partition?: {
40
+ prefixBits: number;
41
+ prefixValue: number;
42
+ };
43
+ };
26
44
  }
27
45
 
28
46
  /**
@@ -33,7 +51,15 @@ export interface ArachnodeInfo {
33
51
  export class ArachnodeFretAdapter {
34
52
  private static readonly ARACHNODE_KEY = 'arachnode';
35
53
 
36
- constructor(private readonly fret: FretService) {}
54
+ constructor(
55
+ private readonly fret: FretService,
56
+ /**
57
+ * This node's own peer id string. When provided, {@link getMyArachnodeInfo} uses it
58
+ * directly instead of reaching into the concrete FretService impl's private `.node` —
59
+ * {@link FretService} (the public interface) exposes no self-peer accessor.
60
+ */
61
+ private readonly selfPeerId?: string
62
+ ) {}
37
63
 
38
64
  /**
39
65
  * Set this node's Arachnode ring membership.
@@ -56,7 +82,12 @@ export class ArachnodeFretAdapter {
56
82
  * Get my own Arachnode info.
57
83
  */
58
84
  getMyArachnodeInfo(): ArachnodeInfo | undefined {
59
- const myPeerId = (this.fret as any).node?.peerId?.toString();
85
+ // Prefer the injected self peer id. Fall back to the concrete Libp2pFretService's private
86
+ // `.node.peerId` only when no id was injected — this cast is load-bearing (it depends on the
87
+ // concrete impl's shape, not the public FretService interface) and silently yields undefined
88
+ // for any impl lacking `.node`, so injection is the intended path.
89
+ const myPeerId = this.selfPeerId
90
+ ?? (this.fret as unknown as { node?: { peerId?: { toString(): string } } }).node?.peerId?.toString();
60
91
  if (!myPeerId) return undefined;
61
92
  return this.getArachnodeInfo(myPeerId);
62
93
  }
@@ -0,0 +1,74 @@
1
+ import type { ArachnodeInfo } from './arachnode-fret-adapter.js';
2
+
3
+ /**
4
+ * An Arachnode ring partition: the first `prefixBits` bits of a hashed coordinate must equal
5
+ * `prefixValue` for a key to fall inside it. Structurally identical to `ArachnodeInfo.partition`.
6
+ */
7
+ export interface RingPartition {
8
+ prefixBits: number;
9
+ prefixValue: number;
10
+ }
11
+
12
+ /**
13
+ * Extract the first `bits` bits of a hashed coordinate as an integer (MSB-first).
14
+ *
15
+ * The single source of truth for the block/peer prefix comparison Arachnode uses to decide
16
+ * responsibility. `RingSelector.calculatePartition` (peer side) and `RestorationCoordinator`
17
+ * (block side) both route through this, so a peer's advertised partition and a block's derived
18
+ * prefix are computed the same way — otherwise "this peer owns this block's slice" would silently
19
+ * stop meaning what it says.
20
+ */
21
+ export function extractPrefix(coord: Uint8Array, bits: number): number {
22
+ let value = 0;
23
+ for (let i = 0; i < bits; i++) {
24
+ const byteIndex = Math.floor(i / 8);
25
+ const bitIndex = 7 - (i % 8);
26
+ const bit = (coord[byteIndex]! >> bitIndex) & 1;
27
+ value = (value << 1) | bit;
28
+ }
29
+ return value;
30
+ }
31
+
32
+ /**
33
+ * Does `partition` cover the key at `coord`? Ring 0 (an undefined partition) covers the whole
34
+ * keyspace, so it always returns true.
35
+ */
36
+ export function partitionCovers(partition: RingPartition | undefined, coord: Uint8Array): boolean {
37
+ if (!partition) {
38
+ return true;
39
+ }
40
+ return extractPrefix(coord, partition.prefixBits) === partition.prefixValue;
41
+ }
42
+
43
+ /**
44
+ * Is the node described by `info` a currently-serving responsible holder for the key at `coord`?
45
+ *
46
+ * **Fail-toward-old-holder.** A node mid-move (`status === 'moving'`) advertises its *target*
47
+ * `partition`, but it keeps serving its *old* range (`moveFrom`) until it releases. So a moving
48
+ * node covers the key if EITHER its old range or its target range covers it — it is still counted
49
+ * as a serving holder for everything it served before the move until it transitions to `active` at
50
+ * the new ring. This is what keeps a shed key covered through Phases A–B and across a mid-handoff
51
+ * crash. See `docs/arachnode-ring-handoff.md` § Part 3.
52
+ */
53
+ export function isServingHolder(info: ArachnodeInfo, coord: Uint8Array): boolean {
54
+ if (info.status === 'moving' && info.moveFrom) {
55
+ return partitionCovers(info.moveFrom.partition, coord) || partitionCovers(info.partition, coord);
56
+ }
57
+ return partitionCovers(info.partition, coord);
58
+ }
59
+
60
+ /**
61
+ * Does the node described by `info` qualify toward *another* mover's Phase-B replication floor for
62
+ * the key at `coord`?
63
+ *
64
+ * A qualifying holder must still cover the key **after its own advertised move** — i.e. its
65
+ * *target* `partition` (the one it currently advertises) must cover it. A concurrent mover that is
66
+ * shedding the same sub-range advertises a target that does NOT cover the key, so it is excluded:
67
+ * two adjacent movers can never both count the other, so at most one reaches "confirmed" on the
68
+ * shared overlap and the other rolls back. For a non-moving (`active`) node, the advertised
69
+ * partition IS its serving range, so this reduces to plain partition coverage. See
70
+ * `docs/arachnode-ring-handoff.md` § Part 3 (concurrent moves).
71
+ */
72
+ export function qualifiesForFloor(info: ArachnodeInfo, coord: Uint8Array): boolean {
73
+ return partitionCovers(info.partition, coord);
74
+ }