@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.
- package/dist/src/cluster/block-transfer-service.d.ts +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +126 -6
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +378 -50
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +234 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +6 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +77 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +131 -20
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +238 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +625 -472
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +122 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +245 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +77 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +226 -100
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +422 -56
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1260
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +6 -1
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +145 -19
- package/src/repo/coordinator-repo.ts +652 -505
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +280 -72
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +293 -112
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
2
2
|
import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig, ICluster, PendResult, CommitResult, StaleFailure } from "@optimystic/db-core";
|
|
3
|
-
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest } from "@optimystic/db-core";
|
|
3
|
+
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest, routingKeyForBlock } from "@optimystic/db-core";
|
|
4
4
|
import { Pending } from "@optimystic/db-core";
|
|
5
5
|
import type { PeerId } from "@libp2p/interface";
|
|
6
6
|
import { createLogger, verbose } from '../logger.js'
|
|
@@ -9,6 +9,7 @@ import type { FretService } from "p2p-fret";
|
|
|
9
9
|
import type { IPeerReputation } from "../reputation/types.js";
|
|
10
10
|
import { PenaltyReason } from "../reputation/types.js";
|
|
11
11
|
import type { ITransactionStateStore } from "../cluster/i-transaction-state-store.js";
|
|
12
|
+
import { ResponsibilityRefusalError } from "./responsibility.js";
|
|
12
13
|
|
|
13
14
|
const log = createLogger('cluster')
|
|
14
15
|
|
|
@@ -91,6 +92,33 @@ export class ConflictRaceLostError extends Error {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The transaction's pend could not proceed because one or more members answered with a signed `held`
|
|
97
|
+
* vote: the requested blocks are reserved by a different unresolved action in that member's durable
|
|
98
|
+
* storage. Sibling of {@link ConflictRaceLostError} and retryable for the same reason — nobody judged
|
|
99
|
+
* this write invalid; it queued behind a reservation that disappears when the holder commits or
|
|
100
|
+
* cancels.
|
|
101
|
+
*
|
|
102
|
+
* The two are separate because they name different things. A conflict vote names the winning rival's
|
|
103
|
+
* `messageHash`, which the member holds whole; a held vote can only name the rival's **action id**,
|
|
104
|
+
* because it fires in the window where the rival has left the member's in-memory table but not yet its
|
|
105
|
+
* storage. `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the
|
|
106
|
+
* normal retry machinery (`isConflictFailure`) absorbs it, exactly as it does a lost race.
|
|
107
|
+
*
|
|
108
|
+
* Only a PEND record can produce it: `held` votes come from `ClusterMember.validatePendOperations`,
|
|
109
|
+
* which inspects pend operations only, so `CoordinatorRepo.commit` never meets one.
|
|
110
|
+
*/
|
|
111
|
+
export class BlocksHeldError extends Error {
|
|
112
|
+
constructor(
|
|
113
|
+
message: string,
|
|
114
|
+
/** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
|
|
115
|
+
readonly heldBy: Record<string, string>
|
|
116
|
+
) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = 'BlocksHeldError';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
/** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
95
123
|
export type TimerCancel = () => void;
|
|
96
124
|
|
|
@@ -140,6 +168,16 @@ interface ClusterTransactionState {
|
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
/** Manages distributed transactions across clusters */
|
|
171
|
+
/**
|
|
172
|
+
* What a cohort lookup established about a block's cohort. `resolved: false` covers BOTH a lookup
|
|
173
|
+
* that threw and one that answered with nobody: neither names a destination for a write, and the
|
|
174
|
+
* durability class both produce is the same (`unrouted`). `reason` is for logs only — never branch
|
|
175
|
+
* on it.
|
|
176
|
+
*/
|
|
177
|
+
export type CohortResolution =
|
|
178
|
+
| { readonly resolved: true; readonly peerIds: readonly string[] }
|
|
179
|
+
| { readonly resolved: false; readonly reason: string };
|
|
180
|
+
|
|
143
181
|
export class ClusterCoordinator {
|
|
144
182
|
private transactions: Map<string, ClusterTransactionState> = new Map();
|
|
145
183
|
private readonly retryInitialIntervalMs: number;
|
|
@@ -236,21 +274,76 @@ export class ClusterCoordinator {
|
|
|
236
274
|
}
|
|
237
275
|
|
|
238
276
|
/**
|
|
239
|
-
*
|
|
277
|
+
* The ONE cohort lookup every accessor on this class derives from: the raw peer map when the key
|
|
278
|
+
* network answered, otherwise the reason it did not. A thrown `findCluster` is logged here and
|
|
279
|
+
* nowhere else. Callers that need the map (`executeClusterTransaction`, which builds the record's
|
|
280
|
+
* `peers`) go through {@link getClusterForBlock}; callers that need to know whether the cohort
|
|
281
|
+
* RESOLVED go through {@link resolveCohort}.
|
|
240
282
|
*/
|
|
241
|
-
private async
|
|
242
|
-
const blockIdBytes = new TextEncoder().encode(blockId);
|
|
283
|
+
private async lookupCluster(blockId: BlockId): Promise<{ peers: ClusterPeers } | { reason: string }> {
|
|
243
284
|
try {
|
|
244
|
-
const peers = await this.keyNetwork.findCluster(
|
|
285
|
+
const peers = await this.keyNetwork.findCluster(routingKeyForBlock(blockId));
|
|
245
286
|
const peerIds = Object.keys(peers ?? {});
|
|
246
287
|
log('cluster-tx:cluster-members', { blockId, peerIds });
|
|
247
|
-
return peers;
|
|
288
|
+
return { peers: peers ?? {} };
|
|
248
289
|
} catch (e) {
|
|
249
290
|
log('WARN findCluster failed for %s: %o', blockId, e)
|
|
250
|
-
return {
|
|
291
|
+
return { reason: `findCluster threw: ${(e as Error)?.message ?? String(e)}` };
|
|
251
292
|
}
|
|
252
293
|
}
|
|
253
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Gets all peers in the cluster for a specific block ID. Empty when the lookup failed — the
|
|
297
|
+
* consensus path treats "no cohort" and "lookup failed" alike (there is nobody to run consensus
|
|
298
|
+
* with either way); a caller that must tell them apart uses {@link resolveCohort}.
|
|
299
|
+
*/
|
|
300
|
+
private async getClusterForBlock(blockId: BlockId): Promise<ClusterPeers> {
|
|
301
|
+
const outcome = await this.lookupCluster(blockId);
|
|
302
|
+
return 'peers' in outcome ? outcome.peers : {};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Whether the block's cohort could be established, and who it is. The primitive behind
|
|
307
|
+
* {@link getClusterPeerIds} and {@link getClusterSize}: a lookup that threw and a lookup that named
|
|
308
|
+
* nobody used to reach every caller as the same empty list, and `CoordinatorRepo`'s solo
|
|
309
|
+
* short-circuit then acknowledged a write it had no idea where to send exactly as it acknowledged a
|
|
310
|
+
* write to a genuine cohort of one (GitHub #19). Both shapes are still `resolved: false` here —
|
|
311
|
+
* neither names a destination — but they are distinguishable from a resolved cohort, which is what
|
|
312
|
+
* the write's durability class needs (`unrouted` vs `local`).
|
|
313
|
+
*/
|
|
314
|
+
async resolveCohort(blockId: BlockId): Promise<CohortResolution> {
|
|
315
|
+
const outcome = await this.lookupCluster(blockId);
|
|
316
|
+
if ('reason' in outcome) return { resolved: false, reason: outcome.reason };
|
|
317
|
+
const peerIds = Object.keys(outcome.peers);
|
|
318
|
+
if (peerIds.length === 0) return { resolved: false, reason: 'findCluster named nobody' };
|
|
319
|
+
return { resolved: true, peerIds };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* A node never runs a cluster transaction for a cohort it is not in. Behind members reconcile from the
|
|
324
|
+
* coordinator's own proof-carrying copy (its member applies before the merged record fans out), and a
|
|
325
|
+
* coordinator outside `record.peers` is not a reconcile target — so a cohort with no holder would stay
|
|
326
|
+
* behind and the commit durability gate would refuse, having first put this node's vote and storage
|
|
327
|
+
* where the cohort does not look. The invariant is held here, at the one place a record's `peers` is
|
|
328
|
+
* chosen, rather than left to the routing convention.
|
|
329
|
+
*
|
|
330
|
+
* Fires only on a RESOLVED cohort (at least one peer) that excludes the wired local member. An empty
|
|
331
|
+
* cohort is a failed lookup, not a cohort this node is outside of, so it is left to `executeTransaction`'s
|
|
332
|
+
* size checks; `CoordinatorRepo`'s solo short-circuit keeps unresolved and single-peer cohorts away from
|
|
333
|
+
* this method altogether in any case. After its responsibility check, what remains is a multi-member
|
|
334
|
+
* cohort that changed inside the responsibility cache's staleness window. With no local member wired the guard does not apply: that
|
|
335
|
+
* bypass exists for wiring without an identity (direct constructors, some tests), never for production.
|
|
336
|
+
*/
|
|
337
|
+
private assertLocalMemberInCohort(blockId: BlockId, peers: ClusterPeers): void {
|
|
338
|
+
if (!this.localCluster) return;
|
|
339
|
+
const peerIds = Object.keys(peers);
|
|
340
|
+
const selfId = this.localCluster.peerId.toString();
|
|
341
|
+
if (peerIds.length === 0 || peerIds.includes(selfId)) return;
|
|
342
|
+
log('cluster-tx:not-in-cohort', { blockId, selfId, peerIds });
|
|
343
|
+
throw new ResponsibilityRefusalError('not-responsible', [blockId],
|
|
344
|
+
`refusing to coordinate a cluster transaction for a cohort this node is not in: ${peerIds.join(', ')}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
254
347
|
private makeRecord(peers: ClusterPeers, messageHash: string, message: RepoMessage, membershipDigestValue: string): ClusterRecord {
|
|
255
348
|
const peerCount = Object.keys(peers ?? {}).length;
|
|
256
349
|
const record: ClusterRecord = {
|
|
@@ -356,6 +449,7 @@ export class ClusterCoordinator {
|
|
|
356
449
|
|
|
357
450
|
// Get the cluster peers for this block
|
|
358
451
|
const peers = await this.getClusterForBlock(blockId);
|
|
452
|
+
this.assertLocalMemberInCohort(blockId, peers);
|
|
359
453
|
|
|
360
454
|
// Bind the responsible membership into the transaction identity (v2): the digest is folded into
|
|
361
455
|
// the messageHash below, so two different peer sets produce two different messageHashes rather
|
|
@@ -494,18 +588,24 @@ export class ClusterCoordinator {
|
|
|
494
588
|
const promised = await this.collectPromises(peers, record);
|
|
495
589
|
const superMajority = Math.ceil(peerCount * this.cfg.superMajorityThreshold);
|
|
496
590
|
|
|
497
|
-
// Count approvals, rejections and
|
|
498
|
-
// saying "not now — I hold the race winner"
|
|
499
|
-
//
|
|
500
|
-
//
|
|
591
|
+
// Count approvals, rejections and the two RETRYABLE refusals separately. A `conflict` vote is a
|
|
592
|
+
// member saying "not now — I hold the race winner"; a `held` vote is a member saying "not now —
|
|
593
|
+
// a different unresolved action holds these blocks in my storage". Neither may count toward
|
|
594
|
+
// approvals OR rejections, or a transient refusal would masquerade as a validator rejection
|
|
595
|
+
// (permanent) or as silence (indistinguishable from an unreachable cohort) — both wrong.
|
|
501
596
|
const promises = promised.record.promises;
|
|
502
597
|
const approvalCount = Object.values(promises).filter(sig => sig.type === 'approve').length;
|
|
503
598
|
const rejectionCount = Object.values(promises).filter(sig => sig.type === 'reject').length;
|
|
504
599
|
const conflictCount = Object.values(promises).filter(sig => sig.type === 'conflict').length;
|
|
600
|
+
const heldCount = Object.values(promises).filter(sig => sig.type === 'held').length;
|
|
505
601
|
|
|
506
602
|
// Check if rejections make super-majority impossible
|
|
507
603
|
// If more than (peerCount - superMajority) nodes reject, we can never reach super-majority
|
|
508
604
|
const maxAllowedRejections = peerCount - superMajority;
|
|
605
|
+
// Whether the merged record itself PROVES super-majority unreachable — the same sum a member
|
|
606
|
+
// re-derives as `ConflictSuperseded`/`Rejected` from the signed votes, which is what makes an
|
|
607
|
+
// abandonment broadcast proof-carrying rather than an unauthenticated "forget this".
|
|
608
|
+
const refusalsProveUnreachable = rejectionCount + conflictCount + heldCount > maxAllowedRejections;
|
|
509
609
|
if (rejectionCount > maxAllowedRejections) {
|
|
510
610
|
const rejectReasonsByPeer = Object.fromEntries(Object.entries(promises)
|
|
511
611
|
.flatMap(([peerId, sig]) => sig.type === 'reject' ? [[peerId, sig.rejectReason ?? 'unknown'] as const] : []));
|
|
@@ -552,7 +652,7 @@ export class ClusterCoordinator {
|
|
|
552
652
|
// super-majority (members re-derive ConflictSuperseded/Rejected from the signed votes and
|
|
553
653
|
// clear their reservations immediately). Below that bar the record proves nothing and a
|
|
554
654
|
// broadcast would be the unauthenticated "forget this" the shortfall NOTE below refuses.
|
|
555
|
-
if (
|
|
655
|
+
if (refusalsProveUnreachable) {
|
|
556
656
|
this.broadcastAbandonment(promised.record, 'conflict-race-lost');
|
|
557
657
|
}
|
|
558
658
|
throw new ConflictRaceLostError(
|
|
@@ -560,6 +660,31 @@ export class ClusterCoordinator {
|
|
|
560
660
|
conflicts);
|
|
561
661
|
}
|
|
562
662
|
|
|
663
|
+
// A `held`-answered shortfall is the OTHER retryable refusal: the pend queued behind a rival's
|
|
664
|
+
// unresolved reservation. Checked after the conflict branch so a lost race still wins when both
|
|
665
|
+
// answer — a conflict vote names the winning transaction's messageHash, which is strictly more
|
|
666
|
+
// actionable than an action id — and, like it, before the generic shortfall, which must stay
|
|
667
|
+
// reserved for the genuinely-silent cohort.
|
|
668
|
+
if (heldCount > 0 && approvalCount < superMajority) {
|
|
669
|
+
const heldBy = Object.fromEntries(Object.entries(promises)
|
|
670
|
+
.flatMap(([peerId, sig]) => sig.type === 'held' ? [[peerId, sig.heldBy] as const] : []));
|
|
671
|
+
log('cluster-tx:pend-blocks-held', {
|
|
672
|
+
messageHash: record.messageHash,
|
|
673
|
+
peerCount,
|
|
674
|
+
approvals: approvalCount,
|
|
675
|
+
rejections: rejectionCount,
|
|
676
|
+
heldBy,
|
|
677
|
+
superMajority
|
|
678
|
+
});
|
|
679
|
+
this.updateTransactionRecord(promised.record, 'pend-blocks-held');
|
|
680
|
+
if (refusalsProveUnreachable) {
|
|
681
|
+
this.broadcastAbandonment(promised.record, 'pend-blocks-held');
|
|
682
|
+
}
|
|
683
|
+
throw new BlocksHeldError(
|
|
684
|
+
`Pend blocks held: ${heldCount}/${peerCount} member(s) hold an unresolved rival action (${approvalCount}/${superMajority} approvals)`,
|
|
685
|
+
heldBy);
|
|
686
|
+
}
|
|
687
|
+
|
|
563
688
|
if (peerCount > 1 && approvalCount < superMajority) {
|
|
564
689
|
log('cluster-tx:supermajority-failed', {
|
|
565
690
|
messageHash: record.messageHash,
|
|
@@ -578,7 +703,8 @@ export class ClusterCoordinator {
|
|
|
578
703
|
// their own staleness sweep instead.
|
|
579
704
|
// NOTE: the message below is load-bearing wire text — the consuming repo
|
|
580
705
|
// (sereus cadre-core control-write-retry) matches it verbatim to retry a genuinely-silent
|
|
581
|
-
// cohort. Keep it byte-identical, and never fold conflict votes into its
|
|
706
|
+
// cohort. Keep it byte-identical, and never fold `conflict` or `held` votes into its
|
|
707
|
+
// rejection count.
|
|
582
708
|
throw new Error(`Failed to get super-majority: ${approvalCount}/${peerCount} approvals (needed ${superMajority}, ${rejectionCount} rejections)`);
|
|
583
709
|
}
|
|
584
710
|
|
|
@@ -614,14 +740,14 @@ export class ClusterCoordinator {
|
|
|
614
740
|
}
|
|
615
741
|
|
|
616
742
|
/**
|
|
617
|
-
* The block's cohort peer ids as currently derivable. Empty when
|
|
618
|
-
* (
|
|
619
|
-
* the degraded-routing branch
|
|
620
|
-
*
|
|
743
|
+
* The block's cohort peer ids as currently derivable. Empty when the cohort did not resolve
|
|
744
|
+
* ({@link resolveCohort}: `findCluster` threw, or named nobody), so a caller branching on
|
|
745
|
+
* `length <= 1` is also taking the degraded-routing branch. Derived from `resolveCohort` rather
|
|
746
|
+
* than re-deriving the cohort, so there is exactly one lookup rule.
|
|
621
747
|
*/
|
|
622
748
|
async getClusterPeerIds(blockId: BlockId): Promise<string[]> {
|
|
623
|
-
const
|
|
624
|
-
return
|
|
749
|
+
const cohort = await this.resolveCohort(blockId);
|
|
750
|
+
return cohort.resolved ? [...cohort.peerIds] : [];
|
|
625
751
|
}
|
|
626
752
|
|
|
627
753
|
/** {@link getClusterPeerIds}, counted. Derived from it rather than re-deriving the cohort, so the
|