Skip to content

Add Vortex DSE specifications - #220

Open
vasilisnasopoulos wants to merge 5 commits into
tlaplus:masterfrom
vasilisnasopoulos:vortex-dse
Open

Add Vortex DSE specifications#220
vasilisnasopoulos wants to merge 5 commits into
tlaplus:masterfrom
vasilisnasopoulos:vortex-dse

Conversation

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor

Adds specifications/VortexDSE/ — a deterministic slot-based admission
protocol 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_CSlot admits a late message into its own slot, so nothing is
dropped. Vortex_DSE_CSlot_TTL rejects it permanently, which bounds memory
behind the frontier. The difference is <= versus = in one conjunct.

Vortex_DSE_CSlot_AE (Freeze / Reconcile / Commit) is specified over the TTL
mode, not the default. Its NoReorderAcrossCslot invariant encodes the strict
gate, 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:

TLC models 6, all success, 0.6–3.3s
Vortex_DSE_CSlot_Proofs 194 obligations
Vortex_DSE_CSlot_ExactlyOnce_Proof 131 obligations
Vortex_DSE_CSlot_AE_Proofs 34 obligations

All three proof modules exit 0 under tlapm --strict and contain no OMITTED
steps. Model runtimes and state counts in manifest.json were measured with
check_small_models.py locally; check_manifest_schema, check_manifest_files,
check_manifest_features and check_markdown_table all pass.

Scope is deliberately limited: Vortex_DSE_CSlot_Skew states the bounded-skew
assumption structurally but does not model the mechanism that maintains it, and
Reconcile is a single atomic step rather than the multi-round protocol
underneath.

Two notes from preparing this, neither blocking:

  • generate_manifest.py rewrites every spec's manifest.json, not only the new
    one; it produced diffs in 14 unrelated manifests, which I reverted. Only
    specifications/VortexDSE/ and the README table are touched here.
  • check_proofs.py reads module['proof']['runtime'], but the schema requires
    maxRuntimeMinutes and sets additionalProperties: false. It raises
    KeyError: 'runtime' on any spec with a proof, including on a clean master.
    CI is unaffected because it invokes tlapm directly. Happy to open a separate
    issue or PR for it if useful.

/\ MaxSlot = 1

\* Conjunction of every safety invariant in the module.
AllInv ==

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@lemmy

lemmy commented Aug 4, 2026

Copy link
Copy Markdown
Member

If any part of this commit was generated with AI assistance, please add a Co-authored-by footer naming the model to the commit message. AI use is permitted; we simply ask that it be disclosed for transparency.

@lemmy

lemmy commented Aug 4, 2026

Copy link
Copy Markdown
Member

Two notes from preparing this, neither blocking:

  • generate_manifest.py rewrites every spec's manifest.json, not only the new
    one; it produced diffs in 14 unrelated manifests, which I reverted. Only
    specifications/VortexDSE/ and the README table are touched here.
  • check_proofs.py reads module['proof']['runtime'], but the schema requires
    maxRuntimeMinutes and sets additionalProperties: false. It raises
    KeyError: 'runtime' on any spec with a proof, including on a clean master.
    CI is unaffected because it invokes tlapm directly. Happy to open a separate
    issue or PR for it if useful.

@ahelwer ^^

MsgIDs = {m1}
MaxSlot = 1

PROPERTIES

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful when checking liveness properties under state- and action-constraints.

Process(n, m) ==
/\ n \in Nodes
/\ m \in network
/\ node_state[n] = "up"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider extracting strings like "up", ... into constants.

/\ UNCHANGED <<current_slot, processed, persisted, node_state>>

\* Slot ticker advances by 1.
Tick ==

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-------------------------------------------------------------------------------
(* STATE-SPACE CONSTRAINT *)

StateConstraint ==

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the constant MaxSlot should be pushed into one of the MC_...tla files.

(***************************************************************************)

Fairness ==
/\ SF_vars(Tick)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WF_ should be sufficient.


\* L1 TICK PROGRESS.
\* Under SF(Tick), the slot counter eventually reaches the horizon.
TickProgress == <>(current_slot = MaxSlot)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 *)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this module refine the other (under some suitable refinement mapping)?

