@optimystic/db-p2p 0.29.0 → 1.0.0-beta.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/cluster-repo.d.ts +75 -16
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +195 -24
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +16 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +66 -7
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +5 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +28 -3
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +6 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +62 -2
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/package.json +2 -2
- package/src/cluster/cluster-repo.ts +2662 -2488
- package/src/repo/cluster-coordinator.ts +1113 -1039
- package/src/repo/coordinator-repo.ts +2675 -2648
- package/src/storage/storage-repo.ts +65 -2
|
@@ -163,6 +163,7 @@ export declare class ClusterMember implements ICluster {
|
|
|
163
163
|
private executedTransactions;
|
|
164
164
|
private executedPendResults;
|
|
165
165
|
private executedCommitResults;
|
|
166
|
+
private refusedPendActions;
|
|
166
167
|
private appliedInvalidations;
|
|
167
168
|
private cleanupQueue;
|
|
168
169
|
private pendingUpdates;
|
|
@@ -245,6 +246,15 @@ export declare class ClusterMember implements ICluster {
|
|
|
245
246
|
*/
|
|
246
247
|
update(record: ClusterRecord): Promise<ClusterRecord>;
|
|
247
248
|
private processUpdate;
|
|
249
|
+
/**
|
|
250
|
+
* Add this member's own conflict-shaped pend verdict to a record's {@link ClusterRecord.applyOutcomes},
|
|
251
|
+
* when storage produced one for this transaction. A success, a bare-reason fault, or no retained
|
|
252
|
+
* verdict at all leaves the record untouched — the coordinator's rule is "an entry means retry",
|
|
253
|
+
* so an entry that does not mean retry must not exist.
|
|
254
|
+
*
|
|
255
|
+
* The member never signs this and never writes another peer's entry.
|
|
256
|
+
*/
|
|
257
|
+
private withOwnApplyOutcome;
|
|
248
258
|
/**
|
|
249
259
|
* Merges two records, validating that non-signature fields match.
|
|
250
260
|
* Detects equivocation (same peer changing vote type) and applies penalties.
|
|
@@ -405,7 +415,10 @@ export declare class ClusterMember implements ICluster {
|
|
|
405
415
|
* "I checked this". The four-way rule, per committed block:
|
|
406
416
|
* - no local `latest`, or `latest.rev < commit.rev` → abstain (approve). Preserves the
|
|
407
417
|
* lagging-member tolerance (`coordinator-repo-commit-divergence.spec.ts`): a member behind
|
|
408
|
-
* the commit cannot judge it.
|
|
418
|
+
* the commit cannot judge it. Abstaining is safe rather than merely tolerable: apply-time
|
|
419
|
+
* catches the case the vote cannot, since `StorageRepo.internalCommit` refuses an update-only
|
|
420
|
+
* transform whose declared `baseRev` is not this member's `latest.rev` and reconciles instead
|
|
421
|
+
* of forking the block's content.
|
|
409
422
|
* - `latest.rev === commit.rev` with the SAME action → abstain (approve). Idempotent
|
|
410
423
|
* redelivery of an already-durable commit; rejecting would make the writer rebase and
|
|
411
424
|
* re-append an action that already landed — a duplicate entry. (Storage's `alreadyDone`
|
|
@@ -420,20 +433,64 @@ export declare class ClusterMember implements ICluster {
|
|
|
420
433
|
* abstains. Reason stays plain prose — it is fed to computeSigningPayload and carried as
|
|
421
434
|
* Signature.rejectReason, exactly like the stale-revision pend reject.
|
|
422
435
|
*
|
|
423
|
-
* Residual (see
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
436
|
+
* Residual (narrowed, see below): a member that signed the winner's commit but has not yet
|
|
437
|
+
* APPLIED it sits in a window where it holds neither the winner's record (reservation dropped at
|
|
438
|
+
* commit-sign, `shouldPersist = false`) nor the winner's revision (storage still behind) — it
|
|
439
|
+
* abstains here. A capability-less member, or one with history truncated below `latest`, abstains
|
|
440
|
+
* at `latest.rev > commit.rev` too. A rival's commit can therefore still pass THIS check if every
|
|
441
|
+
* member is simultaneously in one of those states — on a fast cohort that window is the COMMON
|
|
442
|
+
* case, not the corner.
|
|
443
|
+
*
|
|
444
|
+
* What now covers it: {@link validateCommitAgainstRefusedPend} rejects the same commit from the
|
|
445
|
+
* other direction — not "did a rival take the revision" but "did I already refuse this action's
|
|
446
|
+
* pend" — which is precisely the state a member is left in when it lost the race in the
|
|
447
|
+
* promise→apply window. Downstream of consensus the backstop still stands: every member's apply
|
|
448
|
+
* refuses the rival as stale, the coordinating node's own member retains that refusal
|
|
449
|
+
* (`getExecutedCommitResult`), and `CoordinatorRepo.commit` confirms the rival against local
|
|
450
|
+
* storage and answers the writer with a retryable conflict instead of a fabricated success.
|
|
451
|
+
* `ConflictRaceLostError` conversion and classified rejections close the re-drive route the same
|
|
452
|
+
* way.
|
|
435
453
|
*/
|
|
436
454
|
private validateCommitRevisions;
|
|
455
|
+
/**
|
|
456
|
+
* Promise-round check that this member is not about to co-sign the commit of an action whose PEND
|
|
457
|
+
* its own storage refused.
|
|
458
|
+
*
|
|
459
|
+
* The hole this closes: the commit-round vote is deliberately blind ({@link getTransactionPhase}
|
|
460
|
+
* signs whenever promise approvals reach super-majority), and both other commit-promise checks
|
|
461
|
+
* abstain for a member that holds no pend of the committing action — which is exactly the state a
|
|
462
|
+
* member is in *because* it refused that pend. So a member could refuse action X's pend as
|
|
463
|
+
* conflicting with a rival, and milliseconds later sign X's commit, letting X reach commit
|
|
464
|
+
* consensus at a revision this member will never durably hold.
|
|
465
|
+
*
|
|
466
|
+
* Two conditions, both required:
|
|
467
|
+
* (a) this member retains a conflict-shaped pend refusal for `commit.actionId`
|
|
468
|
+
* ({@link refusedPendActions}), and
|
|
469
|
+
* (b) local storage STILL corroborates it — a rival pending action holds one of the commit's
|
|
470
|
+
* blocks, or a different action already took `commit.rev`.
|
|
471
|
+
*
|
|
472
|
+
* (b) is what keeps this from regressing the lagging-member and cohort-drift tolerances that
|
|
473
|
+
* {@link validateCommitRevisions} documents. A member that merely *missed* the pend retains no
|
|
474
|
+
* refusal and abstains exactly as before. A member whose refusal has since been settled — the
|
|
475
|
+
* rival cancelled, the reservation released — abstains too, rather than vetoing a commit the rest
|
|
476
|
+
* of the cohort is holding perfectly well. The retention is in-memory and TTL'd, so a restart also
|
|
477
|
+
* degrades to the previous abstain behaviour rather than to a stuck veto.
|
|
478
|
+
*
|
|
479
|
+
* Effect on a small cohort: one reject makes super-majority unreachable, so the commit never
|
|
480
|
+
* assembles consensus and the write fails loudly instead of forking the block. That is the
|
|
481
|
+
* intended outcome here — a clean retryable conflict answer is {@link ClusterRecord.applyOutcomes}'
|
|
482
|
+
* job, and this arm is the backstop for the paths that channel cannot reach (a member that applies
|
|
483
|
+
* late, via the scheduled commit-retry timer, after the coordinator has already answered).
|
|
484
|
+
*
|
|
485
|
+
* Never throws out of the vote path: a read fault is an abstain, same rule as its two siblings.
|
|
486
|
+
*/
|
|
487
|
+
private validateCommitAgainstRefusedPend;
|
|
488
|
+
/**
|
|
489
|
+
* Condition (b) of {@link validateCommitAgainstRefusedPend}: does local storage still back up a
|
|
490
|
+
* retained pend refusal for this commit? Returns a short prose fragment naming what corroborates
|
|
491
|
+
* it, or `undefined` for "no longer corroborated — abstain" (which a read fault also yields).
|
|
492
|
+
*/
|
|
493
|
+
private refusedPendConflictsWithLocalState;
|
|
437
494
|
/**
|
|
438
495
|
* Promise-round check of a commit record's declared content digests
|
|
439
496
|
* (`CommitRequest.blockDigests`) against what this member's OWN pended copy of each transform
|
|
@@ -450,9 +507,11 @@ export declare class ClusterMember implements ICluster {
|
|
|
450
507
|
* authored, delivered at pend — a hostile declarer cannot force or dodge a check by mis-declaring
|
|
451
508
|
* `baseRev`):
|
|
452
509
|
* - transform carries an `insert` → base-independent, ALWAYS check (declared `baseRev` ignored);
|
|
453
|
-
* - `updates` only → check iff this member's local base rev equals the declared `baseRev
|
|
454
|
-
*
|
|
455
|
-
*
|
|
510
|
+
* - `updates` only → check iff this member's local base rev equals the declared `baseRev`. A
|
|
511
|
+
* member whose base does not match cannot JUDGE the content — it would preview the transform
|
|
512
|
+
* against different bytes than the declarer used — so it abstains here. It never goes on to
|
|
513
|
+
* materialize those bytes: `StorageRepo.internalCommit` refuses that same mismatch at apply
|
|
514
|
+
* and reconciles from a cohort peer;
|
|
456
515
|
* - `delete` only / no base / unmaterializable base → materializes nothing to compare, abstain;
|
|
457
516
|
* - no pending transform for the action (this member never saw the pend) → abstain.
|
|
458
517
|
* "Abstain" = contribute no content attestation: approve exactly as before this check existed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cluster-repo.d.ts","sourceRoot":"","sources":["../../../src/cluster/cluster-repo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAa,WAAW,EAAE,qBAAqB,EAAE,sBAAsB,EAA2B,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAiB,UAAU,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACpR,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,EAAiC,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGxG,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAM5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAK7E,OAAO,EAA8C,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AA+CtG;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAExH;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;AAEnF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAElF;;;;;;;;GAQG;AACH,MAAM,MAAM,gCAAgC,GAAG,sBAAsB,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IACjC,wGAAwG;IACxG,KAAK,EAAE,YAAY,CAAC;IACpB,yGAAyG;IACzG,UAAU,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAyB/F,wGAAwG;AACxG,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE9E,UAAU,uBAAuB;IAChC,WAAW,EAAE,KAAK,CAAC;IACnB,WAAW,EAAE,YAAY,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,6FAA6F;IAC7F,cAAc,CAAC,EAAE,sBAAsB,CAAC;IACxC,kGAAkG;IAClG,mBAAmB,CAAC,EAAE,qBAAqB,CAAC;IAC5C,oGAAoG;IACpG,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,oHAAoH;IACpH,sBAAsB,CAAC,EAAE,gCAAgC,CAAC;IAC1D,mHAAmH;IACnH,qBAAqB,CAAC,EAAE,6BAA6B,CAAC;IACtD;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,uBAAuB,GAAG,aAAa,CAoBhF;AAKD;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAkBhD;;;GAGG;AACH,qBAAa,aAAc,YAAW,QAAQ;
|
|
1
|
+
{"version":3,"file":"cluster-repo.d.ts","sourceRoot":"","sources":["../../../src/cluster/cluster-repo.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blockIdsForTransforms, isOwnRevision, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
|
|
1
|
+
import { blockIdsForTransforms, isOwnRevision, isConflictFailure, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
|
|
2
2
|
import { computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigest, recordMembershipDigest, clusterVoteSigningPayload, clusterVoteVerificationPayload } from "@optimystic/db-core";
|
|
3
3
|
import { verifyInvalidationCertificate } from "../dispute/invalidation.js";
|
|
4
4
|
import { buildCommitCert, invalidationActionId } from "./commit-cert.js";
|
|
@@ -107,6 +107,14 @@ export class ClusterMember {
|
|
|
107
107
|
// at apply, and without this verdict the coordinator fabricates a success no member durably
|
|
108
108
|
// stored. Pruned alongside executedTransactions (same TTL).
|
|
109
109
|
executedCommitResults = new Map();
|
|
110
|
+
// Conflict-shaped pend refusals this member's storage produced at consensus-apply, keyed by the
|
|
111
|
+
// refused action's id rather than by messageHash. The messageHash-keyed map above cannot serve the
|
|
112
|
+
// commit-promise guard: a commit is a DIFFERENT message with a different hash, so a member holding
|
|
113
|
+
// "I refused action X's pend" has no way to find that verdict when X's commit arrives to be voted
|
|
114
|
+
// on. Keyed by actionId, it does. Same TTL, pruning and rollback discipline as its sibling —
|
|
115
|
+
// see {@link refusedPendConflictsWithLocalState} for how a stale entry is prevented from vetoing a
|
|
116
|
+
// commit the rest of the cohort holds fine. (-> refusedAt timestamp)
|
|
117
|
+
refusedPendActions = new Map();
|
|
110
118
|
// Fast in-memory dedup for applied invalidations, keyed `${invalidatedActionId}:${disputeId}`.
|
|
111
119
|
// The durable source of truth is the invalidation log entry (Log.findInvalidation, re-checked
|
|
112
120
|
// inside the sink); this map only spares redundant work when the same invalidation reaches
|
|
@@ -212,6 +220,7 @@ export class ClusterMember {
|
|
|
212
220
|
this.cleanupQueue.length = 0;
|
|
213
221
|
this.executedPendResults.clear();
|
|
214
222
|
this.executedCommitResults.clear();
|
|
223
|
+
this.refusedPendActions.clear();
|
|
215
224
|
}
|
|
216
225
|
/**
|
|
217
226
|
* Checks if a transaction's operations were already executed during consensus.
|
|
@@ -447,6 +456,14 @@ export class ClusterMember {
|
|
|
447
456
|
}
|
|
448
457
|
// Skip propagation - the coordinator manages distribution
|
|
449
458
|
// await this.propagateIfNeeded(currentRecord);
|
|
459
|
+
// Stamp our own apply verdict on the response, AFTER the phase loop has run (and therefore
|
|
460
|
+
// after any consensus apply this delivery triggered). This is the return channel that closes
|
|
461
|
+
// the non-coordinating-member hole: a member that refused the pend at apply is the only node
|
|
462
|
+
// that knows it, and until now that verdict never left the member. Attached here rather than
|
|
463
|
+
// inside the Consensus branch so a redelivery of an already-applied record answers with the
|
|
464
|
+
// same verdict instead of an empty one. Deliberately not folded into the persisted state: it
|
|
465
|
+
// is a property of this response, not of the transaction the member is holding.
|
|
466
|
+
currentRecord = this.withOwnApplyOutcome(currentRecord);
|
|
450
467
|
log('cluster-member:update-complete', {
|
|
451
468
|
messageHash: record.messageHash,
|
|
452
469
|
promiseCount: Object.keys(currentRecord.promises).length,
|
|
@@ -454,6 +471,24 @@ export class ClusterMember {
|
|
|
454
471
|
});
|
|
455
472
|
return currentRecord;
|
|
456
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Add this member's own conflict-shaped pend verdict to a record's {@link ClusterRecord.applyOutcomes},
|
|
476
|
+
* when storage produced one for this transaction. A success, a bare-reason fault, or no retained
|
|
477
|
+
* verdict at all leaves the record untouched — the coordinator's rule is "an entry means retry",
|
|
478
|
+
* so an entry that does not mean retry must not exist.
|
|
479
|
+
*
|
|
480
|
+
* The member never signs this and never writes another peer's entry.
|
|
481
|
+
*/
|
|
482
|
+
withOwnApplyOutcome(record) {
|
|
483
|
+
const verdict = this.executedPendResults.get(record.messageHash);
|
|
484
|
+
if (verdict === undefined || verdict.success || !isConflictFailure(verdict)) {
|
|
485
|
+
return record;
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
...record,
|
|
489
|
+
applyOutcomes: { ...record.applyOutcomes, [this.peerId.toString()]: { pend: verdict } }
|
|
490
|
+
};
|
|
491
|
+
}
|
|
457
492
|
/**
|
|
458
493
|
* Merges two records, validating that non-signature fields match.
|
|
459
494
|
* Detects equivocation (same peer changing vote type) and applies penalties.
|
|
@@ -502,10 +537,17 @@ export class ClusterMember {
|
|
|
502
537
|
// Merge signatures with equivocation detection
|
|
503
538
|
const mergedPromises = this.detectEquivocation(existing.promises, incoming.promises, 'promise', existing.messageHash);
|
|
504
539
|
const mergedCommits = this.detectEquivocation(existing.commits, incoming.commits, 'commit', existing.messageHash);
|
|
540
|
+
// Union the advisory apply outcomes per peer, first-seen wins. A peer only ever writes its own
|
|
541
|
+
// entry, so "first-seen wins" is per-peer idempotence, not a conflict rule. Omitted entirely
|
|
542
|
+
// when neither side carries any, so an ordinary record gains no empty object.
|
|
543
|
+
const mergedOutcomes = (existing.applyOutcomes || incoming.applyOutcomes)
|
|
544
|
+
? { ...incoming.applyOutcomes, ...existing.applyOutcomes }
|
|
545
|
+
: undefined;
|
|
505
546
|
return {
|
|
506
547
|
...existing,
|
|
507
548
|
promises: mergedPromises,
|
|
508
|
-
commits: mergedCommits
|
|
549
|
+
commits: mergedCommits,
|
|
550
|
+
...(mergedOutcomes === undefined ? {} : { applyOutcomes: mergedOutcomes })
|
|
509
551
|
};
|
|
510
552
|
}
|
|
511
553
|
/**
|
|
@@ -834,6 +876,14 @@ export class ClusterMember {
|
|
|
834
876
|
if (!commitRevValidation.valid) {
|
|
835
877
|
return commitRevValidation;
|
|
836
878
|
}
|
|
879
|
+
// Then our own refusal history: a commit whose pend THIS member refused, where local state
|
|
880
|
+
// still corroborates the refusal. Runs after the revision check because that one is sharper
|
|
881
|
+
// (it names the rival revision) and catches the same commit whenever the rival has already
|
|
882
|
+
// applied here; this arm covers the window where it has not.
|
|
883
|
+
const refusedPendValidation = await this.validateCommitAgainstRefusedPend(record);
|
|
884
|
+
if (!refusedPendValidation.valid) {
|
|
885
|
+
return refusedPendValidation;
|
|
886
|
+
}
|
|
837
887
|
return await this.validateCommitOperations(record);
|
|
838
888
|
}
|
|
839
889
|
/**
|
|
@@ -1188,8 +1238,10 @@ export class ClusterMember {
|
|
|
1188
1238
|
// moves that verdict into the phase where the cohort aggregates it, so the loser is
|
|
1189
1239
|
// refused with a real answer instead of burning a consensus round it cannot win. A
|
|
1190
1240
|
// member that has not yet applied the rival's pend has no record and simply abstains
|
|
1191
|
-
// from this reason; the
|
|
1192
|
-
// (getExecutedPendResult)
|
|
1241
|
+
// from this reason; the apply-time verdict catches that residual — retained locally
|
|
1242
|
+
// (getExecutedPendResult) for the coordinating node's own member, and returned to the
|
|
1243
|
+
// coordinator on the response record (ClusterRecord.applyOutcomes) by every other
|
|
1244
|
+
// member. Self is excluded so a redelivered pend
|
|
1193
1245
|
// for this same action stays approvable. An unavailable block carries no `pendings` and
|
|
1194
1246
|
// abstains (the rev branch above already fail-closes when a revision claim is at stake).
|
|
1195
1247
|
// Reason stays plain prose: it is fed to computeSigningPayload and carried as
|
|
@@ -1246,7 +1298,10 @@ export class ClusterMember {
|
|
|
1246
1298
|
* "I checked this". The four-way rule, per committed block:
|
|
1247
1299
|
* - no local `latest`, or `latest.rev < commit.rev` → abstain (approve). Preserves the
|
|
1248
1300
|
* lagging-member tolerance (`coordinator-repo-commit-divergence.spec.ts`): a member behind
|
|
1249
|
-
* the commit cannot judge it.
|
|
1301
|
+
* the commit cannot judge it. Abstaining is safe rather than merely tolerable: apply-time
|
|
1302
|
+
* catches the case the vote cannot, since `StorageRepo.internalCommit` refuses an update-only
|
|
1303
|
+
* transform whose declared `baseRev` is not this member's `latest.rev` and reconciles instead
|
|
1304
|
+
* of forking the block's content.
|
|
1250
1305
|
* - `latest.rev === commit.rev` with the SAME action → abstain (approve). Idempotent
|
|
1251
1306
|
* redelivery of an already-durable commit; rejecting would make the writer rebase and
|
|
1252
1307
|
* re-append an action that already landed — a duplicate entry. (Storage's `alreadyDone`
|
|
@@ -1261,18 +1316,23 @@ export class ClusterMember {
|
|
|
1261
1316
|
* abstains. Reason stays plain prose — it is fed to computeSigningPayload and carried as
|
|
1262
1317
|
* Signature.rejectReason, exactly like the stale-revision pend reject.
|
|
1263
1318
|
*
|
|
1264
|
-
* Residual (see
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1319
|
+
* Residual (narrowed, see below): a member that signed the winner's commit but has not yet
|
|
1320
|
+
* APPLIED it sits in a window where it holds neither the winner's record (reservation dropped at
|
|
1321
|
+
* commit-sign, `shouldPersist = false`) nor the winner's revision (storage still behind) — it
|
|
1322
|
+
* abstains here. A capability-less member, or one with history truncated below `latest`, abstains
|
|
1323
|
+
* at `latest.rev > commit.rev` too. A rival's commit can therefore still pass THIS check if every
|
|
1324
|
+
* member is simultaneously in one of those states — on a fast cohort that window is the COMMON
|
|
1325
|
+
* case, not the corner.
|
|
1326
|
+
*
|
|
1327
|
+
* What now covers it: {@link validateCommitAgainstRefusedPend} rejects the same commit from the
|
|
1328
|
+
* other direction — not "did a rival take the revision" but "did I already refuse this action's
|
|
1329
|
+
* pend" — which is precisely the state a member is left in when it lost the race in the
|
|
1330
|
+
* promise→apply window. Downstream of consensus the backstop still stands: every member's apply
|
|
1331
|
+
* refuses the rival as stale, the coordinating node's own member retains that refusal
|
|
1332
|
+
* (`getExecutedCommitResult`), and `CoordinatorRepo.commit` confirms the rival against local
|
|
1333
|
+
* storage and answers the writer with a retryable conflict instead of a fabricated success.
|
|
1334
|
+
* `ConflictRaceLostError` conversion and classified rejections close the re-drive route the same
|
|
1335
|
+
* way.
|
|
1276
1336
|
*/
|
|
1277
1337
|
async validateCommitRevisions(record) {
|
|
1278
1338
|
for (const operation of record.message.operations) {
|
|
@@ -1349,6 +1409,93 @@ export class ClusterMember {
|
|
|
1349
1409
|
}
|
|
1350
1410
|
return { valid: true };
|
|
1351
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Promise-round check that this member is not about to co-sign the commit of an action whose PEND
|
|
1414
|
+
* its own storage refused.
|
|
1415
|
+
*
|
|
1416
|
+
* The hole this closes: the commit-round vote is deliberately blind ({@link getTransactionPhase}
|
|
1417
|
+
* signs whenever promise approvals reach super-majority), and both other commit-promise checks
|
|
1418
|
+
* abstain for a member that holds no pend of the committing action — which is exactly the state a
|
|
1419
|
+
* member is in *because* it refused that pend. So a member could refuse action X's pend as
|
|
1420
|
+
* conflicting with a rival, and milliseconds later sign X's commit, letting X reach commit
|
|
1421
|
+
* consensus at a revision this member will never durably hold.
|
|
1422
|
+
*
|
|
1423
|
+
* Two conditions, both required:
|
|
1424
|
+
* (a) this member retains a conflict-shaped pend refusal for `commit.actionId`
|
|
1425
|
+
* ({@link refusedPendActions}), and
|
|
1426
|
+
* (b) local storage STILL corroborates it — a rival pending action holds one of the commit's
|
|
1427
|
+
* blocks, or a different action already took `commit.rev`.
|
|
1428
|
+
*
|
|
1429
|
+
* (b) is what keeps this from regressing the lagging-member and cohort-drift tolerances that
|
|
1430
|
+
* {@link validateCommitRevisions} documents. A member that merely *missed* the pend retains no
|
|
1431
|
+
* refusal and abstains exactly as before. A member whose refusal has since been settled — the
|
|
1432
|
+
* rival cancelled, the reservation released — abstains too, rather than vetoing a commit the rest
|
|
1433
|
+
* of the cohort is holding perfectly well. The retention is in-memory and TTL'd, so a restart also
|
|
1434
|
+
* degrades to the previous abstain behaviour rather than to a stuck veto.
|
|
1435
|
+
*
|
|
1436
|
+
* Effect on a small cohort: one reject makes super-majority unreachable, so the commit never
|
|
1437
|
+
* assembles consensus and the write fails loudly instead of forking the block. That is the
|
|
1438
|
+
* intended outcome here — a clean retryable conflict answer is {@link ClusterRecord.applyOutcomes}'
|
|
1439
|
+
* job, and this arm is the backstop for the paths that channel cannot reach (a member that applies
|
|
1440
|
+
* late, via the scheduled commit-retry timer, after the coordinator has already answered).
|
|
1441
|
+
*
|
|
1442
|
+
* Never throws out of the vote path: a read fault is an abstain, same rule as its two siblings.
|
|
1443
|
+
*/
|
|
1444
|
+
async validateCommitAgainstRefusedPend(record) {
|
|
1445
|
+
for (const operation of record.message.operations) {
|
|
1446
|
+
if (!('commit' in operation)) {
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const commit = operation.commit;
|
|
1450
|
+
if (!this.refusedPendActions.has(commit.actionId)) {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const corroboration = await this.refusedPendConflictsWithLocalState(commit);
|
|
1454
|
+
if (corroboration === undefined) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
log('cluster-member:validation-refused-pend-commit', {
|
|
1458
|
+
messageHash: record.messageHash,
|
|
1459
|
+
actionId: commit.actionId,
|
|
1460
|
+
rev: commit.rev,
|
|
1461
|
+
corroboration
|
|
1462
|
+
});
|
|
1463
|
+
// Prose only, for the same reason every other vote reason here is: it is fed to
|
|
1464
|
+
// computeSigningPayload and carried as Signature.rejectReason.
|
|
1465
|
+
return { valid: false, reason: `refused pend: this member refused action ${commit.actionId}'s pend and ${corroboration}` };
|
|
1466
|
+
}
|
|
1467
|
+
return { valid: true };
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Condition (b) of {@link validateCommitAgainstRefusedPend}: does local storage still back up a
|
|
1471
|
+
* retained pend refusal for this commit? Returns a short prose fragment naming what corroborates
|
|
1472
|
+
* it, or `undefined` for "no longer corroborated — abstain" (which a read fault also yields).
|
|
1473
|
+
*/
|
|
1474
|
+
async refusedPendConflictsWithLocalState(commit) {
|
|
1475
|
+
let blockResults;
|
|
1476
|
+
try {
|
|
1477
|
+
blockResults = await this.storageRepo.get({ blockIds: commit.blockIds });
|
|
1478
|
+
}
|
|
1479
|
+
catch (err) {
|
|
1480
|
+
log('cluster-member:refused-pend-read-error', {
|
|
1481
|
+
actionId: commit.actionId,
|
|
1482
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1483
|
+
});
|
|
1484
|
+
return undefined; // a local read fault is an abstain, never an escape out of the vote path
|
|
1485
|
+
}
|
|
1486
|
+
for (const blockId of commit.blockIds) {
|
|
1487
|
+
const state = blockResults[blockId]?.state;
|
|
1488
|
+
const rivals = (state?.pendings ?? []).filter(actionId => actionId !== commit.actionId);
|
|
1489
|
+
if (rivals.length > 0) {
|
|
1490
|
+
return `block ${blockId} is still held by unresolved action(s) ${rivals.join(', ')}`;
|
|
1491
|
+
}
|
|
1492
|
+
const latest = state?.latest;
|
|
1493
|
+
if (latest?.rev === commit.rev && !isOwnRevision(latest, commit.rev, commit.actionId)) {
|
|
1494
|
+
return `block ${blockId} rev ${commit.rev} is already committed by a different action`;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1352
1499
|
/**
|
|
1353
1500
|
* Promise-round check of a commit record's declared content digests
|
|
1354
1501
|
* (`CommitRequest.blockDigests`) against what this member's OWN pended copy of each transform
|
|
@@ -1365,9 +1512,11 @@ export class ClusterMember {
|
|
|
1365
1512
|
* authored, delivered at pend — a hostile declarer cannot force or dodge a check by mis-declaring
|
|
1366
1513
|
* `baseRev`):
|
|
1367
1514
|
* - transform carries an `insert` → base-independent, ALWAYS check (declared `baseRev` ignored);
|
|
1368
|
-
* - `updates` only → check iff this member's local base rev equals the declared `baseRev
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1515
|
+
* - `updates` only → check iff this member's local base rev equals the declared `baseRev`. A
|
|
1516
|
+
* member whose base does not match cannot JUDGE the content — it would preview the transform
|
|
1517
|
+
* against different bytes than the declarer used — so it abstains here. It never goes on to
|
|
1518
|
+
* materialize those bytes: `StorageRepo.internalCommit` refuses that same mismatch at apply
|
|
1519
|
+
* and reconciles from a cohort peer;
|
|
1371
1520
|
* - `delete` only / no base / unmaterializable base → materializes nothing to compare, abstain;
|
|
1372
1521
|
* - no pending transform for the action (this member never saw the pend) → abstain.
|
|
1373
1522
|
* "Abstain" = contribute no content attestation: approve exactly as before this check existed.
|
|
@@ -1536,9 +1685,15 @@ export class ClusterMember {
|
|
|
1536
1685
|
// divergence is absorbed inside applyConsensusOperation and never reaches here.
|
|
1537
1686
|
// A retained pend verdict rolls back with the marker: it belongs to an apply
|
|
1538
1687
|
// that is now considered not-executed, and a re-run will retain a fresh one.
|
|
1688
|
+
// The actionId-keyed refusals for this record's pend operations go with it — leaving
|
|
1689
|
+
// one behind would let a rolled-back apply veto that action's commit forever.
|
|
1539
1690
|
this.executedTransactions.delete(record.messageHash);
|
|
1540
1691
|
this.executedPendResults.delete(record.messageHash);
|
|
1541
1692
|
this.executedCommitResults.delete(record.messageHash);
|
|
1693
|
+
for (const operation of record.message.operations) {
|
|
1694
|
+
if ('pend' in operation)
|
|
1695
|
+
this.refusedPendActions.delete(operation.pend.actionId);
|
|
1696
|
+
}
|
|
1542
1697
|
throw err;
|
|
1543
1698
|
}
|
|
1544
1699
|
// Persist the durable marker only now that apply has actually succeeded. Writing it
|
|
@@ -1615,6 +1770,14 @@ export class ClusterMember {
|
|
|
1615
1770
|
// already committed) is the optimistic-concurrency verdict, and swallowing it acknowledged
|
|
1616
1771
|
// writes that no member stored.
|
|
1617
1772
|
this.executedPendResults.set(messageHash, result);
|
|
1773
|
+
// Second, actionId-keyed retention for the commit tier: this same refusal must still be
|
|
1774
|
+
// findable when THIS action's commit record (a different messageHash) arrives for a
|
|
1775
|
+
// promise vote, so a member cannot co-sign the commit of a pend it just refused. Only
|
|
1776
|
+
// conflict-shaped refusals are kept — a bare-reason fault is local trouble, not evidence
|
|
1777
|
+
// that a rival holds the blocks, and must not veto the cohort's commit.
|
|
1778
|
+
if (!result.success && isConflictFailure(result)) {
|
|
1779
|
+
this.refusedPendActions.set(operation.pend.actionId, this.now());
|
|
1780
|
+
}
|
|
1618
1781
|
if (!result.success) {
|
|
1619
1782
|
log('cluster-member:consensus-pend-diverged', {
|
|
1620
1783
|
messageHash,
|
|
@@ -1706,10 +1869,12 @@ export class ClusterMember {
|
|
|
1706
1869
|
// lets the coordinating node convert its OWN member's rival-confirmed refusal into a
|
|
1707
1870
|
// conflict answer — that last guard is what closes the signed-but-not-yet-applied
|
|
1708
1871
|
// window, where two commits for one revision both assemble consensus because signing
|
|
1709
|
-
// drops each member's reservation before applying advances its storage
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1872
|
+
// drops each member's reservation before applying advances its storage —
|
|
1873
|
+
// `validateCommitAgainstRefusedPend` now refuses to sign that commit in the first
|
|
1874
|
+
// place on any member that refused its pend. If consensus-without-durability is ever
|
|
1875
|
+
// observed again, look at those guards' remaining abstain residuals (capability-less
|
|
1876
|
+
// or history-truncated storage; a member whose refusal has aged out of retention),
|
|
1877
|
+
// not at this branch.
|
|
1713
1878
|
if (result.missing?.length) {
|
|
1714
1879
|
log('cluster-member:consensus-commit-diverged', {
|
|
1715
1880
|
messageHash,
|
|
@@ -2042,6 +2207,12 @@ export class ClusterMember {
|
|
|
2042
2207
|
this.executedCommitResults.delete(messageHash);
|
|
2043
2208
|
}
|
|
2044
2209
|
}
|
|
2210
|
+
// Prune actionId-keyed pend refusals on the same TTL as the verdicts they were derived from.
|
|
2211
|
+
for (const [actionId, refusedAt] of Array.from(this.refusedPendActions.entries())) {
|
|
2212
|
+
if (refusedAt < expirationThreshold) {
|
|
2213
|
+
this.refusedPendActions.delete(actionId);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2045
2216
|
// Prune old applied-invalidation dedup markers on the same TTL.
|
|
2046
2217
|
for (const [dedupKey, appliedAt] of Array.from(this.appliedInvalidations.entries())) {
|
|
2047
2218
|
if (appliedAt < expirationThreshold) {
|