@optimystic/db-p2p 0.22.0 → 0.24.1
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/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +39 -14
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +226 -119
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +37 -7
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +77 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +88 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +134 -28
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +25 -1
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +62 -9
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +242 -73
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +3 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +3 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +35 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -668
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -203
- package/src/cluster/cluster-repo.ts +242 -122
- package/src/cluster/cluster-size-coupling.ts +45 -45
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -203
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +42 -11
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +79 -19
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -49
- package/src/libp2p-key-network.ts +1120 -990
- package/src/libp2p-node-base.ts +1675 -1651
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +3 -3
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/optimystic-node.ts +36 -36
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +7 -3
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +281 -74
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -36
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -1014
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -417
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -2,7 +2,7 @@ import type { IRepo, ClusterRecord, ClusterPeers, Signature, RepoMessage, ITrans
|
|
|
2
2
|
import type { ICluster } from "@optimystic/db-core";
|
|
3
3
|
import type { IPeerNetwork } from "@optimystic/db-core";
|
|
4
4
|
import { blockIdsForTransforms, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
|
|
5
|
-
import { computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigest, recordMembershipDigest, clampPriority } from "@optimystic/db-core";
|
|
5
|
+
import { computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigest, recordMembershipDigest, clampPriority, clusterVoteSigningPayload, clusterVoteVerificationPayload } from "@optimystic/db-core";
|
|
6
6
|
import { verifyInvalidationCertificate, type ArbitratorSetRecompute } from "../dispute/invalidation.js";
|
|
7
7
|
import { buildCommitCert, invalidationActionId } from "./commit-cert.js";
|
|
8
8
|
import { ClusterClient } from "./client.js";
|
|
@@ -24,14 +24,27 @@ const log = createLogger('cluster-member')
|
|
|
24
24
|
|
|
25
25
|
/** State of a transaction in the cluster */
|
|
26
26
|
enum TransactionPhase {
|
|
27
|
-
Promising, //
|
|
27
|
+
Promising, // We have voted; still collecting promises from other peers
|
|
28
28
|
OurPromiseNeeded, // We need to provide our promise
|
|
29
|
+
OurConflictVoteNeeded, // We hold a conflicting race winner; we must answer with a conflict vote
|
|
29
30
|
OurCommitNeeded, // We need to provide our commit
|
|
30
31
|
Consensus, // Transaction has reached consensus
|
|
31
|
-
Rejected, // Transaction was rejected
|
|
32
|
+
Rejected, // Transaction was rejected (validity judgement — enough reject votes)
|
|
33
|
+
ConflictSuperseded, // Terminal but retryable: conflict votes make super-majority unreachable
|
|
32
34
|
Propagating // Transaction is being propagated
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A phase plus the data its handler needs. Only {@link TransactionPhase.OurConflictVoteNeeded}
|
|
39
|
+
* carries any — `conflictsWith`, the winning rival's messageHash — so the conflict-vote handler need
|
|
40
|
+
* not re-run conflict detection (whose race resolution has side effects) to learn what blocked it.
|
|
41
|
+
* Split by phase rather than an optional field so the handler reads it without an assertion, and so
|
|
42
|
+
* a future phase-with-data cannot silently inherit this one's payload.
|
|
43
|
+
*/
|
|
44
|
+
type PhaseResult =
|
|
45
|
+
| { phase: TransactionPhase.OurConflictVoteNeeded; conflictsWith: string }
|
|
46
|
+
| { phase: Exclude<TransactionPhase, TransactionPhase.OurConflictVoteNeeded> };
|
|
47
|
+
|
|
35
48
|
interface TransactionState {
|
|
36
49
|
record: ClusterRecord;
|
|
37
50
|
promiseTimeout?: NodeJS.Timeout;
|
|
@@ -391,79 +404,104 @@ export class ClusterMember implements ICluster {
|
|
|
391
404
|
});
|
|
392
405
|
}
|
|
393
406
|
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
407
|
+
// Drive the phase machine to a FIXPOINT rather than handling one phase per delivery. Each
|
|
408
|
+
// vote this member adds can put the record straight into the next phase (our promise
|
|
409
|
+
// completes super-majority ⇒ our commit is due; our commit completes the majority ⇒
|
|
410
|
+
// consensus; our reject/conflict vote makes the record terminal), and any follow-on phase
|
|
411
|
+
// not re-handled here would silently wait for the coordinator's next delivery — e.g. a
|
|
412
|
+
// member whose promise the coordinator never collected receives the commit-phase record,
|
|
413
|
+
// adds its promise, and must then also commit in the SAME delivery. One loop replaces the
|
|
414
|
+
// hand-written per-branch re-checks that used to cover only the follow-ons their authors
|
|
415
|
+
// thought of. Only the three vote-adding phases continue; each strictly grows the record
|
|
416
|
+
// (adds a promise or commit key), so the loop terminates — the cap guards a
|
|
417
|
+
// phase-computation bug, not a real bound.
|
|
418
|
+
const MaxPhaseSteps = 8;
|
|
402
419
|
let shouldPersist = true;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
case TransactionPhase.OurPromiseNeeded:
|
|
407
|
-
log('cluster-member:action-promise', {
|
|
408
|
-
messageHash: record.messageHash
|
|
409
|
-
});
|
|
410
|
-
currentRecord = await this.handlePromiseNeeded(currentRecord);
|
|
411
|
-
log('cluster-member:action-promise-complete', {
|
|
412
|
-
messageHash: record.messageHash,
|
|
413
|
-
promises: Object.keys(currentRecord.promises ?? {})
|
|
414
|
-
});
|
|
415
|
-
break;
|
|
416
|
-
case TransactionPhase.OurCommitNeeded:
|
|
417
|
-
log('cluster-member:action-commit', {
|
|
418
|
-
messageHash: record.messageHash
|
|
419
|
-
});
|
|
420
|
-
currentRecord = await this.handleCommitNeeded(currentRecord);
|
|
421
|
-
log('cluster-member:action-commit-complete', {
|
|
422
|
-
messageHash: record.messageHash,
|
|
423
|
-
commits: Object.keys(currentRecord.commits ?? {})
|
|
424
|
-
});
|
|
425
|
-
// After adding our commit, check if we now have consensus and execute if so
|
|
426
|
-
{
|
|
427
|
-
const newPhase = await this.getTransactionPhase(currentRecord);
|
|
428
|
-
if (newPhase === TransactionPhase.Consensus) {
|
|
429
|
-
log('cluster-member:action-consensus-after-commit', {
|
|
430
|
-
messageHash: record.messageHash
|
|
431
|
-
});
|
|
432
|
-
await this.handleConsensus(currentRecord);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
shouldPersist = false;
|
|
436
|
-
break;
|
|
437
|
-
case TransactionPhase.Consensus:
|
|
438
|
-
log('cluster-member:action-consensus', {
|
|
439
|
-
messageHash: record.messageHash
|
|
440
|
-
});
|
|
441
|
-
await this.handleConsensus(currentRecord);
|
|
442
|
-
// Don't call clearTransaction here - it happens in handleConsensus
|
|
443
|
-
shouldPersist = false;
|
|
444
|
-
break;
|
|
445
|
-
case TransactionPhase.Rejected:
|
|
446
|
-
log('cluster-member:action-rejected', {
|
|
447
|
-
messageHash: record.messageHash
|
|
448
|
-
});
|
|
449
|
-
// Don't call clearTransaction here - it happens in handleRejection
|
|
450
|
-
await this.handleRejection(currentRecord);
|
|
451
|
-
shouldPersist = false;
|
|
452
|
-
break;
|
|
453
|
-
case TransactionPhase.Propagating:
|
|
454
|
-
// Transaction is complete and propagating - clean it up
|
|
455
|
-
log('cluster-member:phase-propagating', {
|
|
456
|
-
messageHash: record.messageHash
|
|
457
|
-
});
|
|
458
|
-
shouldPersist = false;
|
|
459
|
-
break;
|
|
460
|
-
case TransactionPhase.Promising:
|
|
461
|
-
// Still collecting promises from peers - if we haven't added ours and there's no conflict, add it
|
|
462
|
-
// This state shouldn't normally be reached since OurPromiseNeeded is checked first
|
|
463
|
-
log('cluster-member:phase-promising-blocked', {
|
|
464
|
-
messageHash: record.messageHash
|
|
465
|
-
});
|
|
420
|
+
phaseLoop: for (let step = 0; ; step++) {
|
|
421
|
+
if (step >= MaxPhaseSteps) {
|
|
422
|
+
log('cluster-member:phase-loop-overflow', { messageHash: record.messageHash, steps: step });
|
|
466
423
|
break;
|
|
424
|
+
}
|
|
425
|
+
const phaseResult = await this.getTransactionPhase(currentRecord);
|
|
426
|
+
log('cluster-member:phase', {
|
|
427
|
+
messageHash: record.messageHash,
|
|
428
|
+
phase: phaseResult.phase,
|
|
429
|
+
step,
|
|
430
|
+
promises: Object.keys(currentRecord.promises ?? {}),
|
|
431
|
+
commits: Object.keys(currentRecord.commits ?? {})
|
|
432
|
+
});
|
|
433
|
+
switch (phaseResult.phase) {
|
|
434
|
+
case TransactionPhase.OurPromiseNeeded:
|
|
435
|
+
log('cluster-member:action-promise', {
|
|
436
|
+
messageHash: record.messageHash
|
|
437
|
+
});
|
|
438
|
+
currentRecord = await this.handlePromiseNeeded(currentRecord);
|
|
439
|
+
log('cluster-member:action-promise-complete', {
|
|
440
|
+
messageHash: record.messageHash,
|
|
441
|
+
promises: Object.keys(currentRecord.promises ?? {})
|
|
442
|
+
});
|
|
443
|
+
// Our own vote can be terminal (a reject where maxAllowedRejections is 0) or complete
|
|
444
|
+
// the super-majority — recompute rather than guess which.
|
|
445
|
+
continue;
|
|
446
|
+
case TransactionPhase.OurConflictVoteNeeded:
|
|
447
|
+
currentRecord = await this.handleConflictVoteNeeded(currentRecord, phaseResult.conflictsWith);
|
|
448
|
+
// Never persist a record we conflict-voted: this member holds the WINNER, and
|
|
449
|
+
// persisting the loser would reserve the same blocks a second time — half of what
|
|
450
|
+
// made the silent-abstention failure self-sustaining.
|
|
451
|
+
shouldPersist = false;
|
|
452
|
+
continue;
|
|
453
|
+
case TransactionPhase.OurCommitNeeded:
|
|
454
|
+
log('cluster-member:action-commit', {
|
|
455
|
+
messageHash: record.messageHash
|
|
456
|
+
});
|
|
457
|
+
currentRecord = await this.handleCommitNeeded(currentRecord);
|
|
458
|
+
log('cluster-member:action-commit-complete', {
|
|
459
|
+
messageHash: record.messageHash,
|
|
460
|
+
commits: Object.keys(currentRecord.commits ?? {})
|
|
461
|
+
});
|
|
462
|
+
shouldPersist = false;
|
|
463
|
+
// Our commit may have completed the majority — recompute; Consensus executes below.
|
|
464
|
+
continue;
|
|
465
|
+
case TransactionPhase.Consensus:
|
|
466
|
+
log('cluster-member:action-consensus', {
|
|
467
|
+
messageHash: record.messageHash
|
|
468
|
+
});
|
|
469
|
+
await this.handleConsensus(currentRecord);
|
|
470
|
+
shouldPersist = false;
|
|
471
|
+
break phaseLoop;
|
|
472
|
+
case TransactionPhase.Rejected:
|
|
473
|
+
log('cluster-member:action-rejected', {
|
|
474
|
+
messageHash: record.messageHash
|
|
475
|
+
});
|
|
476
|
+
await this.handleRejection(currentRecord);
|
|
477
|
+
shouldPersist = false;
|
|
478
|
+
break phaseLoop;
|
|
479
|
+
case TransactionPhase.ConflictSuperseded:
|
|
480
|
+
// Enough conflict votes that super-majority is unreachable. NOT a rejection — the
|
|
481
|
+
// callers retry it as a fresh transaction — so it gets its own terminal phase and the
|
|
482
|
+
// record is cleared rather than held (holding a provably-dead loser would reserve its
|
|
483
|
+
// blocks against the very retry that is supposed to win).
|
|
484
|
+
log('cluster-member:action-conflict-superseded', {
|
|
485
|
+
messageHash: record.messageHash
|
|
486
|
+
});
|
|
487
|
+
shouldPersist = false;
|
|
488
|
+
break phaseLoop;
|
|
489
|
+
case TransactionPhase.Propagating:
|
|
490
|
+
// Transaction is complete and propagating - clean it up
|
|
491
|
+
log('cluster-member:phase-propagating', {
|
|
492
|
+
messageHash: record.messageHash
|
|
493
|
+
});
|
|
494
|
+
shouldPersist = false;
|
|
495
|
+
break phaseLoop;
|
|
496
|
+
case TransactionPhase.Promising:
|
|
497
|
+
// We have already voted (approve, reject, or conflict); the record is still
|
|
498
|
+
// collecting promises from the rest of the cohort. Nothing to add — retain the
|
|
499
|
+
// record only if our vote wasn't a conflict (`shouldPersist` already reflects that).
|
|
500
|
+
log('cluster-member:phase-promising-waiting', {
|
|
501
|
+
messageHash: record.messageHash
|
|
502
|
+
});
|
|
503
|
+
break phaseLoop;
|
|
504
|
+
}
|
|
467
505
|
}
|
|
468
506
|
|
|
469
507
|
if (shouldPersist) {
|
|
@@ -701,14 +739,8 @@ export class ClusterMember implements ICluster {
|
|
|
701
739
|
return computeClusterCommitHash(record.messageHash, record.message, record.promises, recordMembershipDigest(record));
|
|
702
740
|
}
|
|
703
741
|
|
|
704
|
-
private
|
|
705
|
-
const
|
|
706
|
-
return new TextEncoder().encode(payload);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
private async signVote(hash: string, type: 'approve' | 'reject', rejectReason?: string): Promise<string> {
|
|
710
|
-
const payload = this.computeSigningPayload(hash, type, rejectReason);
|
|
711
|
-
const sigBytes = await this.privateKey.sign(payload);
|
|
742
|
+
private async signVote(hash: string, type: Signature['type'], extra?: string): Promise<string> {
|
|
743
|
+
const sigBytes = await this.privateKey.sign(clusterVoteSigningPayload(hash, type, extra));
|
|
712
744
|
return uint8ArrayToString(sigBytes, 'base64url');
|
|
713
745
|
}
|
|
714
746
|
|
|
@@ -744,7 +776,7 @@ export class ClusterMember implements ICluster {
|
|
|
744
776
|
}
|
|
745
777
|
try {
|
|
746
778
|
const pubKey = publicKeyFromRaw(keyBytes);
|
|
747
|
-
const payload =
|
|
779
|
+
const payload = clusterVoteVerificationPayload(hash, signature);
|
|
748
780
|
const sigBytes = uint8ArrayFromString(signature.signature, 'base64url');
|
|
749
781
|
const ok = await pubKey.verify(payload, sigBytes);
|
|
750
782
|
// Key is bound to peerId: a failed verify is a genuine bad vote from a proven identity → penalize.
|
|
@@ -761,7 +793,7 @@ export class ClusterMember implements ICluster {
|
|
|
761
793
|
}
|
|
762
794
|
}
|
|
763
795
|
|
|
764
|
-
private async getTransactionPhase(record: ClusterRecord): Promise<
|
|
796
|
+
private async getTransactionPhase(record: ClusterRecord): Promise<PhaseResult> {
|
|
765
797
|
const peerCount = Object.keys(record.peers).length;
|
|
766
798
|
const promiseCount = Object.keys(record.promises).length;
|
|
767
799
|
const ourId = this.peerId.toString();
|
|
@@ -769,36 +801,68 @@ export class ClusterMember implements ICluster {
|
|
|
769
801
|
const superMajority = Math.ceil(peerCount * this.superMajorityThreshold);
|
|
770
802
|
const maxAllowedRejections = peerCount - superMajority;
|
|
771
803
|
|
|
772
|
-
// Check for rejections — rejected if too many rejections to ever reach super-majority
|
|
804
|
+
// Check for rejections — rejected if too many rejections to ever reach super-majority.
|
|
805
|
+
// ONLY `reject` votes count here: a `conflict` vote is "not now", never a validity
|
|
806
|
+
// judgement, so it must not push a record into the permanent `Rejected` phase.
|
|
773
807
|
const rejectedPromises = Object.values(record.promises).filter(s => s.type === 'reject');
|
|
808
|
+
const conflictPromises = Object.values(record.promises).filter(s => s.type === 'conflict');
|
|
774
809
|
const rejectedCommits = Object.values(record.commits).filter(s => s.type === 'reject');
|
|
775
810
|
if (rejectedPromises.length > maxAllowedRejections || this.hasMajority(rejectedCommits.length, peerCount)) {
|
|
776
|
-
return TransactionPhase.Rejected;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
811
|
+
return { phase: TransactionPhase.Rejected };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Conflict votes don't judge validity, but enough of them still make super-majority
|
|
815
|
+
// unreachable — a distinct terminal outcome (retryable as a fresh transaction) so logs and
|
|
816
|
+
// reputation-adjacent paths keep meaning what they say.
|
|
817
|
+
if (conflictPromises.length > 0 && rejectedPromises.length + conflictPromises.length > maxAllowedRejections) {
|
|
818
|
+
return { phase: TransactionPhase.ConflictSuperseded };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Check if we need to vote. A lost race is answered with a conflict vote, not silence:
|
|
822
|
+
// absence used to mean both "unreachable" and "refusing in favour of a rival", and the
|
|
823
|
+
// coordinator could not tell the two apart. Once our conflict vote is merged into
|
|
824
|
+
// `promises`, this branch is skipped forever — a conflict vote is terminal for this record;
|
|
825
|
+
// a retry must be a fresh transaction (new messageHash), which `CoordinatorRepo.pend`
|
|
826
|
+
// already mints per call.
|
|
827
|
+
if (!record.promises[ourId]) {
|
|
828
|
+
const conflict = this.findConflict(record);
|
|
829
|
+
if (conflict) {
|
|
830
|
+
return { phase: TransactionPhase.OurConflictVoteNeeded, conflictsWith: conflict.blockedBy };
|
|
831
|
+
}
|
|
832
|
+
return { phase: TransactionPhase.OurPromiseNeeded };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check if we have enough approved promises to proceed to commit. Deliberately blind to what
|
|
836
|
+
// OUR own vote was: the rest of the cohort reaching super-majority is the commit rule
|
|
837
|
+
// (Theorem 1 Case 2), so a member that rejected — or conflict-voted — still signs the commit
|
|
838
|
+
// the cohort decided on rather than stalling it. Only the rejection/superseded thresholds
|
|
839
|
+
// above can stop a record here, and both are checked first.
|
|
840
|
+
//
|
|
841
|
+
// NOTE: signing the commit drops this member's reservation on the record
|
|
842
|
+
// (`shouldPersist = false` in the caller), and the phase fixpoint means that can now happen on
|
|
843
|
+
// the FIRST delivery when the record already arrives at super-majority, rather than a
|
|
844
|
+
// round-trip later. The safety argument is quorum intersection (Theorem 9: no rival can
|
|
845
|
+
// assemble its own super-majority once this one has), NOT the reservation — the reservation
|
|
846
|
+
// only orders *concurrently-pending* rivals. If a lost-update between commit-signing and
|
|
847
|
+
// consensus-apply ever shows up, hold the reservation until `handleConsensus` instead of
|
|
848
|
+
// releasing it here.
|
|
785
849
|
const approvedPromises = Object.values(record.promises).filter(s => s.type === 'approve');
|
|
786
850
|
if (approvedPromises.length >= superMajority && !record.commits[ourId]) {
|
|
787
|
-
return TransactionPhase.OurCommitNeeded;
|
|
851
|
+
return { phase: TransactionPhase.OurCommitNeeded };
|
|
788
852
|
}
|
|
789
853
|
|
|
790
854
|
// Check if still collecting promises
|
|
791
855
|
if (promiseCount < peerCount && approvedPromises.length < superMajority) {
|
|
792
|
-
return TransactionPhase.Promising;
|
|
856
|
+
return { phase: TransactionPhase.Promising };
|
|
793
857
|
}
|
|
794
858
|
|
|
795
859
|
// Check for consensus
|
|
796
860
|
const approvedCommits = Object.values(record.commits).filter(s => s.type === 'approve');
|
|
797
861
|
if (this.hasMajority(approvedCommits.length, peerCount)) {
|
|
798
|
-
return TransactionPhase.Consensus;
|
|
862
|
+
return { phase: TransactionPhase.Consensus };
|
|
799
863
|
}
|
|
800
864
|
|
|
801
|
-
return TransactionPhase.Propagating;
|
|
865
|
+
return { phase: TransactionPhase.Propagating };
|
|
802
866
|
}
|
|
803
867
|
|
|
804
868
|
private hasMajority(count: number, total: number): boolean {
|
|
@@ -837,6 +901,33 @@ export class ClusterMember implements ICluster {
|
|
|
837
901
|
};
|
|
838
902
|
}
|
|
839
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Answer a record that lost the deterministic race to a transaction this member already holds
|
|
906
|
+
* (`docs/correctness.md` Theorems 1 & 9: the loser is TOLD it lost, not ignored — an unanswered
|
|
907
|
+
* loss is indistinguishable from an unreachable cohort at the coordinator). `conflictWith` — the
|
|
908
|
+
* winning rival's messageHash — is folded into the signed payload, so the claim is
|
|
909
|
+
* integrity-protected in transit and readable without parsing prose. NOT a validity judgement:
|
|
910
|
+
* {@link getTransactionPhase} never counts conflict votes toward the permanent-rejection
|
|
911
|
+
* threshold, and the coordinator surfaces them as a retryable loss, never a validator rejection.
|
|
912
|
+
*/
|
|
913
|
+
private async handleConflictVoteNeeded(record: ClusterRecord, conflictWith: string): Promise<ClusterRecord> {
|
|
914
|
+
log('cluster-member:action-conflict-vote', {
|
|
915
|
+
messageHash: record.messageHash,
|
|
916
|
+
conflictWith
|
|
917
|
+
});
|
|
918
|
+
const promiseHash = await this.computePromiseHash(record);
|
|
919
|
+
const sig = await this.signVote(promiseHash, 'conflict', conflictWith);
|
|
920
|
+
const signature: Signature = { type: 'conflict', signature: sig, conflictWith };
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
...record,
|
|
924
|
+
promises: {
|
|
925
|
+
...record.promises,
|
|
926
|
+
[this.peerId.toString()]: signature
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
840
931
|
/**
|
|
841
932
|
* The full promise-phase decision for a record: admit the declared membership FIRST, then (only if
|
|
842
933
|
* admitted) validate its pend operations. Failing either yields a `{ valid:false, reason }` the caller
|
|
@@ -1268,7 +1359,7 @@ export class ClusterMember implements ICluster {
|
|
|
1268
1359
|
// Gated on the sink: with no reactivity wired the preimage has no consumer, so a sink-less
|
|
1269
1360
|
// node pays neither the extra `sha256` nor the extra microtask — the true zero-cost default.
|
|
1270
1361
|
if (this.onCommitCertificate) {
|
|
1271
|
-
const commitSignedPayload =
|
|
1362
|
+
const commitSignedPayload = clusterVoteSigningPayload(await this.computeCommitHash(record), 'approve');
|
|
1272
1363
|
this.captureCommitCert(record, commit.actionId, commitSignedPayload);
|
|
1273
1364
|
}
|
|
1274
1365
|
let result: CommitResult;
|
|
@@ -1393,7 +1484,7 @@ export class ClusterMember implements ICluster {
|
|
|
1393
1484
|
// {@link invalidationActionId} the invalidation's change event also carries, so the bridge's
|
|
1394
1485
|
// cert extractor resolves it. Gated on the sink — a node with no reactivity wired pays nothing.
|
|
1395
1486
|
if (this.onCommitCertificate) {
|
|
1396
|
-
const invSignedPayload =
|
|
1487
|
+
const invSignedPayload = clusterVoteSigningPayload(await this.computeCommitHash(record), 'approve');
|
|
1397
1488
|
this.captureCommitCert(record, invalidationActionId(request.invalidatedActionId, request.resolution.disputeId), invSignedPayload);
|
|
1398
1489
|
}
|
|
1399
1490
|
|
|
@@ -1520,12 +1611,20 @@ export class ClusterMember implements ICluster {
|
|
|
1520
1611
|
};
|
|
1521
1612
|
}
|
|
1522
1613
|
|
|
1523
|
-
|
|
1614
|
+
/**
|
|
1615
|
+
* Scan this member's reservation table (`activeTransactions`) for a held transaction that
|
|
1616
|
+
* conflicts with `record` AND wins the deterministic race against it. Returns the winner's
|
|
1617
|
+
* identity — `{ blockedBy: messageHash }` — rather than a bare boolean, because that identity is
|
|
1618
|
+
* exactly what the resulting conflict vote must name (`Signature.conflictWith`); the old boolean
|
|
1619
|
+
* lost it. `undefined` means no blocking conflict. Side-effectful on the way through: stale
|
|
1620
|
+
* entries are swept, and a held transaction that LOSES the race to `record` is cleared.
|
|
1621
|
+
*/
|
|
1622
|
+
private findConflict(record: ClusterRecord): { blockedBy: string } | undefined {
|
|
1524
1623
|
const now = Date.now();
|
|
1525
1624
|
const staleThresholdMs = 2000; // 2 seconds - allow more time for distributed consensus
|
|
1526
1625
|
|
|
1527
1626
|
const incomingBlockIds = this.getAffectedBlockIds(record.message.operations);
|
|
1528
|
-
log('cluster-member:
|
|
1627
|
+
log('cluster-member:findConflict-check', {
|
|
1529
1628
|
messageHash: record.messageHash,
|
|
1530
1629
|
activeCount: this.activeTransactions.size,
|
|
1531
1630
|
incomingBlockIds
|
|
@@ -1537,7 +1636,7 @@ export class ClusterMember implements ICluster {
|
|
|
1537
1636
|
}
|
|
1538
1637
|
|
|
1539
1638
|
const existingBlockIds = this.getAffectedBlockIds(state.record.message.operations);
|
|
1540
|
-
log('cluster-member:
|
|
1639
|
+
log('cluster-member:findConflict-compare', {
|
|
1541
1640
|
existing: existingHash,
|
|
1542
1641
|
incoming: record.messageHash,
|
|
1543
1642
|
existingBlockIds,
|
|
@@ -1563,7 +1662,7 @@ export class ClusterMember implements ICluster {
|
|
|
1563
1662
|
existing: existingHash,
|
|
1564
1663
|
incoming: record.messageHash
|
|
1565
1664
|
});
|
|
1566
|
-
return
|
|
1665
|
+
return { blockedBy: existingHash }; // Reject incoming, naming the winner
|
|
1567
1666
|
} else {
|
|
1568
1667
|
// Accept incoming, abort existing
|
|
1569
1668
|
log('cluster-member:race-accept-incoming', {
|
|
@@ -1576,38 +1675,49 @@ export class ClusterMember implements ICluster {
|
|
|
1576
1675
|
}
|
|
1577
1676
|
}
|
|
1578
1677
|
|
|
1579
|
-
return
|
|
1678
|
+
return undefined; // No blocking conflicts
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/** Number of *approve* promise votes on a record — the count the commit rule uses. */
|
|
1682
|
+
private static approvalCount(record: ClusterRecord): number {
|
|
1683
|
+
return Object.values(record.promises).filter(s => s.type === 'approve').length;
|
|
1580
1684
|
}
|
|
1581
1685
|
|
|
1582
1686
|
/**
|
|
1583
1687
|
* Resolve a race between two conflicting transactions. Total and deterministic, so every honest
|
|
1584
1688
|
* member computes the identical winner (the Theorem 1 Case-2 premise). Order:
|
|
1585
|
-
* 1. more promise signatures wins (progress monotonicity — see safety note below);
|
|
1586
|
-
* 2. equal
|
|
1689
|
+
* 1. more *approve* promise signatures wins (progress monotonicity — see safety note below);
|
|
1690
|
+
* 2. equal approval counts → higher aged priority wins (fairness — see {@link recordPriority});
|
|
1587
1691
|
* 3. still tied → higher message hash wins.
|
|
1588
1692
|
*
|
|
1589
|
-
*
|
|
1693
|
+
* The count is APPROVALS, not `promises` keys. `promises` is the vote map — a reject occupies a key
|
|
1694
|
+
* there exactly as an approve does — so counting keys would treat a rejection as progress, letting a
|
|
1695
|
+
* record that can never commit outrank (and therefore block, via {@link findConflict}) a fresh rival
|
|
1696
|
+
* for the whole staleness window. Approvals is also the count the invariant below actually needs:
|
|
1697
|
+
* the commit rule is `approvedPromises >= superMajority`, which never looks at rejections.
|
|
1698
|
+
*
|
|
1699
|
+
* Approval count is FIRST so this comparison never displaces a transaction that is further along.
|
|
1590
1700
|
* That restores the pre-priority safety invariant: a member commits purely on promise supermajority
|
|
1591
1701
|
* (`handleCommitNeeded` signs whenever `approvedPromises >= superMajority`; the commit path has NO
|
|
1592
1702
|
* conflict re-check), so `resolveRace` is the ONLY arbiter among concurrently-pending conflicts.
|
|
1593
|
-
* With
|
|
1594
|
-
* strictly fewer
|
|
1595
|
-
*
|
|
1703
|
+
* With approvals-first, once transaction X holds a promise supermajority every conflicting rival Y has
|
|
1704
|
+
* strictly fewer approvals — Y can only match X's count by getting the intersecting quorum member to
|
|
1705
|
+
* approve it, but that member already holds X at supermajority and `resolveRace(X, Y)` returns
|
|
1596
1706
|
* `keep-existing` on X's higher count, so it never does. By quorum intersection any Y-supermajority
|
|
1597
1707
|
* overlaps X's in ≥1 honest member, and that member rejects Y. One winner (docs/correctness.md
|
|
1598
1708
|
* Theorem 9). Priority-first would break this: it could displace an already-quorum-reached X for a
|
|
1599
|
-
* higher-priority Y with fewer
|
|
1709
|
+
* higher-priority Y with fewer approvals, letting BOTH commit (split brain) — the regression fixed by
|
|
1600
1710
|
* ticket occ-priority-first-breaks-promise-monotonicity.
|
|
1601
1711
|
*
|
|
1602
|
-
* Priority is now a tie-break that runs only at EQUAL
|
|
1712
|
+
* Priority is now a tie-break that runs only at EQUAL approval counts, which is exactly the
|
|
1603
1713
|
* concurrent-starvation case aging targets (two fresh rivals, 0 promises each, otherwise coin-flipping
|
|
1604
1714
|
* on the hash). Priority still breaks those ties deterministically, so aging still solves the stated
|
|
1605
1715
|
* fairness problem in its common case. It only orders two *concurrently-pending* conflicts; it does NOT
|
|
1606
1716
|
* defer a fresh pend for an absent aged transaction (that residual — sequential sub-window starvation —
|
|
1607
1717
|
* is the deferred feat-occ-priority-reservation).
|
|
1608
1718
|
*
|
|
1609
|
-
* NOTE: residual-fairness tripwire. Under
|
|
1610
|
-
* rival that has *legitimately* gathered even one more
|
|
1719
|
+
* NOTE: residual-fairness tripwire. Under approvals-first an aged transaction can still lose to a fresh
|
|
1720
|
+
* rival that has *legitimately* gathered even one more approval — that is not the pure-coin-flip
|
|
1611
1721
|
* starvation aging targets (equal counts, priority wins), it is the monotonicity behaviour we WANT (a
|
|
1612
1722
|
* more-progressed rival is never displaced). If deeper fairness against a genuinely-more-progressed
|
|
1613
1723
|
* rival is ever needed, it belongs to feat-occ-priority-reservation (reserve/defer at pend time), NOT
|
|
@@ -1616,7 +1726,7 @@ export class ClusterMember implements ICluster {
|
|
|
1616
1726
|
* NOTE: Byzantine self-assert is a fairness DoS, not a safety hole. A coordinator can stamp
|
|
1617
1727
|
* priority == MaxPriority on every transaction; recordPriority clamps to the cap so it cannot
|
|
1618
1728
|
* exceed it, and priority never influences validity/operationsHash/stale-read checks — and now sits
|
|
1619
|
-
* below the
|
|
1729
|
+
* below the approval count, so it can only break equal-count ties it might have ~50% won anyway,
|
|
1620
1730
|
* degrading to at-worst-status-quo fairness (the same graceful-degradation class as spam under
|
|
1621
1731
|
* honest-majority). Binding priority to provable age is out of scope (feat-occ-priority-reservation).
|
|
1622
1732
|
*
|
|
@@ -1625,14 +1735,18 @@ export class ClusterMember implements ICluster {
|
|
|
1625
1735
|
* (design-hot-log-tail-sharding-guidance).
|
|
1626
1736
|
*/
|
|
1627
1737
|
private resolveRace(existing: ClusterRecord, incoming: ClusterRecord): 'keep-existing' | 'accept-incoming' {
|
|
1628
|
-
// 1. Transaction with more
|
|
1629
|
-
|
|
1630
|
-
|
|
1738
|
+
// 1. Transaction with more APPROVALS wins — never displace a more-progressed rival (safety, see
|
|
1739
|
+
// above). Counting `promises` keys instead would count reject votes as progress: a record holding
|
|
1740
|
+
// one rejection would outrank an untouched rival and reserve its blocks for the whole staleness
|
|
1741
|
+
// window, and the commit rule this ordering protects (`approvedPromises >= superMajority`) never
|
|
1742
|
+
// looks at rejections anyway.
|
|
1743
|
+
const existingCount = ClusterMember.approvalCount(existing);
|
|
1744
|
+
const incomingCount = ClusterMember.approvalCount(incoming);
|
|
1631
1745
|
if (existingCount !== incomingCount) {
|
|
1632
1746
|
return existingCount > incomingCount ? 'keep-existing' : 'accept-incoming';
|
|
1633
1747
|
}
|
|
1634
1748
|
|
|
1635
|
-
// 2. Equal
|
|
1749
|
+
// 2. Equal approval counts → higher aged priority wins (fairness tie-break).
|
|
1636
1750
|
const existingPriority = this.recordPriority(existing);
|
|
1637
1751
|
const incomingPriority = this.recordPriority(incoming);
|
|
1638
1752
|
if (existingPriority !== incomingPriority) {
|
|
@@ -1655,7 +1769,7 @@ export class ClusterMember implements ICluster {
|
|
|
1655
1769
|
* NOTE: `message` is fixed for a transaction's whole lifecycle (promises/commits accrue in the
|
|
1656
1770
|
* separate `promises`/`commits` maps, never in `message`), so a transaction keeps its rank through
|
|
1657
1771
|
* the commit phase — there is no "priority drops to 0 at commit" asymmetry. resolveRace is only
|
|
1658
|
-
* consulted at the promise decision (
|
|
1772
|
+
* consulted at the promise decision (findConflict), i.e. between two still-open conflicting
|
|
1659
1773
|
* transactions, which is exactly the concurrent-contention case priority is meant to order.
|
|
1660
1774
|
*/
|
|
1661
1775
|
private recordPriority(record: ClusterRecord): number {
|
|
@@ -1813,7 +1927,13 @@ export class ClusterMember implements ICluster {
|
|
|
1813
1927
|
const state = this.activeTransactions.get(messageHash);
|
|
1814
1928
|
if (!state) continue;
|
|
1815
1929
|
|
|
1816
|
-
|
|
1930
|
+
// NOTE: an expired entry already in a terminal phase is deliberately left alone here —
|
|
1931
|
+
// `processUpdate` clears those on the update that made them terminal, so reaching this point
|
|
1932
|
+
// in one means that update never arrived. It is not stranded: `findConflict`'s 2 s staleness
|
|
1933
|
+
// sweep drops it on the next conflicting arrival. But on a member that then goes idle the
|
|
1934
|
+
// entry lingers until traffic returns. If member memory ever shows entries outliving their
|
|
1935
|
+
// expiration, delete unconditionally here instead of exempting the terminal phases.
|
|
1936
|
+
const { phase } = await this.getTransactionPhase(state.record);
|
|
1817
1937
|
if (phase !== TransactionPhase.Consensus && phase !== TransactionPhase.Rejected) {
|
|
1818
1938
|
this.activeTransactions.delete(messageHash);
|
|
1819
1939
|
}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/** A component that exposes the cluster size it actually resolved to. */
|
|
2
|
-
export interface HasEffectiveClusterSize {
|
|
3
|
-
readonly effectiveClusterSize: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Fail-fast coupling check for a live node's cluster-size wiring, mirroring
|
|
8
|
-
* `assertSuperMajorityCoupling`.
|
|
9
|
-
*
|
|
10
|
-
* `resolveClusterPolicy` (`cluster/cluster-policy.ts`) is the single place that settles the
|
|
11
|
-
* replication factor / target cohort breadth (`clusterSize`). Every consumer that resolves its
|
|
12
|
-
* own cluster size — `Libp2pKeyPeerNetwork` (peer selection) and `NetworkManagerService` (ring
|
|
13
|
-
* sizing) — must be constructed from that SAME resolved value, or the membership admission
|
|
14
|
-
* gate's "is this declared peer set suspiciously small?" yardstick silently diverges from the
|
|
15
|
-
* cohort width peer selection actually assembles (ticket
|
|
16
|
-
* `bug-cluster-size-resolution-single-source`: an unconfigured node ran peer selection at 16
|
|
17
|
-
* while consensus believed full size was 10, so a cohort that had quietly lost six members still
|
|
18
|
-
* measured as full).
|
|
19
|
-
*
|
|
20
|
-
* On a live node both are constructed from `resolveClusterPolicy(options).clusterSize`, so this
|
|
21
|
-
* check normally passes. It exists to catch *future* drift — a call site reverting to its own
|
|
22
|
-
* default, a new consumer added without threading the resolved value — by throwing at
|
|
23
|
-
* construction with every resolved value and its source, rather than letting the node come up
|
|
24
|
-
* mismatched.
|
|
25
|
-
*
|
|
26
|
-
* @throws Error naming the resolved size and every consumer that disagrees with it.
|
|
27
|
-
*/
|
|
28
|
-
export function assertClusterSizeCoupling(
|
|
29
|
-
resolvedClusterSize: number,
|
|
30
|
-
consumers: Record<string, HasEffectiveClusterSize | undefined>
|
|
31
|
-
): void {
|
|
32
|
-
const mismatches = Object.entries(consumers)
|
|
33
|
-
.filter((entry): entry is [string, HasEffectiveClusterSize] => entry[1] !== undefined)
|
|
34
|
-
.filter(([, consumer]) => consumer.effectiveClusterSize !== resolvedClusterSize)
|
|
35
|
-
.map(([name, consumer]) => `${name} resolved ${consumer.effectiveClusterSize}`);
|
|
36
|
-
|
|
37
|
-
if (mismatches.length > 0) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Cluster size mismatch at node startup: resolveClusterPolicy() produced clusterSize=${resolvedClusterSize}, ` +
|
|
40
|
-
`but ${mismatches.join(', ')}. Every cluster-size consumer must be constructed from the SAME resolved ` +
|
|
41
|
-
`value (see cluster/cluster-policy.ts), or the membership admission gate's yardstick can diverge from ` +
|
|
42
|
-
`the cohort width peer selection actually assembles.`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
/** A component that exposes the cluster size it actually resolved to. */
|
|
2
|
+
export interface HasEffectiveClusterSize {
|
|
3
|
+
readonly effectiveClusterSize: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fail-fast coupling check for a live node's cluster-size wiring, mirroring
|
|
8
|
+
* `assertSuperMajorityCoupling`.
|
|
9
|
+
*
|
|
10
|
+
* `resolveClusterPolicy` (`cluster/cluster-policy.ts`) is the single place that settles the
|
|
11
|
+
* replication factor / target cohort breadth (`clusterSize`). Every consumer that resolves its
|
|
12
|
+
* own cluster size — `Libp2pKeyPeerNetwork` (peer selection) and `NetworkManagerService` (ring
|
|
13
|
+
* sizing) — must be constructed from that SAME resolved value, or the membership admission
|
|
14
|
+
* gate's "is this declared peer set suspiciously small?" yardstick silently diverges from the
|
|
15
|
+
* cohort width peer selection actually assembles (ticket
|
|
16
|
+
* `bug-cluster-size-resolution-single-source`: an unconfigured node ran peer selection at 16
|
|
17
|
+
* while consensus believed full size was 10, so a cohort that had quietly lost six members still
|
|
18
|
+
* measured as full).
|
|
19
|
+
*
|
|
20
|
+
* On a live node both are constructed from `resolveClusterPolicy(options).clusterSize`, so this
|
|
21
|
+
* check normally passes. It exists to catch *future* drift — a call site reverting to its own
|
|
22
|
+
* default, a new consumer added without threading the resolved value — by throwing at
|
|
23
|
+
* construction with every resolved value and its source, rather than letting the node come up
|
|
24
|
+
* mismatched.
|
|
25
|
+
*
|
|
26
|
+
* @throws Error naming the resolved size and every consumer that disagrees with it.
|
|
27
|
+
*/
|
|
28
|
+
export function assertClusterSizeCoupling(
|
|
29
|
+
resolvedClusterSize: number,
|
|
30
|
+
consumers: Record<string, HasEffectiveClusterSize | undefined>
|
|
31
|
+
): void {
|
|
32
|
+
const mismatches = Object.entries(consumers)
|
|
33
|
+
.filter((entry): entry is [string, HasEffectiveClusterSize] => entry[1] !== undefined)
|
|
34
|
+
.filter(([, consumer]) => consumer.effectiveClusterSize !== resolvedClusterSize)
|
|
35
|
+
.map(([name, consumer]) => `${name} resolved ${consumer.effectiveClusterSize}`);
|
|
36
|
+
|
|
37
|
+
if (mismatches.length > 0) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Cluster size mismatch at node startup: resolveClusterPolicy() produced clusterSize=${resolvedClusterSize}, ` +
|
|
40
|
+
`but ${mismatches.join(', ')}. Every cluster-size consumer must be constructed from the SAME resolved ` +
|
|
41
|
+
`value (see cluster/cluster-policy.ts), or the membership admission gate's yardstick can diverge from ` +
|
|
42
|
+
`the cohort width peer selection actually assembles.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|