@optimystic/db-p2p 1.0.0-beta.3 → 1.0.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 (234) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +73 -1
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +70 -3
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/block-transfer.d.ts +16 -1
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  7. package/dist/src/cluster/block-transfer.js +73 -72
  8. package/dist/src/cluster/block-transfer.js.map +1 -1
  9. package/dist/src/cluster/client.d.ts +4 -6
  10. package/dist/src/cluster/client.d.ts.map +1 -1
  11. package/dist/src/cluster/client.js +8 -13
  12. package/dist/src/cluster/client.js.map +1 -1
  13. package/dist/src/cluster/cluster-policy.d.ts +2 -1
  14. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  15. package/dist/src/cluster/cluster-policy.js.map +1 -1
  16. package/dist/src/cluster/cluster-repo.d.ts +126 -6
  17. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  18. package/dist/src/cluster/cluster-repo.js +378 -50
  19. package/dist/src/cluster/cluster-repo.js.map +1 -1
  20. package/dist/src/cluster/commit-proof.js +2 -2
  21. package/dist/src/cluster/race-resolution.d.ts +51 -7
  22. package/dist/src/cluster/race-resolution.d.ts.map +1 -1
  23. package/dist/src/cluster/race-resolution.js +71 -7
  24. package/dist/src/cluster/race-resolution.js.map +1 -1
  25. package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
  26. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  27. package/dist/src/cluster/rebalance-monitor.js +120 -26
  28. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  29. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  30. package/dist/src/cluster/spread-on-churn.js +34 -47
  31. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  32. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  33. package/dist/src/cohort-topic/host.js +8 -6
  34. package/dist/src/cohort-topic/host.js.map +1 -1
  35. package/dist/src/cohort-topic/membership-source.js +1 -1
  36. package/dist/src/cohort-topic/membership-source.js.map +1 -1
  37. package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
  38. package/dist/src/cohort-topic/peer-codec.js +5 -2
  39. package/dist/src/cohort-topic/peer-codec.js.map +1 -1
  40. package/dist/src/cohort-topic/stream-util.d.ts +24 -3
  41. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  42. package/dist/src/cohort-topic/stream-util.js +36 -3
  43. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  44. package/dist/src/cohort-topic/topic-router.d.ts +9 -0
  45. package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
  46. package/dist/src/cohort-topic/topic-router.js +12 -2
  47. package/dist/src/cohort-topic/topic-router.js.map +1 -1
  48. package/dist/src/dispute/client.d.ts.map +1 -1
  49. package/dist/src/dispute/client.js +15 -3
  50. package/dist/src/dispute/client.js.map +1 -1
  51. package/dist/src/index.d.ts +5 -0
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +5 -0
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/libp2p-key-network.d.ts +78 -17
  56. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  57. package/dist/src/libp2p-key-network.js +234 -147
  58. package/dist/src/libp2p-key-network.js.map +1 -1
  59. package/dist/src/libp2p-node-base.d.ts +35 -1
  60. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  61. package/dist/src/libp2p-node-base.js +151 -26
  62. package/dist/src/libp2p-node-base.js.map +1 -1
  63. package/dist/src/libp2p-node-rn.d.ts +2 -2
  64. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  65. package/dist/src/libp2p-node-rn.js.map +1 -1
  66. package/dist/src/libp2p-node.d.ts +2 -2
  67. package/dist/src/libp2p-node.d.ts.map +1 -1
  68. package/dist/src/libp2p-node.js.map +1 -1
  69. package/dist/src/logger.d.ts.map +1 -1
  70. package/dist/src/logger.js +6 -1
  71. package/dist/src/logger.js.map +1 -1
  72. package/dist/src/matchmaking/query-transport.d.ts +3 -4
  73. package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
  74. package/dist/src/matchmaking/query-transport.js +9 -9
  75. package/dist/src/matchmaking/query-transport.js.map +1 -1
  76. package/dist/src/network/network-manager-service.d.ts +10 -5
  77. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  78. package/dist/src/network/network-manager-service.js +11 -2
  79. package/dist/src/network/network-manager-service.js.map +1 -1
  80. package/dist/src/network/relay-reservation.d.ts +170 -0
  81. package/dist/src/network/relay-reservation.d.ts.map +1 -0
  82. package/dist/src/network/relay-reservation.js +527 -0
  83. package/dist/src/network/relay-reservation.js.map +1 -0
  84. package/dist/src/pend-validation.d.ts +3 -2
  85. package/dist/src/pend-validation.d.ts.map +1 -1
  86. package/dist/src/pend-validation.js +3 -2
  87. package/dist/src/pend-validation.js.map +1 -1
  88. package/dist/src/protocol-client.d.ts +8 -1
  89. package/dist/src/protocol-client.d.ts.map +1 -1
  90. package/dist/src/protocol-client.js +11 -0
  91. package/dist/src/protocol-client.js.map +1 -1
  92. package/dist/src/reactivity/forwarder-host.d.ts +2 -1
  93. package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
  94. package/dist/src/reactivity/forwarder-host.js +2 -1
  95. package/dist/src/reactivity/forwarder-host.js.map +1 -1
  96. package/dist/src/reactivity/origination-manager.d.ts +3 -3
  97. package/dist/src/reactivity/origination-manager.js +3 -3
  98. package/dist/src/reactivity/recover-transport.d.ts +22 -14
  99. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  100. package/dist/src/reactivity/recover-transport.js +32 -19
  101. package/dist/src/reactivity/recover-transport.js.map +1 -1
  102. package/dist/src/reactivity/subscription-manager.d.ts +2 -2
  103. package/dist/src/reactivity/topic-bytes.d.ts +6 -7
  104. package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
  105. package/dist/src/reactivity/topic-bytes.js +8 -9
  106. package/dist/src/reactivity/topic-bytes.js.map +1 -1
  107. package/dist/src/repo/client.d.ts +5 -2
  108. package/dist/src/repo/client.d.ts.map +1 -1
  109. package/dist/src/repo/client.js +49 -40
  110. package/dist/src/repo/client.js.map +1 -1
  111. package/dist/src/repo/cluster-coordinator.d.ts +77 -5
  112. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  113. package/dist/src/repo/cluster-coordinator.js +131 -20
  114. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  115. package/dist/src/repo/coordinator-repo.d.ts +238 -122
  116. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  117. package/dist/src/repo/coordinator-repo.js +625 -472
  118. package/dist/src/repo/coordinator-repo.js.map +1 -1
  119. package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
  120. package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
  121. package/dist/src/repo/i-under-replication-ledger.js +2 -0
  122. package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
  123. package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
  124. package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
  125. package/dist/src/repo/kv-under-replication-ledger.js +287 -0
  126. package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
  127. package/dist/src/repo/responsibility.d.ts +37 -0
  128. package/dist/src/repo/responsibility.d.ts.map +1 -0
  129. package/dist/src/repo/responsibility.js +37 -0
  130. package/dist/src/repo/responsibility.js.map +1 -0
  131. package/dist/src/repo/service.d.ts +44 -15
  132. package/dist/src/repo/service.d.ts.map +1 -1
  133. package/dist/src/repo/service.js +64 -27
  134. package/dist/src/repo/service.js.map +1 -1
  135. package/dist/src/repo/stuck-reservation.d.ts +127 -0
  136. package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
  137. package/dist/src/repo/stuck-reservation.js +179 -0
  138. package/dist/src/repo/stuck-reservation.js.map +1 -0
  139. package/dist/src/repo/under-replication-drain.d.ts +208 -0
  140. package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
  141. package/dist/src/repo/under-replication-drain.js +461 -0
  142. package/dist/src/repo/under-replication-drain.js.map +1 -0
  143. package/dist/src/rn.d.ts +5 -0
  144. package/dist/src/rn.d.ts.map +1 -1
  145. package/dist/src/rn.js +5 -0
  146. package/dist/src/rn.js.map +1 -1
  147. package/dist/src/storage/block-latch.d.ts +3 -2
  148. package/dist/src/storage/block-latch.d.ts.map +1 -1
  149. package/dist/src/storage/block-latch.js +7 -4
  150. package/dist/src/storage/block-latch.js.map +1 -1
  151. package/dist/src/storage/block-storage.d.ts +45 -3
  152. package/dist/src/storage/block-storage.d.ts.map +1 -1
  153. package/dist/src/storage/block-storage.js +156 -9
  154. package/dist/src/storage/block-storage.js.map +1 -1
  155. package/dist/src/storage/i-block-storage.d.ts +57 -5
  156. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  157. package/dist/src/storage/i-block-storage.js.map +1 -1
  158. package/dist/src/storage/pending-claim.d.ts +167 -0
  159. package/dist/src/storage/pending-claim.d.ts.map +1 -0
  160. package/dist/src/storage/pending-claim.js +156 -0
  161. package/dist/src/storage/pending-claim.js.map +1 -0
  162. package/dist/src/storage/raw-store-codec.d.ts +13 -0
  163. package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +24 -3
  165. package/dist/src/storage/raw-store-codec.js.map +1 -1
  166. package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
  167. package/dist/src/storage/restoration-coordinator.js +2 -1
  168. package/dist/src/storage/restoration-coordinator.js.map +1 -1
  169. package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
  170. package/dist/src/storage/ring-shift-coordinator.js +3 -3
  171. package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
  172. package/dist/src/storage/storage-repo.d.ts +122 -5
  173. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  174. package/dist/src/storage/storage-repo.js +245 -68
  175. package/dist/src/storage/storage-repo.js.map +1 -1
  176. package/dist/src/storage/struct.d.ts +58 -0
  177. package/dist/src/storage/struct.d.ts.map +1 -1
  178. package/dist/src/testing/mesh-harness.d.ts +77 -11
  179. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  180. package/dist/src/testing/mesh-harness.js +226 -100
  181. package/dist/src/testing/mesh-harness.js.map +1 -1
  182. package/package.json +2 -2
  183. package/readme.md +27 -3
  184. package/src/cluster/block-transfer-service.ts +132 -4
  185. package/src/cluster/block-transfer.ts +79 -84
  186. package/src/cluster/client.ts +115 -119
  187. package/src/cluster/cluster-policy.ts +2 -1
  188. package/src/cluster/cluster-repo.ts +422 -56
  189. package/src/cluster/commit-proof.ts +2 -2
  190. package/src/cluster/race-resolution.ts +76 -7
  191. package/src/cluster/rebalance-monitor.ts +677 -539
  192. package/src/cluster/spread-on-churn.ts +34 -54
  193. package/src/cohort-topic/host.ts +8 -6
  194. package/src/cohort-topic/membership-source.ts +1 -1
  195. package/src/cohort-topic/peer-codec.ts +34 -31
  196. package/src/cohort-topic/stream-util.ts +39 -4
  197. package/src/cohort-topic/topic-router.ts +12 -2
  198. package/src/dispute/client.ts +17 -7
  199. package/src/index.ts +64 -59
  200. package/src/libp2p-key-network.ts +1377 -1260
  201. package/src/libp2p-node-base.ts +186 -27
  202. package/src/libp2p-node-rn.ts +31 -30
  203. package/src/libp2p-node.ts +37 -36
  204. package/src/logger.ts +6 -1
  205. package/src/matchmaking/query-transport.ts +9 -9
  206. package/src/network/network-manager-service.ts +16 -5
  207. package/src/network/relay-reservation.ts +636 -0
  208. package/src/pend-validation.ts +3 -2
  209. package/src/protocol-client.ts +13 -1
  210. package/src/reactivity/forwarder-host.ts +2 -1
  211. package/src/reactivity/origination-manager.ts +3 -3
  212. package/src/reactivity/recover-transport.ts +39 -22
  213. package/src/reactivity/subscription-manager.ts +2 -2
  214. package/src/reactivity/topic-bytes.ts +35 -37
  215. package/src/repo/client.ts +172 -173
  216. package/src/repo/cluster-coordinator.ts +145 -19
  217. package/src/repo/coordinator-repo.ts +652 -505
  218. package/src/repo/i-under-replication-ledger.ts +89 -0
  219. package/src/repo/kv-under-replication-ledger.ts +302 -0
  220. package/src/repo/responsibility.ts +48 -0
  221. package/src/repo/service.ts +76 -39
  222. package/src/repo/stuck-reservation.ts +224 -0
  223. package/src/repo/under-replication-drain.ts +582 -0
  224. package/src/rn.ts +74 -69
  225. package/src/storage/block-latch.ts +7 -4
  226. package/src/storage/block-storage.ts +161 -10
  227. package/src/storage/i-block-storage.ts +60 -5
  228. package/src/storage/pending-claim.ts +200 -0
  229. package/src/storage/raw-store-codec.ts +26 -3
  230. package/src/storage/restoration-coordinator.ts +212 -211
  231. package/src/storage/ring-shift-coordinator.ts +271 -271
  232. package/src/storage/storage-repo.ts +280 -72
  233. package/src/storage/struct.ts +58 -0
  234. package/src/testing/mesh-harness.ts +293 -112
