@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { blockIdsForTransforms, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
|
|
2
|
-
import { computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigest, recordMembershipDigest, clampPriority } from "@optimystic/db-core";
|
|
2
|
+
import { computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigest, recordMembershipDigest, clampPriority, clusterVoteSigningPayload, clusterVoteVerificationPayload } from "@optimystic/db-core";
|
|
3
3
|
import { verifyInvalidationCertificate } from "../dispute/invalidation.js";
|
|
4
4
|
import { buildCommitCert, invalidationActionId } from "./commit-cert.js";
|
|
5
5
|
import { ClusterClient } from "./client.js";
|
|
@@ -17,10 +17,12 @@ var TransactionPhase;
|
|
|
17
17
|
(function (TransactionPhase) {
|
|
18
18
|
TransactionPhase[TransactionPhase["Promising"] = 0] = "Promising";
|
|
19
19
|
TransactionPhase[TransactionPhase["OurPromiseNeeded"] = 1] = "OurPromiseNeeded";
|
|
20
|
-
TransactionPhase[TransactionPhase["
|
|
21
|
-
TransactionPhase[TransactionPhase["
|
|
22
|
-
TransactionPhase[TransactionPhase["
|
|
23
|
-
TransactionPhase[TransactionPhase["
|
|
20
|
+
TransactionPhase[TransactionPhase["OurConflictVoteNeeded"] = 2] = "OurConflictVoteNeeded";
|
|
21
|
+
TransactionPhase[TransactionPhase["OurCommitNeeded"] = 3] = "OurCommitNeeded";
|
|
22
|
+
TransactionPhase[TransactionPhase["Consensus"] = 4] = "Consensus";
|
|
23
|
+
TransactionPhase[TransactionPhase["Rejected"] = 5] = "Rejected";
|
|
24
|
+
TransactionPhase[TransactionPhase["ConflictSuperseded"] = 6] = "ConflictSuperseded";
|
|
25
|
+
TransactionPhase[TransactionPhase["Propagating"] = 7] = "Propagating"; // Transaction is being propagated
|
|
24
26
|
})(TransactionPhase || (TransactionPhase = {}));
|
|
25
27
|
/** Stable reject reason a member emits when a declared peer set fails the membership admission gate. */
|
|
26
28
|
export const MEMBERSHIP_NOT_ADMITTED = 'membership-not-admitted';
|
|
@@ -239,78 +241,104 @@ export class ClusterMember {
|
|
|
239
241
|
mergedCommits: Object.keys(currentRecord.commits ?? {})
|
|
240
242
|
});
|
|
241
243
|
}
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
// Drive the phase machine to a FIXPOINT rather than handling one phase per delivery. Each
|
|
245
|
+
// vote this member adds can put the record straight into the next phase (our promise
|
|
246
|
+
// completes super-majority ⇒ our commit is due; our commit completes the majority ⇒
|
|
247
|
+
// consensus; our reject/conflict vote makes the record terminal), and any follow-on phase
|
|
248
|
+
// not re-handled here would silently wait for the coordinator's next delivery — e.g. a
|
|
249
|
+
// member whose promise the coordinator never collected receives the commit-phase record,
|
|
250
|
+
// adds its promise, and must then also commit in the SAME delivery. One loop replaces the
|
|
251
|
+
// hand-written per-branch re-checks that used to cover only the follow-ons their authors
|
|
252
|
+
// thought of. Only the three vote-adding phases continue; each strictly grows the record
|
|
253
|
+
// (adds a promise or commit key), so the loop terminates — the cap guards a
|
|
254
|
+
// phase-computation bug, not a real bound.
|
|
255
|
+
const MaxPhaseSteps = 8;
|
|
250
256
|
let shouldPersist = true;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
log('cluster-member:action-promise', {
|
|
255
|
-
messageHash: record.messageHash
|
|
256
|
-
});
|
|
257
|
-
currentRecord = await this.handlePromiseNeeded(currentRecord);
|
|
258
|
-
log('cluster-member:action-promise-complete', {
|
|
259
|
-
messageHash: record.messageHash,
|
|
260
|
-
promises: Object.keys(currentRecord.promises ?? {})
|
|
261
|
-
});
|
|
262
|
-
break;
|
|
263
|
-
case TransactionPhase.OurCommitNeeded:
|
|
264
|
-
log('cluster-member:action-commit', {
|
|
265
|
-
messageHash: record.messageHash
|
|
266
|
-
});
|
|
267
|
-
currentRecord = await this.handleCommitNeeded(currentRecord);
|
|
268
|
-
log('cluster-member:action-commit-complete', {
|
|
269
|
-
messageHash: record.messageHash,
|
|
270
|
-
commits: Object.keys(currentRecord.commits ?? {})
|
|
271
|
-
});
|
|
272
|
-
// After adding our commit, check if we now have consensus and execute if so
|
|
273
|
-
{
|
|
274
|
-
const newPhase = await this.getTransactionPhase(currentRecord);
|
|
275
|
-
if (newPhase === TransactionPhase.Consensus) {
|
|
276
|
-
log('cluster-member:action-consensus-after-commit', {
|
|
277
|
-
messageHash: record.messageHash
|
|
278
|
-
});
|
|
279
|
-
await this.handleConsensus(currentRecord);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
shouldPersist = false;
|
|
283
|
-
break;
|
|
284
|
-
case TransactionPhase.Consensus:
|
|
285
|
-
log('cluster-member:action-consensus', {
|
|
286
|
-
messageHash: record.messageHash
|
|
287
|
-
});
|
|
288
|
-
await this.handleConsensus(currentRecord);
|
|
289
|
-
// Don't call clearTransaction here - it happens in handleConsensus
|
|
290
|
-
shouldPersist = false;
|
|
291
|
-
break;
|
|
292
|
-
case TransactionPhase.Rejected:
|
|
293
|
-
log('cluster-member:action-rejected', {
|
|
294
|
-
messageHash: record.messageHash
|
|
295
|
-
});
|
|
296
|
-
// Don't call clearTransaction here - it happens in handleRejection
|
|
297
|
-
await this.handleRejection(currentRecord);
|
|
298
|
-
shouldPersist = false;
|
|
299
|
-
break;
|
|
300
|
-
case TransactionPhase.Propagating:
|
|
301
|
-
// Transaction is complete and propagating - clean it up
|
|
302
|
-
log('cluster-member:phase-propagating', {
|
|
303
|
-
messageHash: record.messageHash
|
|
304
|
-
});
|
|
305
|
-
shouldPersist = false;
|
|
306
|
-
break;
|
|
307
|
-
case TransactionPhase.Promising:
|
|
308
|
-
// Still collecting promises from peers - if we haven't added ours and there's no conflict, add it
|
|
309
|
-
// This state shouldn't normally be reached since OurPromiseNeeded is checked first
|
|
310
|
-
log('cluster-member:phase-promising-blocked', {
|
|
311
|
-
messageHash: record.messageHash
|
|
312
|
-
});
|
|
257
|
+
phaseLoop: for (let step = 0;; step++) {
|
|
258
|
+
if (step >= MaxPhaseSteps) {
|
|
259
|
+
log('cluster-member:phase-loop-overflow', { messageHash: record.messageHash, steps: step });
|
|
313
260
|
break;
|
|
261
|
+
}
|
|
262
|
+
const phaseResult = await this.getTransactionPhase(currentRecord);
|
|
263
|
+
log('cluster-member:phase', {
|
|
264
|
+
messageHash: record.messageHash,
|
|
265
|
+
phase: phaseResult.phase,
|
|
266
|
+
step,
|
|
267
|
+
promises: Object.keys(currentRecord.promises ?? {}),
|
|
268
|
+
commits: Object.keys(currentRecord.commits ?? {})
|
|
269
|
+
});
|
|
270
|
+
switch (phaseResult.phase) {
|
|
271
|
+
case TransactionPhase.OurPromiseNeeded:
|
|
272
|
+
log('cluster-member:action-promise', {
|
|
273
|
+
messageHash: record.messageHash
|
|
274
|
+
});
|
|
275
|
+
currentRecord = await this.handlePromiseNeeded(currentRecord);
|
|
276
|
+
log('cluster-member:action-promise-complete', {
|
|
277
|
+
messageHash: record.messageHash,
|
|
278
|
+
promises: Object.keys(currentRecord.promises ?? {})
|
|
279
|
+
});
|
|
280
|
+
// Our own vote can be terminal (a reject where maxAllowedRejections is 0) or complete
|
|
281
|
+
// the super-majority — recompute rather than guess which.
|
|
282
|
+
continue;
|
|
283
|
+
case TransactionPhase.OurConflictVoteNeeded:
|
|
284
|
+
currentRecord = await this.handleConflictVoteNeeded(currentRecord, phaseResult.conflictsWith);
|
|
285
|
+
// Never persist a record we conflict-voted: this member holds the WINNER, and
|
|
286
|
+
// persisting the loser would reserve the same blocks a second time — half of what
|
|
287
|
+
// made the silent-abstention failure self-sustaining.
|
|
288
|
+
shouldPersist = false;
|
|
289
|
+
continue;
|
|
290
|
+
case TransactionPhase.OurCommitNeeded:
|
|
291
|
+
log('cluster-member:action-commit', {
|
|
292
|
+
messageHash: record.messageHash
|
|
293
|
+
});
|
|
294
|
+
currentRecord = await this.handleCommitNeeded(currentRecord);
|
|
295
|
+
log('cluster-member:action-commit-complete', {
|
|
296
|
+
messageHash: record.messageHash,
|
|
297
|
+
commits: Object.keys(currentRecord.commits ?? {})
|
|
298
|
+
});
|
|
299
|
+
shouldPersist = false;
|
|
300
|
+
// Our commit may have completed the majority — recompute; Consensus executes below.
|
|
301
|
+
continue;
|
|
302
|
+
case TransactionPhase.Consensus:
|
|
303
|
+
log('cluster-member:action-consensus', {
|
|
304
|
+
messageHash: record.messageHash
|
|
305
|
+
});
|
|
306
|
+
await this.handleConsensus(currentRecord);
|
|
307
|
+
shouldPersist = false;
|
|
308
|
+
break phaseLoop;
|
|
309
|
+
case TransactionPhase.Rejected:
|
|
310
|
+
log('cluster-member:action-rejected', {
|
|
311
|
+
messageHash: record.messageHash
|
|
312
|
+
});
|
|
313
|
+
await this.handleRejection(currentRecord);
|
|
314
|
+
shouldPersist = false;
|
|
315
|
+
break phaseLoop;
|
|
316
|
+
case TransactionPhase.ConflictSuperseded:
|
|
317
|
+
// Enough conflict votes that super-majority is unreachable. NOT a rejection — the
|
|
318
|
+
// callers retry it as a fresh transaction — so it gets its own terminal phase and the
|
|
319
|
+
// record is cleared rather than held (holding a provably-dead loser would reserve its
|
|
320
|
+
// blocks against the very retry that is supposed to win).
|
|
321
|
+
log('cluster-member:action-conflict-superseded', {
|
|
322
|
+
messageHash: record.messageHash
|
|
323
|
+
});
|
|
324
|
+
shouldPersist = false;
|
|
325
|
+
break phaseLoop;
|
|
326
|
+
case TransactionPhase.Propagating:
|
|
327
|
+
// Transaction is complete and propagating - clean it up
|
|
328
|
+
log('cluster-member:phase-propagating', {
|
|
329
|
+
messageHash: record.messageHash
|
|
330
|
+
});
|
|
331
|
+
shouldPersist = false;
|
|
332
|
+
break phaseLoop;
|
|
333
|
+
case TransactionPhase.Promising:
|
|
334
|
+
// We have already voted (approve, reject, or conflict); the record is still
|
|
335
|
+
// collecting promises from the rest of the cohort. Nothing to add — retain the
|
|
336
|
+
// record only if our vote wasn't a conflict (`shouldPersist` already reflects that).
|
|
337
|
+
log('cluster-member:phase-promising-waiting', {
|
|
338
|
+
messageHash: record.messageHash
|
|
339
|
+
});
|
|
340
|
+
break phaseLoop;
|
|
341
|
+
}
|
|
314
342
|
}
|
|
315
343
|
if (shouldPersist) {
|
|
316
344
|
// Update transaction state
|
|
@@ -515,13 +543,8 @@ export class ClusterMember {
|
|
|
515
543
|
async computeCommitHash(record) {
|
|
516
544
|
return computeClusterCommitHash(record.messageHash, record.message, record.promises, recordMembershipDigest(record));
|
|
517
545
|
}
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
return new TextEncoder().encode(payload);
|
|
521
|
-
}
|
|
522
|
-
async signVote(hash, type, rejectReason) {
|
|
523
|
-
const payload = this.computeSigningPayload(hash, type, rejectReason);
|
|
524
|
-
const sigBytes = await this.privateKey.sign(payload);
|
|
546
|
+
async signVote(hash, type, extra) {
|
|
547
|
+
const sigBytes = await this.privateKey.sign(clusterVoteSigningPayload(hash, type, extra));
|
|
525
548
|
return uint8ArrayToString(sigBytes, 'base64url');
|
|
526
549
|
}
|
|
527
550
|
/**
|
|
@@ -557,7 +580,7 @@ export class ClusterMember {
|
|
|
557
580
|
}
|
|
558
581
|
try {
|
|
559
582
|
const pubKey = publicKeyFromRaw(keyBytes);
|
|
560
|
-
const payload =
|
|
583
|
+
const payload = clusterVoteVerificationPayload(hash, signature);
|
|
561
584
|
const sigBytes = uint8ArrayFromString(signature.signature, 'base64url');
|
|
562
585
|
const ok = await pubKey.verify(payload, sigBytes);
|
|
563
586
|
// Key is bound to peerId: a failed verify is a genuine bad vote from a proven identity → penalize.
|
|
@@ -580,31 +603,62 @@ export class ClusterMember {
|
|
|
580
603
|
const ourId = this.peerId.toString();
|
|
581
604
|
const superMajority = Math.ceil(peerCount * this.superMajorityThreshold);
|
|
582
605
|
const maxAllowedRejections = peerCount - superMajority;
|
|
583
|
-
// Check for rejections — rejected if too many rejections to ever reach super-majority
|
|
606
|
+
// Check for rejections — rejected if too many rejections to ever reach super-majority.
|
|
607
|
+
// ONLY `reject` votes count here: a `conflict` vote is "not now", never a validity
|
|
608
|
+
// judgement, so it must not push a record into the permanent `Rejected` phase.
|
|
584
609
|
const rejectedPromises = Object.values(record.promises).filter(s => s.type === 'reject');
|
|
610
|
+
const conflictPromises = Object.values(record.promises).filter(s => s.type === 'conflict');
|
|
585
611
|
const rejectedCommits = Object.values(record.commits).filter(s => s.type === 'reject');
|
|
586
612
|
if (rejectedPromises.length > maxAllowedRejections || this.hasMajority(rejectedCommits.length, peerCount)) {
|
|
587
|
-
return TransactionPhase.Rejected;
|
|
588
|
-
}
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
613
|
+
return { phase: TransactionPhase.Rejected };
|
|
614
|
+
}
|
|
615
|
+
// Conflict votes don't judge validity, but enough of them still make super-majority
|
|
616
|
+
// unreachable — a distinct terminal outcome (retryable as a fresh transaction) so logs and
|
|
617
|
+
// reputation-adjacent paths keep meaning what they say.
|
|
618
|
+
if (conflictPromises.length > 0 && rejectedPromises.length + conflictPromises.length > maxAllowedRejections) {
|
|
619
|
+
return { phase: TransactionPhase.ConflictSuperseded };
|
|
620
|
+
}
|
|
621
|
+
// Check if we need to vote. A lost race is answered with a conflict vote, not silence:
|
|
622
|
+
// absence used to mean both "unreachable" and "refusing in favour of a rival", and the
|
|
623
|
+
// coordinator could not tell the two apart. Once our conflict vote is merged into
|
|
624
|
+
// `promises`, this branch is skipped forever — a conflict vote is terminal for this record;
|
|
625
|
+
// a retry must be a fresh transaction (new messageHash), which `CoordinatorRepo.pend`
|
|
626
|
+
// already mints per call.
|
|
627
|
+
if (!record.promises[ourId]) {
|
|
628
|
+
const conflict = this.findConflict(record);
|
|
629
|
+
if (conflict) {
|
|
630
|
+
return { phase: TransactionPhase.OurConflictVoteNeeded, conflictsWith: conflict.blockedBy };
|
|
631
|
+
}
|
|
632
|
+
return { phase: TransactionPhase.OurPromiseNeeded };
|
|
633
|
+
}
|
|
634
|
+
// Check if we have enough approved promises to proceed to commit. Deliberately blind to what
|
|
635
|
+
// OUR own vote was: the rest of the cohort reaching super-majority is the commit rule
|
|
636
|
+
// (Theorem 1 Case 2), so a member that rejected — or conflict-voted — still signs the commit
|
|
637
|
+
// the cohort decided on rather than stalling it. Only the rejection/superseded thresholds
|
|
638
|
+
// above can stop a record here, and both are checked first.
|
|
639
|
+
//
|
|
640
|
+
// NOTE: signing the commit drops this member's reservation on the record
|
|
641
|
+
// (`shouldPersist = false` in the caller), and the phase fixpoint means that can now happen on
|
|
642
|
+
// the FIRST delivery when the record already arrives at super-majority, rather than a
|
|
643
|
+
// round-trip later. The safety argument is quorum intersection (Theorem 9: no rival can
|
|
644
|
+
// assemble its own super-majority once this one has), NOT the reservation — the reservation
|
|
645
|
+
// only orders *concurrently-pending* rivals. If a lost-update between commit-signing and
|
|
646
|
+
// consensus-apply ever shows up, hold the reservation until `handleConsensus` instead of
|
|
647
|
+
// releasing it here.
|
|
594
648
|
const approvedPromises = Object.values(record.promises).filter(s => s.type === 'approve');
|
|
595
649
|
if (approvedPromises.length >= superMajority && !record.commits[ourId]) {
|
|
596
|
-
return TransactionPhase.OurCommitNeeded;
|
|
650
|
+
return { phase: TransactionPhase.OurCommitNeeded };
|
|
597
651
|
}
|
|
598
652
|
// Check if still collecting promises
|
|
599
653
|
if (promiseCount < peerCount && approvedPromises.length < superMajority) {
|
|
600
|
-
return TransactionPhase.Promising;
|
|
654
|
+
return { phase: TransactionPhase.Promising };
|
|
601
655
|
}
|
|
602
656
|
// Check for consensus
|
|
603
657
|
const approvedCommits = Object.values(record.commits).filter(s => s.type === 'approve');
|
|
604
658
|
if (this.hasMajority(approvedCommits.length, peerCount)) {
|
|
605
|
-
return TransactionPhase.Consensus;
|
|
659
|
+
return { phase: TransactionPhase.Consensus };
|
|
606
660
|
}
|
|
607
|
-
return TransactionPhase.Propagating;
|
|
661
|
+
return { phase: TransactionPhase.Propagating };
|
|
608
662
|
}
|
|
609
663
|
hasMajority(count, total) {
|
|
610
664
|
return count > total / 2;
|
|
@@ -636,6 +690,31 @@ export class ClusterMember {
|
|
|
636
690
|
}
|
|
637
691
|
};
|
|
638
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Answer a record that lost the deterministic race to a transaction this member already holds
|
|
695
|
+
* (`docs/correctness.md` Theorems 1 & 9: the loser is TOLD it lost, not ignored — an unanswered
|
|
696
|
+
* loss is indistinguishable from an unreachable cohort at the coordinator). `conflictWith` — the
|
|
697
|
+
* winning rival's messageHash — is folded into the signed payload, so the claim is
|
|
698
|
+
* integrity-protected in transit and readable without parsing prose. NOT a validity judgement:
|
|
699
|
+
* {@link getTransactionPhase} never counts conflict votes toward the permanent-rejection
|
|
700
|
+
* threshold, and the coordinator surfaces them as a retryable loss, never a validator rejection.
|
|
701
|
+
*/
|
|
702
|
+
async handleConflictVoteNeeded(record, conflictWith) {
|
|
703
|
+
log('cluster-member:action-conflict-vote', {
|
|
704
|
+
messageHash: record.messageHash,
|
|
705
|
+
conflictWith
|
|
706
|
+
});
|
|
707
|
+
const promiseHash = await this.computePromiseHash(record);
|
|
708
|
+
const sig = await this.signVote(promiseHash, 'conflict', conflictWith);
|
|
709
|
+
const signature = { type: 'conflict', signature: sig, conflictWith };
|
|
710
|
+
return {
|
|
711
|
+
...record,
|
|
712
|
+
promises: {
|
|
713
|
+
...record.promises,
|
|
714
|
+
[this.peerId.toString()]: signature
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
639
718
|
/**
|
|
640
719
|
* The full promise-phase decision for a record: admit the declared membership FIRST, then (only if
|
|
641
720
|
* admitted) validate its pend operations. Failing either yields a `{ valid:false, reason }` the caller
|
|
@@ -1051,7 +1130,7 @@ export class ClusterMember {
|
|
|
1051
1130
|
// Gated on the sink: with no reactivity wired the preimage has no consumer, so a sink-less
|
|
1052
1131
|
// node pays neither the extra `sha256` nor the extra microtask — the true zero-cost default.
|
|
1053
1132
|
if (this.onCommitCertificate) {
|
|
1054
|
-
const commitSignedPayload =
|
|
1133
|
+
const commitSignedPayload = clusterVoteSigningPayload(await this.computeCommitHash(record), 'approve');
|
|
1055
1134
|
this.captureCommitCert(record, commit.actionId, commitSignedPayload);
|
|
1056
1135
|
}
|
|
1057
1136
|
let result;
|
|
@@ -1170,7 +1249,7 @@ export class ClusterMember {
|
|
|
1170
1249
|
// {@link invalidationActionId} the invalidation's change event also carries, so the bridge's
|
|
1171
1250
|
// cert extractor resolves it. Gated on the sink — a node with no reactivity wired pays nothing.
|
|
1172
1251
|
if (this.onCommitCertificate) {
|
|
1173
|
-
const invSignedPayload =
|
|
1252
|
+
const invSignedPayload = clusterVoteSigningPayload(await this.computeCommitHash(record), 'approve');
|
|
1174
1253
|
this.captureCommitCert(record, invalidationActionId(request.invalidatedActionId, request.resolution.disputeId), invSignedPayload);
|
|
1175
1254
|
}
|
|
1176
1255
|
if (!this.onInvalidate) {
|
|
@@ -1280,11 +1359,19 @@ export class ClusterMember {
|
|
|
1280
1359
|
resolutionTimeout: setTimeout(() => this.resolveWithPeers(record.messageHash), record.message.expiration + 5000 - Date.now()).unref()
|
|
1281
1360
|
};
|
|
1282
1361
|
}
|
|
1283
|
-
|
|
1362
|
+
/**
|
|
1363
|
+
* Scan this member's reservation table (`activeTransactions`) for a held transaction that
|
|
1364
|
+
* conflicts with `record` AND wins the deterministic race against it. Returns the winner's
|
|
1365
|
+
* identity — `{ blockedBy: messageHash }` — rather than a bare boolean, because that identity is
|
|
1366
|
+
* exactly what the resulting conflict vote must name (`Signature.conflictWith`); the old boolean
|
|
1367
|
+
* lost it. `undefined` means no blocking conflict. Side-effectful on the way through: stale
|
|
1368
|
+
* entries are swept, and a held transaction that LOSES the race to `record` is cleared.
|
|
1369
|
+
*/
|
|
1370
|
+
findConflict(record) {
|
|
1284
1371
|
const now = Date.now();
|
|
1285
1372
|
const staleThresholdMs = 2000; // 2 seconds - allow more time for distributed consensus
|
|
1286
1373
|
const incomingBlockIds = this.getAffectedBlockIds(record.message.operations);
|
|
1287
|
-
log('cluster-member:
|
|
1374
|
+
log('cluster-member:findConflict-check', {
|
|
1288
1375
|
messageHash: record.messageHash,
|
|
1289
1376
|
activeCount: this.activeTransactions.size,
|
|
1290
1377
|
incomingBlockIds
|
|
@@ -1294,7 +1381,7 @@ export class ClusterMember {
|
|
|
1294
1381
|
continue;
|
|
1295
1382
|
}
|
|
1296
1383
|
const existingBlockIds = this.getAffectedBlockIds(state.record.message.operations);
|
|
1297
|
-
log('cluster-member:
|
|
1384
|
+
log('cluster-member:findConflict-compare', {
|
|
1298
1385
|
existing: existingHash,
|
|
1299
1386
|
incoming: record.messageHash,
|
|
1300
1387
|
existingBlockIds,
|
|
@@ -1317,7 +1404,7 @@ export class ClusterMember {
|
|
|
1317
1404
|
existing: existingHash,
|
|
1318
1405
|
incoming: record.messageHash
|
|
1319
1406
|
});
|
|
1320
|
-
return
|
|
1407
|
+
return { blockedBy: existingHash }; // Reject incoming, naming the winner
|
|
1321
1408
|
}
|
|
1322
1409
|
else {
|
|
1323
1410
|
// Accept incoming, abort existing
|
|
@@ -1330,37 +1417,47 @@ export class ClusterMember {
|
|
|
1330
1417
|
}
|
|
1331
1418
|
}
|
|
1332
1419
|
}
|
|
1333
|
-
return
|
|
1420
|
+
return undefined; // No blocking conflicts
|
|
1421
|
+
}
|
|
1422
|
+
/** Number of *approve* promise votes on a record — the count the commit rule uses. */
|
|
1423
|
+
static approvalCount(record) {
|
|
1424
|
+
return Object.values(record.promises).filter(s => s.type === 'approve').length;
|
|
1334
1425
|
}
|
|
1335
1426
|
/**
|
|
1336
1427
|
* Resolve a race between two conflicting transactions. Total and deterministic, so every honest
|
|
1337
1428
|
* member computes the identical winner (the Theorem 1 Case-2 premise). Order:
|
|
1338
|
-
* 1. more promise signatures wins (progress monotonicity — see safety note below);
|
|
1339
|
-
* 2. equal
|
|
1429
|
+
* 1. more *approve* promise signatures wins (progress monotonicity — see safety note below);
|
|
1430
|
+
* 2. equal approval counts → higher aged priority wins (fairness — see {@link recordPriority});
|
|
1340
1431
|
* 3. still tied → higher message hash wins.
|
|
1341
1432
|
*
|
|
1342
|
-
*
|
|
1433
|
+
* The count is APPROVALS, not `promises` keys. `promises` is the vote map — a reject occupies a key
|
|
1434
|
+
* there exactly as an approve does — so counting keys would treat a rejection as progress, letting a
|
|
1435
|
+
* record that can never commit outrank (and therefore block, via {@link findConflict}) a fresh rival
|
|
1436
|
+
* for the whole staleness window. Approvals is also the count the invariant below actually needs:
|
|
1437
|
+
* the commit rule is `approvedPromises >= superMajority`, which never looks at rejections.
|
|
1438
|
+
*
|
|
1439
|
+
* Approval count is FIRST so this comparison never displaces a transaction that is further along.
|
|
1343
1440
|
* That restores the pre-priority safety invariant: a member commits purely on promise supermajority
|
|
1344
1441
|
* (`handleCommitNeeded` signs whenever `approvedPromises >= superMajority`; the commit path has NO
|
|
1345
1442
|
* conflict re-check), so `resolveRace` is the ONLY arbiter among concurrently-pending conflicts.
|
|
1346
|
-
* With
|
|
1347
|
-
* strictly fewer
|
|
1348
|
-
*
|
|
1443
|
+
* With approvals-first, once transaction X holds a promise supermajority every conflicting rival Y has
|
|
1444
|
+
* strictly fewer approvals — Y can only match X's count by getting the intersecting quorum member to
|
|
1445
|
+
* approve it, but that member already holds X at supermajority and `resolveRace(X, Y)` returns
|
|
1349
1446
|
* `keep-existing` on X's higher count, so it never does. By quorum intersection any Y-supermajority
|
|
1350
1447
|
* overlaps X's in ≥1 honest member, and that member rejects Y. One winner (docs/correctness.md
|
|
1351
1448
|
* Theorem 9). Priority-first would break this: it could displace an already-quorum-reached X for a
|
|
1352
|
-
* higher-priority Y with fewer
|
|
1449
|
+
* higher-priority Y with fewer approvals, letting BOTH commit (split brain) — the regression fixed by
|
|
1353
1450
|
* ticket occ-priority-first-breaks-promise-monotonicity.
|
|
1354
1451
|
*
|
|
1355
|
-
* Priority is now a tie-break that runs only at EQUAL
|
|
1452
|
+
* Priority is now a tie-break that runs only at EQUAL approval counts, which is exactly the
|
|
1356
1453
|
* concurrent-starvation case aging targets (two fresh rivals, 0 promises each, otherwise coin-flipping
|
|
1357
1454
|
* on the hash). Priority still breaks those ties deterministically, so aging still solves the stated
|
|
1358
1455
|
* fairness problem in its common case. It only orders two *concurrently-pending* conflicts; it does NOT
|
|
1359
1456
|
* defer a fresh pend for an absent aged transaction (that residual — sequential sub-window starvation —
|
|
1360
1457
|
* is the deferred feat-occ-priority-reservation).
|
|
1361
1458
|
*
|
|
1362
|
-
* NOTE: residual-fairness tripwire. Under
|
|
1363
|
-
* rival that has *legitimately* gathered even one more
|
|
1459
|
+
* NOTE: residual-fairness tripwire. Under approvals-first an aged transaction can still lose to a fresh
|
|
1460
|
+
* rival that has *legitimately* gathered even one more approval — that is not the pure-coin-flip
|
|
1364
1461
|
* starvation aging targets (equal counts, priority wins), it is the monotonicity behaviour we WANT (a
|
|
1365
1462
|
* more-progressed rival is never displaced). If deeper fairness against a genuinely-more-progressed
|
|
1366
1463
|
* rival is ever needed, it belongs to feat-occ-priority-reservation (reserve/defer at pend time), NOT
|
|
@@ -1369,7 +1466,7 @@ export class ClusterMember {
|
|
|
1369
1466
|
* NOTE: Byzantine self-assert is a fairness DoS, not a safety hole. A coordinator can stamp
|
|
1370
1467
|
* priority == MaxPriority on every transaction; recordPriority clamps to the cap so it cannot
|
|
1371
1468
|
* exceed it, and priority never influences validity/operationsHash/stale-read checks — and now sits
|
|
1372
|
-
* below the
|
|
1469
|
+
* below the approval count, so it can only break equal-count ties it might have ~50% won anyway,
|
|
1373
1470
|
* degrading to at-worst-status-quo fairness (the same graceful-degradation class as spam under
|
|
1374
1471
|
* honest-majority). Binding priority to provable age is out of scope (feat-occ-priority-reservation).
|
|
1375
1472
|
*
|
|
@@ -1378,13 +1475,17 @@ export class ClusterMember {
|
|
|
1378
1475
|
* (design-hot-log-tail-sharding-guidance).
|
|
1379
1476
|
*/
|
|
1380
1477
|
resolveRace(existing, incoming) {
|
|
1381
|
-
// 1. Transaction with more
|
|
1382
|
-
|
|
1383
|
-
|
|
1478
|
+
// 1. Transaction with more APPROVALS wins — never displace a more-progressed rival (safety, see
|
|
1479
|
+
// above). Counting `promises` keys instead would count reject votes as progress: a record holding
|
|
1480
|
+
// one rejection would outrank an untouched rival and reserve its blocks for the whole staleness
|
|
1481
|
+
// window, and the commit rule this ordering protects (`approvedPromises >= superMajority`) never
|
|
1482
|
+
// looks at rejections anyway.
|
|
1483
|
+
const existingCount = ClusterMember.approvalCount(existing);
|
|
1484
|
+
const incomingCount = ClusterMember.approvalCount(incoming);
|
|
1384
1485
|
if (existingCount !== incomingCount) {
|
|
1385
1486
|
return existingCount > incomingCount ? 'keep-existing' : 'accept-incoming';
|
|
1386
1487
|
}
|
|
1387
|
-
// 2. Equal
|
|
1488
|
+
// 2. Equal approval counts → higher aged priority wins (fairness tie-break).
|
|
1388
1489
|
const existingPriority = this.recordPriority(existing);
|
|
1389
1490
|
const incomingPriority = this.recordPriority(incoming);
|
|
1390
1491
|
if (existingPriority !== incomingPriority) {
|
|
@@ -1405,7 +1506,7 @@ export class ClusterMember {
|
|
|
1405
1506
|
* NOTE: `message` is fixed for a transaction's whole lifecycle (promises/commits accrue in the
|
|
1406
1507
|
* separate `promises`/`commits` maps, never in `message`), so a transaction keeps its rank through
|
|
1407
1508
|
* the commit phase — there is no "priority drops to 0 at commit" asymmetry. resolveRace is only
|
|
1408
|
-
* consulted at the promise decision (
|
|
1509
|
+
* consulted at the promise decision (findConflict), i.e. between two still-open conflicting
|
|
1409
1510
|
* transactions, which is exactly the concurrent-contention case priority is meant to order.
|
|
1410
1511
|
*/
|
|
1411
1512
|
recordPriority(record) {
|
|
@@ -1554,7 +1655,13 @@ export class ClusterMember {
|
|
|
1554
1655
|
const state = this.activeTransactions.get(messageHash);
|
|
1555
1656
|
if (!state)
|
|
1556
1657
|
continue;
|
|
1557
|
-
|
|
1658
|
+
// NOTE: an expired entry already in a terminal phase is deliberately left alone here —
|
|
1659
|
+
// `processUpdate` clears those on the update that made them terminal, so reaching this point
|
|
1660
|
+
// in one means that update never arrived. It is not stranded: `findConflict`'s 2 s staleness
|
|
1661
|
+
// sweep drops it on the next conflicting arrival. But on a member that then goes idle the
|
|
1662
|
+
// entry lingers until traffic returns. If member memory ever shows entries outliving their
|
|
1663
|
+
// expiration, delete unconditionally here instead of exempting the terminal phases.
|
|
1664
|
+
const { phase } = await this.getTransactionPhase(state.record);
|
|
1558
1665
|
if (phase !== TransactionPhase.Consensus && phase !== TransactionPhase.Rejected) {
|
|
1559
1666
|
this.activeTransactions.delete(messageHash);
|
|
1560
1667
|
}
|