@vasilisnasopoulos
vasilisnasopoulos force-pushed the vortex-dse branch 3 times, most recently from 50377ce to 15b922d Compare August 5, 2026 10:32
@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

Thank you — this was a more careful read than I expected, and the MaxSlot point was right.

MaxSlot is gone from all four specifications. Each now has an MC_ module that fixes the horizon inside the actions rather than through a CONSTRAINT, which also answers the liveness warning: no successor states are discarded, so the temporal properties are checked against a genuinely finite graph rather than a truncated one. There is no CONSTRAINT left anywhere in the directory. MaxSkew stays in the Skew module, since it is an assumption the protocol relies on rather than a checking artifact.

FiniteMonotonic was the useful pointer — the MC_ modules follow its shape.

Also done: WF_vars(Tick) now that the ticker is unbounded; TickProgress strengthened to <>[] in the MC modules, where the horizon makes it permanent; node states and AE phases named rather than written as bare strings; and AllInv dropped so a checker reports which invariant failed.

On refinement — a fair question, and the answer today is no.

The obstacle is not the admission rule. It is that Vortex_DSE_CSlot_AE does something neither core module can: NextCslot clears processed for every node at once at a slot boundary. In the core specifications processed only grows, except when a single node crashes and goes down. So AE behaviours are not core behaviours under any mapping that hides phase and committed_set. The two also model disjoint failure dimensions — the core has crash and rejoin, AE has neither, deliberately.

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 tlapm --strict.

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

The macOS failure here is not from this change. EWD998_proof.tla hit its 4-minute wall in the shared proof step:

54195 Killed: 9  timeout --signal=KILL 4m ... EWD998_proof.tla

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.

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

Added the Co-authored-by footer to both commits here, and to #221. Claude Opus 5 throughout — writing the module and README text, running the models and proofs, and preparing the manifest.

@lemmy

lemmy commented Aug 5, 2026

Copy link
Copy Markdown
Member

Build problem appears to be caused by apalache-mc/apalache#3432 (comment)

@lemmy

lemmy commented Aug 5, 2026

Copy link
Copy Markdown
Member

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

The red here is not from this branch. master itself is failing all six jobs right now — run 31018433496 — for the same reasons:

specifications/GermanProtocol/APGermanData.cfg      expected success, got 1
specifications/FlashProtocol/APFlashWithMutex.cfg   expected success, got 1
Error: Could not create the Java Virtual Machine.

Apalache is not starting on the runners. This branch additionally hits smoke_test_large_models.py, where TLC reports SmokeEWD998.cfg: File not found although the file is present in the tree — a working-directory problem rather than a missing file.

Nothing under specifications/VortexDSE/ appears in either failure, and its own models and proofs pass in every job.

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

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 Nodes and MsgIDs with nothing constraining them, and Skew's MaxSkew with nothing saying it is a natural number. Named assumptions are in now. Worth having here in particular, since these modules carry TLAPS 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 cannot be enumerated, so the MC module carries a bounded MCTypeInvariant for TLC instead of the specification bending to suit it.

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 Process. That is recorded next to the definition.

One deviation, deliberate. The guidelines prefer a state CONSTRAINT over enablement conditions, and I moved the horizon into MCTick instead. I tried the constraint first and it 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 in the module header rather than left unexplained. Happy to be told there is a better way around it.

Seven TLC models, all under five seconds; the three proof modules discharge 191, 128 and 32 obligations and exit 0 under tlapm --strict.

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

The refinement module is written and proved, on a branch stacked on this one. It keeps the core's variables, lets processed accumulate rather than clearing it at a slot boundary, and holds the per-slot input set in committed, so THEOREM Refinement == Spec => C!Spec goes through under the identity mapping — 25 obligations, exit 0 under tlapm --strict, and TLC checks it as a temporal property as well.

I will open it as its own pull request once this one lands, rather than growing this one further.

@lemmy

lemmy commented Aug 5, 2026

Copy link
Copy Markdown
Member

master is green again

vasilisnasopoulos and others added 3 commits August 5, 2026 22:13
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>
vasilisnasopoulos added a commit to vasilisnasopoulos/Examples that referenced this pull request Aug 5, 2026
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 muenchnerkindl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) ==

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ==

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ==

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ==

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ==

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, probably no need for this condition.

node_state[n] = Down =>
persisted[n] \subseteq {m.id : m \in network}

