Add Vortex DSE specifications - #220
Conversation
| /\ MaxSlot = 1 | ||
|
|
||
| \* Conjunction of every safety invariant in the module. | ||
| AllInv == |
There was a problem hiding this comment.
Isn’t it better if Apalache reports the name of the invariant that’s been violated?
| \* @type: Set(Str); | ||
| MsgIDs, \* finite set of distinct message identifiers | ||
| \* @type: Int; | ||
| MaxSlot \* slot horizon (state-space bound) |
There was a problem hiding this comment.
MaxSlot is a model-checking concern, so it doesn’t belong in the high-level specification. Instead, TLC should be used to box the state space by using state and action constraints.
|
If any part of this commit was generated with AI assistance, please add a |
@ahelwer ^^ |
| MsgIDs = {m1} | ||
| MaxSlot = 1 | ||
|
|
||
| PROPERTIES |
There was a problem hiding this comment.
Careful when checking liveness properties under state- and action-constraints.
| Process(n, m) == | ||
| /\ n \in Nodes | ||
| /\ m \in network | ||
| /\ node_state[n] = "up" |
There was a problem hiding this comment.
nit: consider extracting strings like "up", ... into constants.
| /\ UNCHANGED <<current_slot, processed, persisted, node_state>> | ||
|
|
||
| \* Slot ticker advances by 1. | ||
| Tick == |
There was a problem hiding this comment.
| ------------------------------------------------------------------------------- | ||
| (* STATE-SPACE CONSTRAINT *) | ||
|
|
||
| StateConstraint == |
There was a problem hiding this comment.
This and the constant MaxSlot should be pushed into one of the MC_...tla files.
| (***************************************************************************) | ||
|
|
||
| Fairness == | ||
| /\ SF_vars(Tick) |
|
|
||
| \* L1 TICK PROGRESS. | ||
| \* Under SF(Tick), the slot counter eventually reaches the horizon. | ||
| TickProgress == <>(current_slot = MaxSlot) |
There was a problem hiding this comment.
This could be strengthened to <>[], but MaxSlot should be here anyway.
| (***************************************************************************) | ||
| (* Vortex DSE — Agreement Extension Layer (L4) *) | ||
| (* *) | ||
| (* Companion module to Vortex_DSE_CSlot.tla. The core module models the *) |
There was a problem hiding this comment.
Should this module refine the other (under some suitable refinement mapping)?
50377ce to
15b922d
Compare
|
Thank you — this was a more careful read than I expected, and the
Also done: On refinement — a fair question, and the answer today is no. The obstacle is not the admission rule. It is that They are complementary views rather than stacked layers: the core is accumulation across slots under crash, AE is one slot's agreement cycle with a reset at the boundary. The README now says as much instead of implying composition. A module that does cross-cut both is the right thing to have, and I intend to write it — but as its own contribution, with a proved mapping, rather than asserted here. Seven TLC models, all under five seconds. The three proof modules discharge 191, 128 and 32 obligations and exit 0 under |
|
The macOS failure here is not from this change. It proved fine in the other five jobs, the proof step runs sequentially so the modules added here do not compete for CPU, and they take about three seconds between them. That budget is one I set in #218, and it is too tight — locally the proof takes 86 seconds, while the macOS runner needed more than 2.8x that. #221 raises it to 6 minutes. |
d778fc1 to
ac2cea6
Compare
|
Added the |
ac2cea6 to
e2e49aa
Compare
|
Build problem appears to be caused by apalache-mc/apalache#3432 (comment) |
|
FWIW: https://github.com/tlaplus/vscode-tlaplus/blob/master/resources/knowledgebase/tla-review-guidelines.md might be useful for your agentic workflow |
|
The red here is not from this branch. Apalache is not starting on the runners. This branch additionally hits Nothing under |
|
Thanks for the guidelines link — I ran the specs against it and four things were not being followed. Constants had no assumptions. All four specifications declared Type invariants are back to the Strong fairness on Process is now justified by measurement rather than assertion. I tried weak fairness, as the guidelines ask: the liveness model reports a temporal-property violation under it, because a crash intermittently disables One deviation, deliberate. The guidelines prefer a state Seven TLC models, all under five seconds; the three proof modules discharge 191, 128 and 32 obligations and exit 0 under |
|
The refinement module is written and proved, on a branch stacked on this one. It keeps the core's variables, lets I will open it as its own pull request once this one lands, rather than growing this one further. |
|
master is green again |
Deterministic slot-based admission with a separate per-slot agreement layer: a message carries its own slot stamp and each node decides admission locally, with no leader, quorum or vote. Two admission modes are specified because both are implemented: the default admits a late message into its own slot, while the opt-in TTL mode rejects it permanently to bound memory. Vortex_DSE_CSlot_AE is specified over the TTL mode; the README says so rather than implying it composes with the default. Six TLC models, all completing in under four seconds. Three TLAPS modules discharging 194, 131 and 34 obligations, all exit 0 under tlapm --strict with no OMITTED steps. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasilis Nasopoulos <vasilis_nasopoulos@hotmail.com>
MaxSlot was a state-space horizon living in the specifications. It is now gone from all four: Tick and NextCslot are unbounded, and the adversary may forge any slot in Nat. Each specification gets an MC_ module that fixes the horizon inside the actions rather than through a CONSTRAINT, so the state graph is finite instead of truncated — under a state constraint TLC drops successor states, which is unsound for the temporal properties. MaxSkew stays in Vortex_DSE_CSlot_Skew: it is an assumption the protocol relies on, not a checking artifact. Also: WF is enough for the now-unbounded ticker, so SF_vars(Tick) becomes WF_vars(Tick); TickProgress is stated unboundedly in the specifications and strengthened to <>[] in the MC modules, where the horizon makes it permanent; node states and AE phases are named rather than written as bare strings; and the AE harness no longer bundles its invariants into a single conjunction, so a checker reports which one failed. Type invariants now constrain the network elementwise, since MsgRecord ranges over Nat and cannot be enumerated. Seven TLC models, all completing in under five seconds. The three TLAPS modules discharge 191, 128 and 32 obligations and exit 0 under tlapm --strict. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasilis Nasopoulos <vasilis_nasopoulos@hotmail.com>
Four things in the review guidelines were not being followed. Constants now carry named assumptions. All four specifications declared Nodes and MsgIDs with nothing constraining them, and Skew's MaxSkew with nothing saying it is a natural number. The guidelines call this crucial for TLAPS, and these modules do carry proofs. Type invariants are back to the var \in S form: network \subseteq MsgRecord rather than a quantifier over its elements. MsgRecord ranges over Nat and so cannot be enumerated, which is why the MC module carries its own bounded MCTypeInvariant for TLC. Strong fairness on Process is now justified by measurement rather than assertion. Weak fairness was tried, as the guidelines ask: the liveness model reports a temporal-property violation under it, because a crash intermittently disables Process. The module says so. The horizon stays inside MCTick rather than moving to a state CONSTRAINT, which the guidelines otherwise prefer. A constraint was tried first and does not work here: TLC evaluates invariants on the state that crosses the boundary before the constraint discards it, so with MaxSlot = 2 a Tick reaches current_slot = 3 and any invariant mentioning the horizon fails there. The reason is recorded in the module header rather than left as an unexplained deviation. Seven TLC models, all completing in under five seconds. The three TLAPS modules discharge 191, 128 and 32 obligations and exit 0 under tlapm --strict. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasilis Nasopoulos <vasilis_nasopoulos@hotmail.com>
c4266ee to
9b3d8a9
Compare
Vortex_DSE_CSlot_AE is not a refinement of anything, as noted on tlaplus#220: its NextCslot clears processed for every node at a slot boundary, which no core action can do, and it models no crash. Calling it a layer on top of the core was an overstatement. Vortex_DSE_CSlot_AE_Refinement is the same Freeze / Reconcile / Commit cycle written so that it is one. It carries the core's variables unchanged, lets processed accumulate, and keeps the per-slot input set in committed rather than recovering it by wiping processed. Every action is either a core action or leaves the core's variables alone: Submit, DuplicateInject are the core's Process is the core's, additionally gated on Open, and with the stronger equality gate NextCslot is the core's Tick Freeze, Reconcile change only phase and committed so THEOREM Refinement == Spec => C!Spec holds under the identity mapping on the core's variables. Proved in Vortex_DSE_CSlot_AE_Refinement_Proofs, 25 obligations, exit 0 under tlapm --strict. Crash and Rejoin are absent, which costs nothing: refinement asks that every behaviour of this module be a behaviour of the core, not the reverse. TLC checks the refinement as well, against the core's next-state relation with the forged slot bounded, since the core quantifies it over Nat: 103805 states, no error, alongside the four safety invariants. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasilis Nasopoulos <vasilis_nasopoulos@hotmail.com>
muenchnerkindl
left a comment
There was a problem hiding this comment.
I'm a little late to the party, but perhaps you'll find my comments helpful nevertheless.
| \* Adversarial duplicate / replay injection. | ||
| \* Attacker injects a message with arbitrary cslot value (past, present, | ||
| \* or future). The C-slot gate must still hold. | ||
| DuplicateInject(id, fake_cslot) == |
There was a problem hiding this comment.
This action clearly subsumes Submit(id), why do you have both – in particular given that there is no fairness constraint on Submit?
|
|
||
| \* I1: EXACTLY-ONCE PER NODE. | ||
| \* No node processes the same id twice (set semantics + guard). | ||
| ExactlyOncePerNode == |
There was a problem hiding this comment.
This is a direct consequence of the type invariant. I think you even have the stronger invariant
\A n \in Nodes : processed[n] \subseteq {m.id : m \in network}
which states that only sent messages are processed.
| \* [0, current_slot]: a real, present-or-past slot, never future-dated. | ||
| \* Late messages (cslot < current_slot) ARE admitted here (into their | ||
| \* own slot) — that is intended; only future-dated admission is barred. | ||
| NoFutureAdmission == |
There was a problem hiding this comment.
NB: The invariant I suggested above is a consequence of this one.
| \* mmap snapshot never invents ids that were not in the network. | ||
| PersistedReflectsReality == | ||
| \A n \in Nodes : | ||
| node_state[n] = Down => |
There was a problem hiding this comment.
Why is this asserted only when the node is down? It should hold at all times?
|
|
||
| \* I4: NO PHANTOM PROCESS. | ||
| \* Every processed id corresponds to a real network record. | ||
| NoPhantomProcess == |
There was a problem hiding this comment.
That's what I suggested above, and it immediately follows from NoFutureAdmission.
|
|
||
| TypeInvariant == | ||
| /\ node_slot \in [Nodes -> Nat] | ||
| /\ \A m \in network : m.id \in MsgIDs /\ m.cslot \in Nat /\ m.origin \in Nodes |
There was a problem hiding this comment.
This should rather be stated as network \in SUBSET MsgRecord. In fact, the semantics of TLA+ is such that this is not implied by the predicate asserted here.
| /\ node_slot[n1] - node_slot[n2] <= MaxSkew | ||
| /\ node_slot[n2] - node_slot[n1] <= MaxSkew | ||
|
|
||
| ExactlyOncePerNode == |
There was a problem hiding this comment.
Cf. my comment to Vortex_DSE_CSlot.tla: this one is a direct consequence of the type invariant.
|
|
||
| PersistedReflectsReality == | ||
| \A n \in Nodes : | ||
| node_state[n] = Down => |
There was a problem hiding this comment.
Again, probably no need for this condition.
| node_state[n] = Down => | ||
| persisted[n] \subseteq {m.id : m \in network} | ||
|
|
||
| NoPhantomProcess == |
There was a problem hiding this comment.
Immediate consequence of CSlotLocalAdmission.
| /\ m \in network | ||
| /\ node_state[n] = Up | ||
| /\ m.id \notin processed[n] \* exactly-once guard (local) | ||
| /\ m.cslot = current_slot \* STRICT C-slot equality |
There was a problem hiding this comment.
If I'm not mistaken, the only difference to Vortex_DSE_CSlot.tla is that here equality is required whereas the other spec has <=. Therefore, all safety properties verified for the other spec also hold for this one. Moreover, the only liveness properties stated here are independent of the processing mechanism (and indeed, liveness for sent messages would not hold here because a message received late will not be accepted anymore). So, I wonder if this spec is of real interest?
Proofs first. NextType, NextSafe and NextStrictExactlyOnce were case splits where one BY DEF suffices, and StrictExactlyOnceCorrect carried the type invariant through as a conjunct instead of appealing to the theorem that establishes it. Applying the suggested pattern: Vortex_DSE_CSlot_Proofs 263 -> 88 lines, 191 -> 23 obligations Vortex_DSE_CSlot_ExactlyOnce_Proof 295 -> 131 lines, 128 -> 19 Vortex_DSE_CSlot_AE_Proofs 60 -> 36 lines, 32 -> 10 All still exit 0 under tlapm --strict. Submit and DuplicateInject are now one Send action. They differed only in preconditions that nothing relied on, and with no fairness on either the split distinguished nothing. The invariant sections stated corollaries as though they were independent results. Each module now names the property of interest and marks the rest as following from it: NoFutureAdmission in the core, and in the agreement layer the pair ProcessedAreCurrentSlot and CommittedIsUnion, which were the two facts suggested in review. MerkleAgreement, CommittedSupersetsProcessed and NoPhantomInCommitted follow from those, and are checked as regressions rather than claimed as separate strength. PersistedReflectsReality was asserted only while a node is down. It holds at all times; the guard is gone, in the core, TTL and Skew. Skew: network is typed as `network \in SUBSET MsgRecord` rather than by a quantifier over its elements, which does not imply membership. The origin field was written and never read, so spoofing it modelled nothing; it is removed along with the ByzantineInject parameter that set it. AE: weak fairness for Reconcile and NextCslot, confirmed sufficient by the liveness model. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasilis Nasopoulos <vasilis_nasopoulos@hotmail.com>
|
All of it applied — thank you, this was a much closer read than I had any right to expect. The proofs. You were right that they collapse.
Seven times more obligations than the argument needed.
The invariant sections. Each module now names the property of interest and marks the rest as following from it, rather than presenting a flat list that reads as more strength than is there. In the agreement layer I took the two facts you gave —
Skew. AE fairness — weak is enough for both, confirmed against the liveness model. On the global Seven TLC models and three proof modules, all green locally. |
The observation was right: the strict mode differed from the default only in requiring equality where the default admits on <=, so its safety properties were already established elsewhere, and the liveness it stated was independent of the admission mechanism. As written it added nothing. Both halves of that are now said in the specification rather than left for a reader to notice. Vortex_DSE_CSlot_TTL_Proofs proves Spec => C!Spec: the strict mode refines the default one under the identity mapping, so safety is inherited rather than reproved. The type invariant is needed in exactly one place — going from m.cslot = current_slot to m.cslot <= current_slot requires knowing both are naturals — and nowhere else. 34 obligations. MC_Vortex_DSE_CSlot_TTL_admission.cfg records what the mode gives up. EventualAdmission holds under the default rule and is violated here, because a message whose slot has passed is refused for good. It is registered as a liveness failure, which is the point: the strict rule is a concession to bounded memory, and the cost is now checked rather than described in a comment. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Vasilis Nasopoulos <vasilis_nasopoulos@hotmail.com>
|
You are right on both counts, and as written the module did not earn its place. Rather than argue for it, I have made it say the two things you pointed at. The relationship is now proved, not asserted. And the loss is now checked. That, I think, is the only reason for the module to exist: not that the strict rule is interesting on its own, but that the pair makes the trade-off explicit. The default admits late messages and keeps eventual admission; the strict rule bounds memory and gives it up. Neither dominates, and now neither claim rests on prose. If you would still rather the directory carried one admission rule, I am happy to drop it — the refinement proof would then move into the core module's README as a remark. |
|
Thank you, LGTM. |
| @@ -0,0 +1,96 @@ | |||
| # Vortex DSE — C-slot admission and per-slot agreement | |||
|
|
|||
| Vortex DSE is a deterministic consensus protocol in which a message carries its | |||
There was a problem hiding this comment.
"deterministic" doesn't seem to be needed here.
| and cross-node agreement on the per-slot input set is established afterwards by | ||
| a separate layer. | ||
|
|
||
| These specifications model that structure. They are the formal counterpart of a |
There was a problem hiding this comment.
What is “that structure”? Is it the “separate layer” or the part that has a very high-level characterization above?
In general, the README doesn’t help readers understand Vortex DSE’s conceptual functioning. Most of the other examples in this repository are accompanied by primarily academic publications. It would be beneficial for this example to also state the problem and how the solution works. The TLA+ specifications provide the formal proof that the solution solves the problem.
| scope here. | ||
|
|
||
| Source repositories, including the whitepaper and the model-checking logs: | ||
| <https://github.com/vasilisnasopoulos> |
|
Since I lack the time to familiarize myself with Vortex DSE, I sought a review from a frontier model. Here’s the unfiltered response:
|
|
You are right on every point, and the framing you quote is better than mine. Where the overstatement came from. The specifications in this directory are one part of a much larger private set — a bit over a hundred modules and around a hundred and twenty model configurations, which between them cover the things you list as missing: per-node clocks and bounded skew, lossy delivery, equivocation and accountability, crash and rejoin composed with agreement, the timing layer that keeps the slot boundary. I chose to publish this part and keep the rest private, and then wrote the README as though it described the whole thing. That is how "consensus" ended up in the first sentence of a directory that does not model consensus. I am not offering the private material as evidence — you cannot review what you cannot read, and a reviewer should not be asked to take a claim on trust. I mention it only because it explains the mistake, and because it is the honest answer to "what is that structure": it is the structure of a system most of which is not here. So, for what is here: slot admission plus idealized set reconciliation. The README will say that and nothing larger. The specific points all hold:
On "that structure": the sentence pointed at nothing definite, and it is going. On the README more generally — you are right that the other examples come with a paper and this one does not. I will rewrite it to state the problem, how the solution works conceptually, and explicitly what these modules do not cover, so a reader can tell what is proved and what is left open. I would rather that be plain than have someone infer a stronger result from one word. |
Adds
specifications/VortexDSE/— a deterministic slot-based admissionprotocol with a separate per-slot agreement layer. A message carries its own
slot stamp and each node decides admission locally against its own clock;
there is no leader, quorum or vote.
Two admission modes, both specified because both are implemented.
Vortex_DSE_CSlotadmits a late message into its own slot, so nothing isdropped.
Vortex_DSE_CSlot_TTLrejects it permanently, which bounds memorybehind the frontier. The difference is
<=versus=in one conjunct.Vortex_DSE_CSlot_AE(Freeze / Reconcile / Commit) is specified over the TTLmode, not the default. Its
NoReorderAcrossCslotinvariant encodes the strictgate, so it is not a refinement of the late-tolerant rule; extending it would
mean restating what "no reordering across slots" means, and I have not done
that here. The spec README says this rather than leaving it to be discovered.
Checked:
success, 0.6–3.3sVortex_DSE_CSlot_ProofsVortex_DSE_CSlot_ExactlyOnce_ProofVortex_DSE_CSlot_AE_ProofsAll three proof modules exit 0 under
tlapm --strictand contain noOMITTEDsteps. Model runtimes and state counts in
manifest.jsonwere measured withcheck_small_models.pylocally;check_manifest_schema,check_manifest_files,check_manifest_featuresandcheck_markdown_tableall pass.Scope is deliberately limited:
Vortex_DSE_CSlot_Skewstates the bounded-skewassumption structurally but does not model the mechanism that maintains it, and
Reconcileis a single atomic step rather than the multi-round protocolunderneath.
Two notes from preparing this, neither blocking:
generate_manifest.pyrewrites every spec'smanifest.json, not only the newone; it produced diffs in 14 unrelated manifests, which I reverted. Only
specifications/VortexDSE/and the README table are touched here.check_proofs.pyreadsmodule['proof']['runtime'], but the schema requiresmaxRuntimeMinutesand setsadditionalProperties: false. It raisesKeyError: 'runtime'on any spec with a proof, including on a cleanmaster.CI is unaffected because it invokes
tlapmdirectly. Happy to open a separateissue or PR for it if useful.