@@ -1,4 +1,4 @@
1
- import { blockIdsForTransforms, blockIdToBytes } from "@optimystic/db-core";
1
+ import { blockIdsForTransforms } from "@optimystic/db-core";
2
2
  import { ProtocolClient } from "../protocol-client.js";
3
3
  import { MAX_BLOCK_MESSAGE_BYTES } from "../protocol-limits.js";
4
4
  import { peerIdFromString } from "@libp2p/peer-id";
@@ -59,9 +59,28 @@ export class RepoClient extends ProtocolClient {
59
59
  // ResponseTimeoutError and mask the caller-facing 'RepoClient timeout' message.
60
60
  const deadlineController = new AbortController();
61
61
  const timer = setTimeout(() => deadlineController.abort(new Error('RepoClient timeout')), deadlineMs);
62
- const combinedSignal = options?.signal
63
- ? AbortSignal.any([options.signal, deadlineController.signal])
64
- : deadlineController.signal;
62
+ // Explicit combinator rather than the native `AbortSignal.any`, which Hermes
63
+ // (React Native's JS engine) does not provide. This has to forward whichever
64
+ // source signal's *reason* fired — that's how the caller-facing 'RepoClient
65
+ // timeout' message above (and any reason a caller's own `options.signal` carries)
66
+ // survives the combine. `any-signal` (the package libp2p itself uses for this) was
67
+ // tried and rejected here: it calls the composite `AbortController.abort()` with no
68
+ // argument, which discards the source reason and replaces it with a generic
69
+ // AbortError — confirmed against both Node's native `AbortSignal.any` and
70
+ // `any-signal@4.1.1` directly. Listeners are removed in the `finally` below so a
71
+ // long-lived caller `options.signal` never accumulates one per call.
72
+ const abortController = new AbortController();
73
+ const forwardAbort = (source) => () => abortController.abort(source.reason);
74
+ const onCallerAbort = options?.signal ? forwardAbort(options.signal) : undefined;
75
+ const onDeadlineAbort = forwardAbort(deadlineController.signal);
76
+ deadlineController.signal.addEventListener('abort', onDeadlineAbort, { once: true });
77
+ if (options?.signal) {
78
+ if (options.signal.aborted)
79
+ abortController.abort(options.signal.reason);
80
+ else
81
+ options.signal.addEventListener('abort', onCallerAbort, { once: true });
82
+ }
83
+ const combinedSignal = abortController.signal;
65
84
  let response;
66
85
  try {
67
86
  response = await super.processMessage(message, preferred, {
@@ -74,6 +93,9 @@ export class RepoClient extends ProtocolClient {
74
93
  }
75
94
  finally {
76
95
  clearTimeout(timer);
96
+ deadlineController.signal.removeEventListener('abort', onDeadlineAbort);
97
+ if (onCallerAbort)
98
+ options?.signal?.removeEventListener('abort', onCallerAbort);
77
99
  }
78
100
  // Type the redirect branch against the payload the service actually produces
79
101
  // (`RedirectPayload`) rather than reading it off the `any` response — the untyped read
@@ -94,50 +116,37 @@ export class RepoClient extends ProtocolClient {
94
116
  // notice we get that this peer matters, and if it is relay-only we have no address
95
117
  // for it at all. Merging after the dial would help only some later hop.
96
118
  this.peerNetwork.recordPeerAddresses?.(nextId, next.addrs ?? []);
97
- // cache hint await so the recorded key bytes match what findCoordinator
98
- // later looks up (blockIdToBytes is async); deterministic ordering also
99
- // keeps the hint testable. The redirect retry below is async anyway.
100
- await this.recordCoordinatorForOpsIfSupported(operations, nextId);
119
+ // Cache hint: a follow-up op on this block can dial the target directly.
120
+ const coordinated = this.coordinatedBlockId(operations);
121
+ if (coordinated)
122
+ this.recordCoordinatorHint(coordinated, nextId);
101
123
  // single-hop retry against target peer using repo protocol
102
124
  const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix);
103
125
  return await nextClient.processRepoMessage(operations, options, hop + 1);
104
126
  }
105
127
  return response;
106
128
  }
107
- async extractKeyFromOperations(ops) {
129
+ /**
130
+ * The block a redirected op is coordinated on — the block `RepoService.deriveBlockKey` redirected
131
+ * it by, and so the block its coordinator hint belongs to.
132
+ */
133
+ coordinatedBlockId(ops) {
108
134
  const op = ops[0];
109
- // The recorded key MUST be the sha256 digest produced by blockIdToBytes — the
110
- // exact bytes findCoordinator/recordCoordinator key the coordinator cache on
111
- // (NetworkTransactor passes blockIdToBytes(blockId)). Raw utf8 of the id would
112
- // never match, so the hint would silently never be retrieved.
113
- if ('get' in op) {
114
- const id = op.get.blockIds[0];
115
- return id ? await blockIdToBytes(id) : undefined;
116
- }
117
- if ('pend' in op) {
118
- // Key on a real block id the pend touches, NOT a structural transforms field
119
- // name ('inserts'/'updates'/'deletes'); see RepoService.deriveBlockKey.
120
- const id = blockIdsForTransforms(op.pend.transforms)[0];
121
- return id ? await blockIdToBytes(id) : undefined;
122
- }
123
- if ('commit' in op) {
124
- // Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
125
- // verifyResponsibility), NOT tailId — they differ for a non-tail batch.
126
- const id = op.commit.blockIds[0];
127
- return id ? await blockIdToBytes(id) : undefined;
128
- }
129
- if ('cancel' in op) {
130
- const id = op.cancel.actionRef.blockIds[0];
131
- return id ? await blockIdToBytes(id) : undefined;
132
- }
135
+ if (!op)
136
+ return undefined;
137
+ if ('get' in op)
138
+ return op.get.blockIds[0];
139
+ // Key on a real block id the pend touches, NOT a structural transforms field
140
+ // name ('inserts'/'updates'/'deletes'); see RepoService.deriveBlockKey.
141
+ if ('pend' in op)
142
+ return blockIdsForTransforms(op.pend.transforms)[0];
143
+ // Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
144
+ // verifyResponsibility), NOT tailId they differ for a non-tail batch.
145
+ if ('commit' in op)
146
+ return op.commit.blockIds[0];
147
+ if ('cancel' in op)
148
+ return op.cancel.actionRef.blockIds[0];
133
149
  return undefined;
134
150
  }
135
- async recordCoordinatorForOpsIfSupported(ops, peerId) {
136
- const keyBytes = await this.extractKeyFromOperations(ops);
137
- const pn = this.peerNetwork;
138
- if (keyBytes != null && typeof pn?.recordCoordinator === 'function') {
139
- pn.recordCoordinator(keyBytes, peerId);
140
- }
141
- }
142
151
  }
143
152
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/repo/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/repo/client.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,OAAO,UAAW,SAAQ,cAAc;IAC2B;IAAxE,YAAoB,MAAc,EAAE,WAAyB,EAAW,cAAuB;QAC9F,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAD4C,mBAAc,GAAd,cAAc,CAAS;IAE/F,CAAC;IAED,mCAAmC;IAC5B,MAAM,CAAC,MAAM,CAAC,MAAc,EAAE,WAAyB,EAAE,cAAuB;QACtF,OAAO,IAAI,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAoB,EAAE,OAAuB;QACtD,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EACpB,OAAO,CACP,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAoB,EAAE,OAAuB;QACvD,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EACnB,OAAO,CACP,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,SAAuB,EAAE,OAAuB;QAC5D,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,EAC3B,OAAO,CACP,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAAsB,EAAE,OAAuB;QAC3D,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EACrB,OAAO,CACP,CAAC;IACH,CAAC;IAEO,oBAAoB,CAAC,UAAqC;QACjE,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1B,IAAI,MAAM,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC1C,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC9C,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;QACxD,OAAO,SAAS,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAC/B,UAAqC,EACrC,OAAuB,EACvB,MAAc,CAAC;QAEf,MAAM,OAAO,GAAgB;YAC5B,UAAU;YACV,UAAU,EAAE,OAAO,CAAC,UAAU;SAC9B,CAAC;QACF,MAAM,aAAa,GAAG,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAA;QAC5D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QACrD,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,SAAS,CAAC,GAAG,aAAa,CAAA;QAEpE,2EAA2E;QAC3E,6EAA6E;QAC7E,4EAA4E;QAC5E,+EAA+E;QAC/E,yEAAyE;QACzE,gFAAgF;QAChF,4EAA4E;QAC5E,qEAAqE;QACrE,EAAE;QACF,+EAA+E;QAC/E,+DAA+D;QAC/D,gFAAgF;QAChF,MAAM,kBAAkB,GAAG,IAAI,eAAe,EAAE,CAAA;QAChD,MAAM,KAAK,GAAG,UAAU,CACvB,GAAG,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,EAC/D,UAAU,CACV,CAAA;QACD,6EAA6E;QAC7E,6EAA6E;QAC7E,4EAA4E;QAC5E,kFAAkF;QAClF,mFAAmF;QACnF,oFAAoF;QACpF,4EAA4E;QAC5E,0EAA0E;QAC1E,iFAAiF;QACjF,qEAAqE;QACrE,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA;QAC7C,MAAM,YAAY,GAAG,CAAC,MAAmB,EAAE,EAAE,CAAC,GAAS,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC9F,MAAM,aAAa,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAChF,MAAM,eAAe,GAAG,YAAY,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;QAC/D,kBAAkB,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACpF,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO;gBAAE,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;;gBACnE,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,aAAc,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QAC9E,CAAC;QACD,MAAM,cAAc,GAAG,eAAe,CAAC,MAAM,CAAA;QAC7C,IAAI,QAAa,CAAA;QACjB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,CAAM,OAAO,EAAE,SAAS,EAAE;gBAC9D,MAAM,EAAE,cAAc;gBACtB,aAAa;gBACb,aAAa,EAAE,OAAO,EAAE,aAAa;gBACrC,+DAA+D;gBAC/D,aAAa,EAAE,uBAAuB;aACtC,CAAC,CAAA;QACH,CAAC;gBAAS,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,kBAAkB,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;YACvE,IAAI,aAAa;gBAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAA;QAChF,CAAC;QAED,6EAA6E;QAC7E,uFAAuF;QACvF,iEAAiE;QACjE,MAAM,gBAAgB,GAAG,QAAoC,CAAA;QAC7D,IAAI,gBAAgB,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YAC/C,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;YAC3E,CAAC;YACD,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAA;YAC3C,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,KAAK,CAAA;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,YAAY,CAAC,IAAI,KAAK,CAAC,CAAC,CAAE,CAAA;YAClE,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YACxC,IAAI,IAAI,CAAC,EAAE,KAAK,YAAY,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;YACpE,CAAC;YACD,oFAAoF;YACpF,mFAAmF;YACnF,wEAAwE;YACxE,IAAI,CAAC,WAAW,CAAC,mBAAmB,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;YAChE,yEAAyE;YACzE,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAA;YACvD,IAAI,WAAW;gBAAE,IAAI,CAAC,qBAAqB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;YAChE,2DAA2D;YAC3D,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;YACnF,OAAO,MAAM,UAAU,CAAC,kBAAkB,CAAI,UAAU,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;QAC5E,CAAC;QACD,OAAO,QAAa,CAAC;IACtB,CAAC;IAED;;;OAGG;IACK,kBAAkB,CAAC,GAA8B;QACxD,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1B,IAAI,KAAK,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3C,6EAA6E;QAC7E,wEAAwE;QACxE,IAAI,MAAM,IAAI,EAAE;YAAE,OAAO,qBAAqB,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,uEAAuE;QACvE,wEAAwE;QACxE,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACjD,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3D,OAAO,SAAS,CAAC;IAClB,CAAC;CAED"}
@@ -43,6 +43,29 @@ export declare class ConflictRaceLostError extends Error {
43
43
  /** peerId → messageHash of the rival transaction that member holds as the race winner. */
44
44
  conflicts: Record<string, string>);
45
45
  }
46
+ /**
47
+ * The transaction's pend could not proceed because one or more members answered with a signed `held`
48
+ * vote: the requested blocks are reserved by a different unresolved action in that member's durable
49
+ * storage. Sibling of {@link ConflictRaceLostError} and retryable for the same reason — nobody judged
50
+ * this write invalid; it queued behind a reservation that disappears when the holder commits or
51
+ * cancels.
52
+ *
53
+ * The two are separate because they name different things. A conflict vote names the winning rival's
54
+ * `messageHash`, which the member holds whole; a held vote can only name the rival's **action id**,
55
+ * because it fires in the window where the rival has left the member's in-memory table but not yet its
56
+ * storage. `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the
57
+ * normal retry machinery (`isConflictFailure`) absorbs it, exactly as it does a lost race.
58
+ *
59
+ * Only a PEND record can produce it: `held` votes come from `ClusterMember.validatePendOperations`,
60
+ * which inspects pend operations only, so `CoordinatorRepo.commit` never meets one.
61
+ */
62
+ export declare class BlocksHeldError extends Error {
63
+ /** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
64
+ readonly heldBy: Record<string, string>;
65
+ constructor(message: string,
66
+ /** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
67
+ heldBy: Record<string, string>);
68
+ }
46
69
  /** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
47
70
  export type TimerCancel = () => void;
48
71
  /**
@@ -57,6 +80,19 @@ export interface ClusterCoordinatorClock {
57
80
  setTimer?: (fn: () => void, delayMs: number) => TimerCancel;
58
81
  }
59
82
  /** Manages distributed transactions across clusters */
83
+ /**
84
+ * What a cohort lookup established about a block's cohort. `resolved: false` covers BOTH a lookup
85
+ * that threw and one that answered with nobody: neither names a destination for a write, and the
86
+ * durability class both produce is the same (`unrouted`). `reason` is for logs only — never branch
87
+ * on it.
88
+ */
89
+ export type CohortResolution = {
90
+ readonly resolved: true;
91
+ readonly peerIds: readonly string[];
92
+ } | {
93
+ readonly resolved: false;
94
+ readonly reason: string;
95
+ };
60
96
  export declare class ClusterCoordinator {
61
97
  private readonly keyNetwork;
62
98
  /** Factory for a per-peer cluster RPC handle; only `update` is ever called, hence `ICluster`. */
@@ -116,9 +152,45 @@ export declare class ClusterCoordinator {
116
152
  */
117
153
  private createMessageHash;
118
154
  /**
119
- * Gets all peers in the cluster for a specific block ID
155
+ * The ONE cohort lookup every accessor on this class derives from: the raw peer map when the key
156
+ * network answered, otherwise the reason it did not. A thrown `findCluster` is logged here and
157
+ * nowhere else. Callers that need the map (`executeClusterTransaction`, which builds the record's
158
+ * `peers`) go through {@link getClusterForBlock}; callers that need to know whether the cohort
159
+ * RESOLVED go through {@link resolveCohort}.
160
+ */
161
+ private lookupCluster;
162
+ /**
163
+ * Gets all peers in the cluster for a specific block ID. Empty when the lookup failed — the
164
+ * consensus path treats "no cohort" and "lookup failed" alike (there is nobody to run consensus
165
+ * with either way); a caller that must tell them apart uses {@link resolveCohort}.
120
166
  */
121
167
  private getClusterForBlock;
168
+ /**
169
+ * Whether the block's cohort could be established, and who it is. The primitive behind
170
+ * {@link getClusterPeerIds} and {@link getClusterSize}: a lookup that threw and a lookup that named
171
+ * nobody used to reach every caller as the same empty list, and `CoordinatorRepo`'s solo
172
+ * short-circuit then acknowledged a write it had no idea where to send exactly as it acknowledged a
173
+ * write to a genuine cohort of one (GitHub #19). Both shapes are still `resolved: false` here —
174
+ * neither names a destination — but they are distinguishable from a resolved cohort, which is what
175
+ * the write's durability class needs (`unrouted` vs `local`).
176
+ */
177
+ resolveCohort(blockId: BlockId): Promise<CohortResolution>;
178
+ /**
179
+ * A node never runs a cluster transaction for a cohort it is not in. Behind members reconcile from the
180
+ * coordinator's own proof-carrying copy (its member applies before the merged record fans out), and a
181
+ * coordinator outside `record.peers` is not a reconcile target — so a cohort with no holder would stay
182
+ * behind and the commit durability gate would refuse, having first put this node's vote and storage
183
+ * where the cohort does not look. The invariant is held here, at the one place a record's `peers` is
184
+ * chosen, rather than left to the routing convention.
185
+ *
186
+ * Fires only on a RESOLVED cohort (at least one peer) that excludes the wired local member. An empty
187
+ * cohort is a failed lookup, not a cohort this node is outside of, so it is left to `executeTransaction`'s
188
+ * size checks; `CoordinatorRepo`'s solo short-circuit keeps unresolved and single-peer cohorts away from
189
+ * this method altogether in any case. After its responsibility check, what remains is a multi-member
190
+ * cohort that changed inside the responsibility cache's staleness window. With no local member wired the guard does not apply: that
191
+ * bypass exists for wiring without an identity (direct constructors, some tests), never for production.
192
+ */
193
+ private assertLocalMemberInCohort;
122
194
  private makeRecord;
123
195
  /**
124
196
  * Initiates a 2-phase transaction for a specific block ID.
@@ -181,10 +253,10 @@ export declare class ClusterCoordinator {
181
253
  */
182
254
  private executeTransaction;
183
255
  /**
184
- * The block's cohort peer ids as currently derivable. Empty when `findCluster` fails
185
- * (getClusterForBlock swallows the throw), so a caller branching on `length <= 1` is also taking
186
- * the degraded-routing branch; `CoordinatorRepo.commit` uses the ids to log whether a solo cohort
187
- * is genuinely just self or a routing failure.
256
+ * The block's cohort peer ids as currently derivable. Empty when the cohort did not resolve
257
+ * ({@link resolveCohort}: `findCluster` threw, or named nobody), so a caller branching on
258
+ * `length <= 1` is also taking the degraded-routing branch. Derived from `resolveCohort` rather
259
+ * than re-deriving the cohort, so there is exactly one lookup rule.
188
260
  */
189
261
  getClusterPeerIds(blockId: BlockId): Promise<string[]>;
190
262
  /** {@link getClusterPeerIds}, counted. Derived from it rather than re-deriving the cohort, so the
@@ -1 +1 @@
1
- {"version":3,"file":"cluster-coordinator.d.ts","sourceRoot":"","sources":["../../../src/repo/cluster-coordinator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cluster-coordinator.d.ts","sourceRoot":"","sources":["../../../src/repo/cluster-coordinator.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,9 @@
1
1
  import { peerIdFromString } from "@libp2p/peer-id";
2
- import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest } from "@optimystic/db-core";
2
+ import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest, routingKeyForBlock } from "@optimystic/db-core";
3
3
  import { Pending } from "@optimystic/db-core";
4
4
  import { createLogger, verbose } from '../logger.js';
5
5
  import { PenaltyReason } from "../reputation/types.js";
6
+ import { ResponsibilityRefusalError } from "./responsibility.js";
6
7
  const log = createLogger('cluster');
7
8
  /**
8
9
  * Pick each peer's OWN {@link ClusterRecord.applyOutcomes} entry out of the record that peer answered
@@ -79,6 +80,32 @@ export class ConflictRaceLostError extends Error {
79
80
  this.name = 'ConflictRaceLostError';
80
81
  }
81
82
  }
83
+ /**
84
+ * The transaction's pend could not proceed because one or more members answered with a signed `held`
85
+ * vote: the requested blocks are reserved by a different unresolved action in that member's durable
86
+ * storage. Sibling of {@link ConflictRaceLostError} and retryable for the same reason — nobody judged
87
+ * this write invalid; it queued behind a reservation that disappears when the holder commits or
88
+ * cancels.
89
+ *
90
+ * The two are separate because they name different things. A conflict vote names the winning rival's
91
+ * `messageHash`, which the member holds whole; a held vote can only name the rival's **action id**,
92
+ * because it fires in the window where the rival has left the member's in-memory table but not yet its
93
+ * storage. `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the
94
+ * normal retry machinery (`isConflictFailure`) absorbs it, exactly as it does a lost race.
95
+ *
96
+ * Only a PEND record can produce it: `held` votes come from `ClusterMember.validatePendOperations`,
97
+ * which inspects pend operations only, so `CoordinatorRepo.commit` never meets one.
98
+ */
99
+ export class BlocksHeldError extends Error {
100
+ heldBy;
101
+ constructor(message,
102
+ /** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
103
+ heldBy) {
104
+ super(message);
105
+ this.heldBy = heldBy;
106
+ this.name = 'BlocksHeldError';
107
+ }
108
+ }
82
109
  /**
83
110
  * Production timer binding: a one-shot `setTimeout` whose handle is **unref'd** so a pending
84
111
  * commit-retry (or the deferred transaction cleanup) never keeps an otherwise-idle process alive.
@@ -91,7 +118,6 @@ function defaultSetTimer(fn, delayMs) {
91
118
  handle.unref?.();
92
119
  return () => clearTimeout(handle);
93
120
  }
94
- /** Manages distributed transactions across clusters */
95
121
  export class ClusterCoordinator {
96
122
  keyNetwork;
97
123
  createClusterClient;
@@ -183,21 +209,76 @@ export class ClusterCoordinator {
183
209
  return computeClusterMessageHash(message, membershipDigestValue);
184
210
  }
185
211
  /**
186
- * Gets all peers in the cluster for a specific block ID
212
+ * The ONE cohort lookup every accessor on this class derives from: the raw peer map when the key
213
+ * network answered, otherwise the reason it did not. A thrown `findCluster` is logged here and
214
+ * nowhere else. Callers that need the map (`executeClusterTransaction`, which builds the record's
215
+ * `peers`) go through {@link getClusterForBlock}; callers that need to know whether the cohort
216
+ * RESOLVED go through {@link resolveCohort}.
187
217
  */
188
- async getClusterForBlock(blockId) {
189
- const blockIdBytes = new TextEncoder().encode(blockId);
218
+ async lookupCluster(blockId) {
190
219
  try {
191
- const peers = await this.keyNetwork.findCluster(blockIdBytes);
220
+ const peers = await this.keyNetwork.findCluster(routingKeyForBlock(blockId));
192
221
  const peerIds = Object.keys(peers ?? {});
193
222
  log('cluster-tx:cluster-members', { blockId, peerIds });
194
- return peers;
223
+ return { peers: peers ?? {} };
195
224
  }
196
225
  catch (e) {
197
226
  log('WARN findCluster failed for %s: %o', blockId, e);
198
- return {};
227
+ return { reason: `findCluster threw: ${e?.message ?? String(e)}` };
199
228
  }
200
229
  }
230
+ /**
231
+ * Gets all peers in the cluster for a specific block ID. Empty when the lookup failed — the
232
+ * consensus path treats "no cohort" and "lookup failed" alike (there is nobody to run consensus
233
+ * with either way); a caller that must tell them apart uses {@link resolveCohort}.
234
+ */
235
+ async getClusterForBlock(blockId) {
236
+ const outcome = await this.lookupCluster(blockId);
237
+ return 'peers' in outcome ? outcome.peers : {};
238
+ }
239
+ /**
240
+ * Whether the block's cohort could be established, and who it is. The primitive behind
241
+ * {@link getClusterPeerIds} and {@link getClusterSize}: a lookup that threw and a lookup that named
242
+ * nobody used to reach every caller as the same empty list, and `CoordinatorRepo`'s solo
243
+ * short-circuit then acknowledged a write it had no idea where to send exactly as it acknowledged a
244
+ * write to a genuine cohort of one (GitHub #19). Both shapes are still `resolved: false` here —
245
+ * neither names a destination — but they are distinguishable from a resolved cohort, which is what
246
+ * the write's durability class needs (`unrouted` vs `local`).
247
+ */
248
+ async resolveCohort(blockId) {
249
+ const outcome = await this.lookupCluster(blockId);
250
+ if ('reason' in outcome)
251
+ return { resolved: false, reason: outcome.reason };
252
+ const peerIds = Object.keys(outcome.peers);
253
+ if (peerIds.length === 0)
254
+ return { resolved: false, reason: 'findCluster named nobody' };
255
+ return { resolved: true, peerIds };
256
+ }
257
+ /**
258
+ * A node never runs a cluster transaction for a cohort it is not in. Behind members reconcile from the
259
+ * coordinator's own proof-carrying copy (its member applies before the merged record fans out), and a
260
+ * coordinator outside `record.peers` is not a reconcile target — so a cohort with no holder would stay
261
+ * behind and the commit durability gate would refuse, having first put this node's vote and storage
262
+ * where the cohort does not look. The invariant is held here, at the one place a record's `peers` is
263
+ * chosen, rather than left to the routing convention.
264
+ *
265
+ * Fires only on a RESOLVED cohort (at least one peer) that excludes the wired local member. An empty
266
+ * cohort is a failed lookup, not a cohort this node is outside of, so it is left to `executeTransaction`'s
267
+ * size checks; `CoordinatorRepo`'s solo short-circuit keeps unresolved and single-peer cohorts away from
268
+ * this method altogether in any case. After its responsibility check, what remains is a multi-member
269
+ * cohort that changed inside the responsibility cache's staleness window. With no local member wired the guard does not apply: that
270
+ * bypass exists for wiring without an identity (direct constructors, some tests), never for production.
271
+ */
272
+ assertLocalMemberInCohort(blockId, peers) {
273
+ if (!this.localCluster)
274
+ return;
275
+ const peerIds = Object.keys(peers);
276
+ const selfId = this.localCluster.peerId.toString();
277
+ if (peerIds.length === 0 || peerIds.includes(selfId))
278
+ return;
279
+ log('cluster-tx:not-in-cohort', { blockId, selfId, peerIds });
280
+ throw new ResponsibilityRefusalError('not-responsible', [blockId], `refusing to coordinate a cluster transaction for a cohort this node is not in: ${peerIds.join(', ')}`);
281
+ }
201
282
  makeRecord(peers, messageHash, message, membershipDigestValue) {
202
283
  const peerCount = Object.keys(peers ?? {}).length;
203
284
  const record = {
@@ -253,6 +334,7 @@ export class ClusterCoordinator {
253
334
  : { ...message, coordinatingBlockIds: [blockId] };
254
335
  // Get the cluster peers for this block
255
336
  const peers = await this.getClusterForBlock(blockId);
337
+ this.assertLocalMemberInCohort(blockId, peers);
256
338
  // Bind the responsible membership into the transaction identity (v2): the digest is folded into
257
339
  // the messageHash below, so two different peer sets produce two different messageHashes rather
258
340
  // than one hash with a silent internal disagreement about who is responsible.
@@ -382,17 +464,23 @@ export class ClusterCoordinator {
382
464
  // Collect promises with super-majority requirement
383
465
  const promised = await this.collectPromises(peers, record);
384
466
  const superMajority = Math.ceil(peerCount * this.cfg.superMajorityThreshold);
385
- // Count approvals, rejections and conflict votes separately. A `conflict` vote is a member
386
- // saying "not now — I hold the race winner": it must count toward NEITHER approvals NOR
387
- // rejections, or a lost race would masquerade as a validator rejection (permanent) or as
388
- // silence (indistinguishable from an unreachable cohort) both wrong.
467
+ // Count approvals, rejections and the two RETRYABLE refusals separately. A `conflict` vote is a
468
+ // member saying "not now — I hold the race winner"; a `held` vote is a member saying "not now —
469
+ // a different unresolved action holds these blocks in my storage". Neither may count toward
470
+ // approvals OR rejections, or a transient refusal would masquerade as a validator rejection
471
+ // (permanent) or as silence (indistinguishable from an unreachable cohort) — both wrong.
389
472
  const promises = promised.record.promises;
390
473
  const approvalCount = Object.values(promises).filter(sig => sig.type === 'approve').length;
391
474
  const rejectionCount = Object.values(promises).filter(sig => sig.type === 'reject').length;
392
475
  const conflictCount = Object.values(promises).filter(sig => sig.type === 'conflict').length;
476
+ const heldCount = Object.values(promises).filter(sig => sig.type === 'held').length;
393
477
  // Check if rejections make super-majority impossible
394
478
  // If more than (peerCount - superMajority) nodes reject, we can never reach super-majority
395
479
  const maxAllowedRejections = peerCount - superMajority;
480
+ // Whether the merged record itself PROVES super-majority unreachable — the same sum a member
481
+ // re-derives as `ConflictSuperseded`/`Rejected` from the signed votes, which is what makes an
482
+ // abandonment broadcast proof-carrying rather than an unauthenticated "forget this".
483
+ const refusalsProveUnreachable = rejectionCount + conflictCount + heldCount > maxAllowedRejections;
396
484
  if (rejectionCount > maxAllowedRejections) {
397
485
  const rejectReasonsByPeer = Object.fromEntries(Object.entries(promises)
398
486
  .flatMap(([peerId, sig]) => sig.type === 'reject' ? [[peerId, sig.rejectReason ?? 'unknown']] : []));
@@ -436,11 +524,33 @@ export class ClusterCoordinator {
436
524
  // super-majority (members re-derive ConflictSuperseded/Rejected from the signed votes and
437
525
  // clear their reservations immediately). Below that bar the record proves nothing and a
438
526
  // broadcast would be the unauthenticated "forget this" the shortfall NOTE below refuses.
439
- if (rejectionCount + conflictCount > maxAllowedRejections) {
527
+ if (refusalsProveUnreachable) {
440
528
  this.broadcastAbandonment(promised.record, 'conflict-race-lost');
441
529
  }
442
530
  throw new ConflictRaceLostError(`Conflict race lost: ${conflictCount}/${peerCount} member(s) hold a conflicting winner (${approvalCount}/${superMajority} approvals)`, conflicts);
443
531
  }
532
+ // A `held`-answered shortfall is the OTHER retryable refusal: the pend queued behind a rival's
533
+ // unresolved reservation. Checked after the conflict branch so a lost race still wins when both
534
+ // answer — a conflict vote names the winning transaction's messageHash, which is strictly more
535
+ // actionable than an action id — and, like it, before the generic shortfall, which must stay
536
+ // reserved for the genuinely-silent cohort.
537
+ if (heldCount > 0 && approvalCount < superMajority) {
538
+ const heldBy = Object.fromEntries(Object.entries(promises)
539
+ .flatMap(([peerId, sig]) => sig.type === 'held' ? [[peerId, sig.heldBy]] : []));
540
+ log('cluster-tx:pend-blocks-held', {
541
+ messageHash: record.messageHash,
542
+ peerCount,
543
+ approvals: approvalCount,
544
+ rejections: rejectionCount,
545
+ heldBy,
546
+ superMajority
547
+ });
548
+ this.updateTransactionRecord(promised.record, 'pend-blocks-held');
549
+ if (refusalsProveUnreachable) {
550
+ this.broadcastAbandonment(promised.record, 'pend-blocks-held');
551
+ }
552
+ throw new BlocksHeldError(`Pend blocks held: ${heldCount}/${peerCount} member(s) hold an unresolved rival action (${approvalCount}/${superMajority} approvals)`, heldBy);
553
+ }
444
554
  if (peerCount > 1 && approvalCount < superMajority) {
445
555
  log('cluster-tx:supermajority-failed', {
446
556
  messageHash: record.messageHash,
@@ -459,7 +569,8 @@ export class ClusterCoordinator {
459
569
  // their own staleness sweep instead.
460
570
  // NOTE: the message below is load-bearing wire text — the consuming repo
461
571
  // (sereus cadre-core control-write-retry) matches it verbatim to retry a genuinely-silent
462
- // cohort. Keep it byte-identical, and never fold conflict votes into its rejection count.
572
+ // cohort. Keep it byte-identical, and never fold `conflict` or `held` votes into its
573
+ // rejection count.
463
574
  throw new Error(`Failed to get super-majority: ${approvalCount}/${peerCount} approvals (needed ${superMajority}, ${rejectionCount} rejections)`);
464
575
  }
465
576
  // Mark as disputed when minority rejections exist but super-majority approves
@@ -492,14 +603,14 @@ export class ClusterCoordinator {
492
603
  return await this.commitTransaction(promised.record);
493
604
  }
494
605
  /**
495
- * The block's cohort peer ids as currently derivable. Empty when `findCluster` fails
496
- * (getClusterForBlock swallows the throw), so a caller branching on `length <= 1` is also taking
497
- * the degraded-routing branch; `CoordinatorRepo.commit` uses the ids to log whether a solo cohort
498
- * is genuinely just self or a routing failure.
606
+ * The block's cohort peer ids as currently derivable. Empty when the cohort did not resolve
607
+ * ({@link resolveCohort}: `findCluster` threw, or named nobody), so a caller branching on
608
+ * `length <= 1` is also taking the degraded-routing branch. Derived from `resolveCohort` rather
609
+ * than re-deriving the cohort, so there is exactly one lookup rule.
499
610
  */
500
611
  async getClusterPeerIds(blockId) {
501
- const peers = await this.getClusterForBlock(blockId);
502
- return Object.keys(peers ?? {});
612
+ const cohort = await this.resolveCohort(blockId);
613
+ return cohort.resolved ? [...cohort.peerIds] : [];
503
614
  }
504
615
  /** {@link getClusterPeerIds}, counted. Derived from it rather than re-deriving the cohort, so the
505
616
  * size a caller branches on and the ids it logs can never come from two different rules. */