NoPhantomProcess ==

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

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. NextType, NextSafe and NextStrictExactlyOnce were case splits where one BY DEF does the job, and StrictExactlyOnceCorrect was carrying the type invariant through as a conjunct instead of appealing to TypeCorrect, which is exactly the pattern you described:

lines obligations
Vortex_DSE_CSlot_Proofs 263 → 88 191 → 23
Vortex_DSE_CSlot_ExactlyOnce_Proof 295 → 131 128 → 19
Vortex_DSE_CSlot_AE_Proofs 60 → 36 32 → 10

Seven times more obligations than the argument needed.

Submit and DuplicateInject are now one Send. They differed only in preconditions nothing relied on, and with no fairness on either the split distinguished nothing — as you say.

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 — ProcessedAreCurrentSlot and CommittedIsUnion — as the pair everything else derives from, with intermediate definitions as suggested. The corollaries stay defined and checked, but as regressions rather than claims.

PersistedReflectsReality — no reason at all; it holds unconditionally, verified, and the guard is gone in all three modules.

Skew. network \in SUBSET MsgRecord now, and thank you for being precise about why the quantified form does not imply it. The origin field was written and never read, so an adversary spoofing it changed nothing observable — it is removed rather than left looking like modelled adversarial power.

AE fairness — weak is enough for both, confirmed against the liveness model.

On the global current_slot: Vortex_DSE_CSlot_Skew is that weakening, with node_slot[n] per node and a bounded-drift tick. It is a sibling module rather than a refinement of the core, which is probably why it did not read as an answer to the point.

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>
@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

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. Vortex_DSE_CSlot_TTL_Proofs establishes Spec => C!Spec: the strict mode refines the default one under the identity mapping, so every safety property of the default is inherited and none of it is reproved. Your observation that equality is the only difference is exactly what the proof turns on — and the type invariant is needed in precisely one place, going from m.cslot = current_slot to m.cslot <= current_slot, since that step needs both to be naturals. 34 obligations.

And the loss is now checked. MC_Vortex_DSE_CSlot_TTL_admission.cfg states EventualAdmission and registers it as a liveness failure, which is the point of the module: a message whose slot has passed is refused for good. That property holds under the default rule and does not hold here, so the difference between the two modes is a machine-checked fact rather than a sentence in a comment.

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.

@muenchnerkindl

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not deep links?

@lemmy

lemmy commented Aug 6, 2026

Copy link
Copy Markdown
Member

Since I lack the time to familiarize myself with Vortex DSE, I sought a review from a frontier model. Here’s the unfiltered response:

AI-based assessment: The specs model a meaningful but narrow component: local time-slot admission, duplicate filtering, abstract crash/rejoin state, and the difference between late-tolerant and strict admission. They do not yet model a meaningful consensus system. The project’s own public documentation similarly describes C-slot as a local admission predicate rather than consensus and says the admission-to-agreement system is not formally composed.

The core uses one globally visible network set and records only processed message IDs. It therefore does not represent separate per-node delivery state, selective loss or equivocation, conflicting payloads, within-slot ordering, or replicated application state. The agreement module’s Reconcile action atomically assigns the union of every node’s processed set to all nodes, so agreement is assumed by the action rather than established by a distributed reconciliation protocol.

The TTL model also never deletes network, processed, or persisted state, so its bounded-memory motivation is not actually modeled.

Recommendation: include it only if it is framed as slot admission plus idealized set reconciliation. Keeping the consensus protocol framing should require a composed model with per-node channels, loss/equivocation and crashes during reconciliation, persistent ordered history or state-machine effects, garbage collection, and end-to-end agreement, validity, and termination properties.

@vasilisnasopoulos

Copy link
Copy Markdown
Contributor Author

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:

  • One global network set. No per-node delivery state, no selective loss, no conflicting payloads for one id, no within-slot ordering, no replicated application state.
  • Reconcile assigns the union in a single atomic step. Agreement is a property of that action, not something a reconciliation protocol establishes. I wrote as much in the scope note and then titled the module an agreement layer, which reads as more than it is.
  • The TTL module never deletes anything, so bounded memory — the whole reason that mode exists — is not modelled. I added a liveness failure showing what the strict rule refuses, and left unmodelled the half that motivates it. That is the weakest thing I submitted.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants