Skip to content

consensus: add Lean proofs for Mysticeti v3 - #27687

Open
mwtian wants to merge 42 commits into
mainfrom
tmw/mysticeti-v3-lean-main
Open

consensus: add Lean proofs for Mysticeti v3#27687
mwtian wants to merge 42 commits into
mainfrom
tmw/mysticeti-v3-lean-main

Conversation

@mwtian

@mwtian mwtian commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This is an attempt to bridge Mysticeti v3 implementation in Rust to a specification in Lean with provable safety and liveness under partial synchrony.

The theoretical specifications of Mysticeti have been proven manually (https://arxiv.org/abs/2310.14821), and with AI assistance (https://github.com/gdanezis/lean-dag). But there are some gaps between the proven specs and the actual implementation. Proving safety and liveness closely based on the actual Rust implementation is also almost impossible, even if we are allowed to acknowledge implementation gaps, because the proof will likely be bogged down in deriving guarantees from detailed behaviors.

Here, the explicit assumptions that went into the Lean spec and proofs are more detailed and realistic than usual proofs, but still not exactly the implementations. For example:

  • Allow block proposals to skip rounds, but adds a to-be-implemented commit progress recovery mode where proposal rounds cannot be skipped.
  • GC, commit sync and ancestor selections are all covered by the safety and liveness proofs if applicable.
  • Use finite throughput capacity for catchup, instead of assuming bounded time catchup regardless of the size of the backlog.

The goal for this specification is to provide a foundation to help us reason about the correctness of future protocol changes in future.

Test plan

CI

@mwtian
mwtian deployed to sui-typescript-aws-kms-test-env August 13, 2026 17:37 — with GitHub Actions Active
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sui-docs Ready Ready Preview Aug 22, 2026 5:19pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
multisig-toolkit Ignored Ignored Preview Aug 22, 2026 5:19pm
sui-kiosk Ignored Ignored Preview Aug 22, 2026 5:19pm

Request Review

mwtian added 2 commits August 19, 2026 14:01
The v3 schedule is adaptive. `LeaderScheduleV3` recomputes the
allowed-leader vector from committed scores, so leader identity flows
upward from the decisions below, while a decision flows downward from its
anchor. Composed without a restriction, the verdict of a slot could
depend on the leader of its own anchor, whose identity depends on that
verdict. Two schedules could then each justify themselves.

Rust already prevents this. `min_next_leader_round` is one round above
the leader round of the last commit in the pending window.
`FlexCommitter::maybe_refresh_pending_commit_state` asserts that the
value is identical at one commit index, that it moves strictly forward
with the index, and that a schedule change drops every pending round
below the new gate.

Model that gate. `V3ScheduleGateSequence.governing_commits_are_below_round`
proves the stratification on the modeled state: every commit that feeds
the schedule governing a round sits at a leader round below it.
`adaptive_run_unique` then proves that at most one run of decisions is
consistent with the rule, and `consistent_runs_are_equal` lifts that to
every reading of the run.

The common commit chain of `ASM-SAFE-COMMIT-CHAIN` therefore follows for
the adaptive schedule rather than being assumed. What remains for that
assumption is the refinement obligation that each correct host's behavior
is a consistent run of one common rule.
`consistent_run_readings_agree` was a schema over an arbitrary reading of
a run. Nothing instantiated it, so no theorem mentioned commits.

Add the instantiation. `committedPrefix` is the ordered committed leaders
of a run through a round bound, and `committed_prefix_agrees` proves that
two consistent runs produce the same one. `v3_committed_candidates_agree`
fixes the verdict to the exact commit candidate that a FlexCommitter
round scan returns, so the statement is about commits rather than about
an abstract reading. `v3_committed_leaders_agree` gives the leader-level
form.

The argument is induction on the round. Agreement below a round gives one
schedule at that round, and one schedule gives one verdict. Prefix
agreement is the conclusion at each step, not a hypothesis. Assuming it
would assume `ASM-SAFE-COMMIT-CHAIN` itself.

`adaptive_run_unique` uses no axiom.
mwtian added 2 commits August 19, 2026 16:10
Two gaps remained after the uniqueness proof.

`ValidatorV3AdaptiveScheduleExistence` closes the first.
`adaptive_run_exists_unique` proves that exactly one run agrees with the
adaptive rule. The construction is strong recursion on the round. Values
above the current round come from a fixed default, and the gate rules
prove that the schedule never reads it, so the theorem needs no
`Inhabited` instance.

`ValidatorV3FlexScheduleRun` closes part of the second.
`V3FlexScheduleHost.consistentRun` derives `ConsistentRun` for one
modeled host from five local facts: the initial schedule, the no-commit
step, the `add_commit` step, the selected order at the governing gate,
and each post-scan slot. The proof reconstructs the modeled
`LeaderScheduleV3` state from the host's own earlier verdicts. It does
not assume `ConsistentRun`, and it does not compare two hosts.

The cross-host step stays open. Uniqueness holds for two runs of one
rule, so agreement still needs every correct host to use the same commit
material, governing gate, and final slot function. Local DAG and GC views
differ, so that step needs the protocol evidence and exact installation
proofs.

Record the revalidation trigger that this work created. The fixpoint
rests on three assertions in
`FlexCommitter::maybe_refresh_pending_commit_state`. Removing one removes
the stratification, and no Lean build failure reports it.
`V3ScheduleGateSequence` took the governing commit index as a field, so
two hosts could in principle govern one round with different indexes.
They cannot. The gate moves strictly forward, so the largest index whose
gate does not exceed a round is unique.

Derive it. `derivedGoverningIndex` is `Nat.findGreatest` over the gate
condition, `derived_gate_not_above` discharges the gate rule for every
round at or above the first gate, and `derived_governing_is_greatest`
shows no later index governs. `index_le_gate` supplies the search bound
from the strictly increasing gate.

`derived_governing_index_agrees` then removes one degree of freedom from
the cross-host step: two hosts with the same schedule states govern each
round with the same commit index, by construction rather than by
assumption.
@mwtian
mwtian deployed to sui-typescript-aws-kms-test-env August 19, 2026 23:56 — with GitHub Actions Active
…l gives

The cross-host step for `ASM-SAFE-COMMIT-CHAIN` cannot be full verdict
equality. The Flex round scan stops at the first undecided selected slot,
so a host with less evidence returns no candidate where a better informed
host returns one. Their verdicts differ at that moment. A theorem that
claimed otherwise would be false of a real execution.

State agreement where both hosts have decided instead.
`round_candidates_agree` gives one round, and
`committed_candidates_agree_across_hosts` gives the installed sequence.
The extra condition is that neither host is ahead of the other in
deciding the round. A lagging host fails that condition until it decides,
which is the correct behavior.

The proof reuses the per-slot result. `final_reference_slot_decision_agrees`
already concludes only when both sides are final, so the partiality was
handled correctly in the slot layer. This lifts it to the round verdict
and the run.

The open condition is now `CrossViewExactSlotAgreement` for each selected
slot. That is the named cross-view safety goal of `ReferenceFlexCommitter`,
not a new assumption.
mwtian added 2 commits August 19, 2026 17:43
Safety has two parts, and the specification proves both. They sat in
separate modules, and the aggregate `mysticeti_v3_safety` theorem covers
only the first, so a reader could not see the second.

Part one is per-slot exclusion. One selected leader slot cannot hold a
commit result and a skip result at the same time, and one transaction
cannot be accepted and rejected.

Part two is same-index commit agreement. Two correct, available
validators that have installed a commit at one index have installed the
same commit. `ExactCommitPrefixSafety.exactInstalledHeadsAtSameIndexAgree`
already proved it. The commit head carries the index, the commit
identifier, and the leader round, so equality of heads is equality of the
digest, and agreement at every index gives prefix agreement entry by
entry.

`MysticetiSafetyCapstone` names the two parts together and re-exports the
second as `correct_validators_agree_on_commit_at_index`. Its docstring
lists what part two assumes: authenticated Flex votes, which carry the
quorum-evidence adapter, and install provenance, whose fields are
one-host storage and hash-chain rules. No provenance field mentions two
validators, and no field states agreement.
A change to `consensus/core` cannot break a Lean target, and the
specification workflow does not run for such a change. The only link is
the prose revalidation-trigger lines in the evidence ledger.

Design a workflow job that reports which proof obligations a product
change puts at risk. The job reads the diff, walks each hunk to its
enclosing Rust item, and looks the item up in a checked-in map. It
compiles nothing.

Measurements over the last 200 commits of this branch: nine touch
`consensus/core/src`, and one touches a proof-critical file. The job is
therefore quiet rather than noisy, so each map entry must carry the
consequence in prose and not only an identifier.

Staleness is the failure that matters. A rename makes the map stop
matching, and silence reads as no risk. The job must check that every
mapped item still exists, and that check fails the build.

Nothing is implemented.
`ExactCommitInstallProvenance.correct_validators_agree_on_commit_at_index`
already proves the safety statement. Two correct, available validators
that installed a commit at one index installed the same commit. Its
inputs hold no cross-host equality: every field of
`AuthenticatedFlexVoteSourceMap` and of `ExactCommitInstallProvenance` is
a one-host rule.

`ValidatorV3FlexScheduleRun` and `ValidatorV3FlexCrossHostAgreement`
built a second route to the same conclusion through a common per-host
model. That route needed cross-host agreement of the commit material and
the post-scan slots, which the first route does not need. It added no
safety result, and nothing outside those two files used any of it.

Remove both files, 360 lines. The fixpoint work stays. It justifies the
head-indexed signatures that the first route uses, such as
`firstPendingRoundForHead`: the schedule recursion is stratified by the
Rust gate, so exactly one run of decisions is consistent with the rule
and the schedule is a function of the commit prefix.
Add a section to the proof scope that names the boundary, so a later
reader does not plan work that cannot finish.

The specification is a conditional model. A theorem applies to the
product only when each source-map field is true of the running code.
Proving that the Rust equals the Lean model is not achievable from the
source alone. It needs extraction, code generation, or translation
validation, and none of those exists here. The comparable `lean-dag`
formalization does not attempt the step either; it names no Rust.

List what is achievable instead: construct a source-map field from a
model of one Rust function, keep the ledger statuses honest, add
conformance tests, and report the obligations that a source change puts
at risk.

Give the worked example. `ASM-SAFE-EVIDENCE-REFINEMENT` needs the
deciding anchor of an indirect result. Rust stores `Decision::Indirect`,
one value of a three-value enumeration. It records that the indirect rule
decided the slot, not which anchor decided it, and
`FlexCommitter::try_indirect_decide` only logs the anchor reference. A
restart or a leader-schedule reset rebuilds the pending state without
that provenance, so the field cannot be filled from the current code.
An earlier commit recorded that a restart or a leader-schedule reset
rebuilds the pending state and loses the provenance of an indirect
decision. That is wrong, and `ASM-SAFE-EVIDENCE-REFINEMENT` carried the
same mistaken premise.

No decision is durable. `PendingCommitState` is in-memory, starts from
its default value, and never reaches the store. `WriteBatch` carries
accepted blocks, commits, commit info, and finalized commits with their
rejected transaction indices. It carries no slot status and no decision
origin. A restart recomputes every verdict from recovered blocks and
commits, and `maybe_refresh_pending_commit_state` discards the pending
rounds on a schedule change. No stale indirect result survives either
event, so there is nothing to misclassify across them. The anchor search
also starts at `min_next_leader_round` and reads pending rounds only, so
a committed leader is never an anchor.

The open part is narrower than recorded: within one pending-state
lifetime, `Decision::Indirect` records that the indirect rule decided a
slot, and `try_indirect_decide` only logs the deciding anchor.

Replace the worked example in the proof scope with one that holds. Safety
has 22 hypothesis fields across two structures and no module constructs
either, and `ASM-SAFE-PARAMETERS` is a field the code is known to break.
Add the instruction to check the store before recording a durability
hazard.

Record that Rust has two commit tables where the model has one durable
notion. Both key on the same index and digest, so the single notion is
adequate for commit agreement. Transaction outcomes are separate.
Split compound rows so each row holds one idea, and point every row at
the exact goal-theorem input field it supplies. Rows that state what a
proof derives lose that text; rows that supply no field are deleted.

- `ASM-SAFE-COMMIT-CHAIN` becomes `ASM-SAFE-DIGEST-IDENTITY`,
  `ASM-SAFE-COMMIT-STORE`, and `ASM-SAFE-INSTALL-PROVENANCE`. The chain
  rule is no longer assumed: `ValidatorCommitHead` now carries
  `previousId`, the same back-link as `CommitV1.previous_digest`,
  `DigestLinkedCommits` defines the chain check, and
  `digest_chain_entry_matches_installed_prefix` derives the
  chain-to-prefix rule from the digest row and the gap-free store.
- `ASM-SAFE-FIRST-TRIGGER` now states the actual finalizer queue:
  compare the newest commit's leader round with the earliest one's,
  then run the indirect rule on the earliest commit and pop it.
- `ASM-SAFE-NON-EQUIVOCATION` keeps the signing rule and covers its
  transaction-vote field. The counting rule moves to
  `ASM-SAFE-VOTE-SET-OVERLAP`, whose leader half is already derived by
  `twoViewEvidence`; the certificate and transaction halves are not.
- `ASM-LIVE-LEADER` splits into `ASM-LIVE-LEADER-STAKE` and
  `ASM-LIVE-LEADER-SCHEDULE`, and the schedule row now names only
  fields that exist. `ASM-SAFE-EVIDENCE-REFINEMENT` moves its
  cached-anchor half to `ASM-SAFE-INDIRECT-ORIGIN`. `ASM-SAFE-GC`
  moves its frontier half to `ASM-LIVE-GC-FRONTIER`, which points at
  the CL `retention` binder. `ASM-LIVE-FINITE-REFERENCE-SPACE` keeps
  the finite encoding and moves its capsule half to
  `ASM-LIVE-CAPSULE-PROJECTION`.
- `ASM-LIVE-COMMIT-SYNC` folds into `ASM-LIVE-TASK-FAIRNESS`; only its
  non-starvation content was an assumption. `ASM-LIVE-DURABILITY`
  keeps the stage timing bounds; the durable-before-exposure facts
  become `REF-DURABLE-PROPOSAL` and `REF-DURABLE-COMMIT-OUTPUT`, and
  restart is described in the proof scope.

Complete the source map so every rule-bearing input field of a goal
theorem has an owner row, and add a structural-fields note for the
data fields that define the model itself. The time-zero boundary
fields `initial` and `genesisParents` get the new row
`ASM-LIVE-COMMON-GENESIS`, and `authorityCountAtLeastTwo` joins
`ASM-SAFE-PARAMETERS`. Delete the dead
`localGcCutoffCausal`, `exactAnchorCausalData`, and `flexCausalView`
inputs, the unconsumed Flex-scan cutoff structure, and the replay,
durable-work, remote-commit-traffic, and action-scoped-leader-parent
modules that no goal theorem consumes.

The ledger check now accepts five type kinds and covers 38 rows with
56 Rust mappings.
Verify the assumption rows against the current Rust and against the
in-progress branch `tmw/mysticeti-v3-transaction-voting`, which the
product intends to merge. A new ledger convention marks rows whose
evidence comes from that branch; they revalidate on merge.

Restate `ASM-SAFE-FIRST-TRIGGER` with the v3 finalizer algorithm: each
new commit reruns direct finalization for all pending commits; the
first local descendant on each authority chain can give an implicit
accept vote through the round after the commit leader; the vote
tracker supplies explicit rejects; indirect finalization uses only
committed descendants with certification stake; the shared depth-two
trigger rejects the remainder, so depth two is the latest release
point. `ASM-LIVE-FINALIZER-TRIGGER` moves from a known mismatch to an
open proof obligation: the trigger path now exists, and what remains
is to derive the arrival of a deep enough commit from commit liveness.

Correct rows against the branch code: the signed cutoff is at least
the block-cleanup and vote-cleanup rounds and vote-target truncation
can raise it; the proposer computes it live in `try_new_block`; the
slot decider, the FlexCommitter, and the v3 finalizer share one
indirect depth. Refresh the non-finalizer rows against current code:
the core handler gate, the own-round fetch trigger, the proposal
flush, the direct and indirect decision rules, the scan bounds, the
commit timestamp rule, and the schedule reader behavior.

Update the Lean model to the branch vote semantics. A vote's target
must only be below its voting round, because first-descendant votes
span mixed rounds. A block that does not accept supplies no stake, so
the reject-side lemmas are deleted; explicit rejects and the depth-two
trigger carry rejection. `cutoffCoversGc` replaces the cutoff-equality
field, and `accepts_only_above_gc` is reproved from the inequality.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant