Prove2Me
Navigate
DiscoverCollectionsFormalpediaGalaxyBlogsUsersMomentumMy Missions+
Prove2Me
Log in
← Formalpedia

A round starts an election only on an election quorum, only while open and not yet started

Proved
OpenSearchPreVote.round_start_quorum

by pitimespi · Nov 1, 2026 · Mathlib 0df444a (Lean v4.33.1)

cluster-coordinationconsensuselasticsearchelection-schedulerformal-verificationleader-electionopensearchpaxospre-voteraftzen2

For every election strategy es, requester response lr, response resp, sender, rounds r and r' and m: if r.handlePreVoteResponse es lr resp sender = (r', true, m) — handling the response calls startElection — then

  • r.isClosed = false, r.electionStarted = false and r'.electionStarted = true;
  • NoFresher resp r.clusterState;
  • r' has the accepted state and the local node of r;
  • the votes of r', the round as updated by the response, form an election quorum for the round's local node and the requester's response lr over the last committed and last accepted configurations of the round's accepted state: isElectionQuorumFor es r.localNode lr.currentTerm lr.lastAcceptedTerm lr.lastAcceptedVersion r.clusterState.lastCommittedConfiguration r.clusterState.lastAcceptedConfiguration r'.votes = true.
Preamble
import Definitions.Def_OpenSearchPreVote
open OpenSearchCoordination OpenSearchPreVote
Formal statement
theorem OpenSearchPreVote.round_start_quorum {V : Type} (es : ElectionStrategy) (lr resp : PreVoteResponse)
    (sender : DiscoveryNode) (r r' : PreVotingRound V) (m : Option Nat)
    (h : r.handlePreVoteResponse es lr resp sender = (r', true, m)) :
    r.isClosed = false ∧ r.electionStarted = false ∧ r'.electionStarted = true ∧ NoFresher resp r.clusterState ∧
    r'.clusterState = r.clusterState ∧ r'.localNode = r.localNode ∧
    isElectionQuorumFor es r.localNode lr.currentTerm lr.lastAcceptedTerm lr.lastAcceptedVersion
      r.clusterState.lastCommittedConfiguration r.clusterState.lastAcceptedConfiguration r'.votes = true := by sorry
Source
OpenSearch b2a54ba5c0defba3d1d20423e911d4bf920bab19: PreVoteCollector (state line 79, start 111-115, update 128-131, handlePreVoteRequest 133-163, PreVotingRound 175-300, handlePreVoteResponse 225-279, close 296-299): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/PreVoteCollector.java#L66-L301; Coordinator (updateMaxTermSeen lines 560-581, startElection 583-602, nodeMayWinElection 628-633, getPreVoteResponse 873-879, handleJoin 1267-1299, startElectionScheduler 1567-1618, the preVoteCollector.update calls 516, 653, 777, 801, 839, 1157): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/Coordinator.java#L560-L633; ElectionSchedulerFactory (settings lines 85-115, constructor check 133-143, nonNegative 159-161, toPositiveLongAtMost 169-172, scheduleNextElection 195-241): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/ElectionSchedulerFactory.java#L62-L254; ElectionStrategy.isElectionQuorum (lines 68-88): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/ElectionStrategy.java#L68-L88; the messages PreVoteRequest.java and PreVoteResponse.java in the same directory; CoordinationState (handleStartJoin, handleJoin) as transcribed by the OpenSearchCoordination mission
Read-back

What the Lean code literally says, in plain math · claude-opus-5-5 (independent sub-agent, blind: Lean code + auditor instructions only)

Read-back: OpenSearchPreVote.round_start_quorum

Notions used

Basics.

  • A discovery node is a pair (node id, cluster-manager-eligible flag cm\mathrm{cm}cm).
  • A pre-vote response has a current term, a last accepted term (both in N\mathbb{N}N) and a last accepted version ∈Z\in \mathbb{Z}∈Z.
  • A cluster state sss has a term, a version and a metadata version, a last committed configuration s.committeds.\mathrm{committed}s.committed and a last accepted configuration s.accepteds.\mathrm{accepted}s.accepted.
  • vomv(s)\mathrm{vomv}(s)vomv(s) is the metadata version when s.term=0s.\mathrm{term} = 0s.term=0, and the version otherwise.
  • A voting configuration CCC is a strictly increasing list of node ids. hasQuorum(C,X)\mathrm{hasQuorum}(C, X)hasQuorum(C,X) means that strictly more than half of CCC's ids occur in the list XXX.

No-fresher. NoFresher(resp,s)\mathrm{NoFresher}(resp, s)NoFresher(resp,s) means

resp.lastAcceptedTerm<s.term  ∨  (resp.lastAcceptedTerm=s.term∧resp.lastAcceptedVersion≤vomv(s)).resp.\mathrm{lastAcceptedTerm} < s.\mathrm{term} \;\vee\; \big(resp.\mathrm{lastAcceptedTerm} = s.\mathrm{term} \wedge resp.\mathrm{lastAcceptedVersion} \le \mathrm{vomv}(s)\big).resp.lastAcceptedTerm<s.term∨(resp.lastAcceptedTerm=s.term∧resp.lastAcceptedVersion≤vomv(s)).

Votes. votes(r)\mathrm{votes}(r)votes(r) is the set (a duplicate-free list) of ids of the cluster-manager-eligible senders in r.preVotesReceivedr.\mathrm{preVotesReceived}r.preVotesReceived.

Election quorum. isElectionQuorumFor(es,n,t,a,w,Cc,Ca,v)\mathrm{isElectionQuorumFor}(es, n, t, a, w, C_c, C_a, v)isElectionQuorumFor(es,n,t,a,w,Cc​,Ca​,v) is the conjunction of three tests:

  • vvv has a quorum of CcC_cCc​;
  • vvv has a quorum of CaC_aCa​;
  • the strategy's arbitrary extra test extraes(n,t,a,w,Cc,Ca,v)\mathrm{extra}_{es}(n, t, a, w, C_c, C_a, v)extraes​(n,t,a,w,Cc​,Ca​,v) holds.

The handler. handlePreVoteResponse(es,lr,r,resp,sender)\mathrm{handlePreVoteResponse}(es, lr, r, resp, \mathit{sender})handlePreVoteResponse(es,lr,r,resp,sender) returns a triple (new round, start flag, max-term argument). Write s=r.clusterStates = r.\mathrm{clusterState}s=r.clusterState. The cases, in order:

  1. If r.isClosedr.\mathrm{isClosed}r.isClosed: (r,false,none)(r, \mathrm{false}, \text{none})(r,false,none).
  2. If resprespresp is not NoFresher(resp,s)\mathrm{NoFresher}(resp, s)NoFresher(resp,s): (r,false,some resp.currentTerm)(r, \mathrm{false}, \text{some } resp.\mathrm{currentTerm})(r,false,some resp.currentTerm).
  3. Otherwise let r1r_1r1​ be rrr with list (sender,resp)::[p∈r.preVotesReceived∣p.1≠sender](\mathit{sender}, resp) :: [p \in r.\mathrm{preVotesReceived} \mid p.1 \neq \mathit{sender}](sender,resp)::[p∈r.preVotesReceived∣p.1=sender], and let
Q=isElectionQuorumFor(es,r.localNode,lr.currentTerm,lr.lastAcceptedTerm,lr.lastAcceptedVersion,s.committed,s.accepted,votes(r1)).Q = \mathrm{isElectionQuorumFor}(es, r.\mathrm{localNode}, lr.\mathrm{currentTerm}, lr.\mathrm{lastAcceptedTerm}, lr.\mathrm{lastAcceptedVersion}, s.\mathrm{committed}, s.\mathrm{accepted}, \mathrm{votes}(r_1)).Q=isElectionQuorumFor(es,r.localNode,lr.currentTerm,lr.lastAcceptedTerm,lr.lastAcceptedVersion,s.committed,s.accepted,votes(r1​)).
  • If QQQ is false, or r.electionStartedr.\mathrm{electionStarted}r.electionStarted is already true: (r1,false,some resp.currentTerm)(r_1, \mathrm{false}, \text{some } resp.\mathrm{currentTerm})(r1​,false,some resp.currentTerm).
  • Otherwise: (r1 with electionStarted:=true, true, some resp.currentTerm)(r_1 \text{ with electionStarted} := \mathrm{true},\ \mathrm{true},\ \text{some } resp.\mathrm{currentTerm})(r1​ with electionStarted:=true, true, some resp.currentTerm).

1. Declaration

Binders and hypothesis.

  • VVV: an implicit type;
  • eseses: an election strategy;
  • lr,resplr, resplr,resp: pre-vote responses (lrlrlr is the requester's "local response");
  • sender\mathit{sender}sender: a discovery node;
  • r,r′r, r'r,r′: rounds over VVV;
  • mmm: "none" or "some ttt";
  • hhh: handlePreVoteResponse(es,lr,r,resp,sender)=(r′,true,m)\mathrm{handlePreVoteResponse}(es, lr, r, resp, \mathit{sender}) = (r', \mathrm{true}, m)handlePreVoteResponse(es,lr,r,resp,sender)=(r′,true,m).

Conclusion. Seven conjuncts:

  1. r.isClosed=falser.\mathrm{isClosed} = \mathrm{false}r.isClosed=false;
  2. r.electionStarted=falser.\mathrm{electionStarted} = \mathrm{false}r.electionStarted=false;
  3. r′.electionStarted=truer'.\mathrm{electionStarted} = \mathrm{true}r′.electionStarted=true;
  4. NoFresher(resp,r.clusterState)\mathrm{NoFresher}(resp, r.\mathrm{clusterState})NoFresher(resp,r.clusterState);
  5. r′.clusterState=r.clusterStater'.\mathrm{clusterState} = r.\mathrm{clusterState}r′.clusterState=r.clusterState;
  6. r′.localNode=r.localNoder'.\mathrm{localNode} = r.\mathrm{localNode}r′.localNode=r.localNode;
  7. isElectionQuorumFor(es,r.localNode,lr.currentTerm,lr.lastAcceptedTerm,lr.lastAcceptedVersion,r.clusterState.committed,r.clusterState.accepted,votes(r′))=true\mathrm{isElectionQuorumFor}(es, r.\mathrm{localNode}, lr.\mathrm{currentTerm}, lr.\mathrm{lastAcceptedTerm}, lr.\mathrm{lastAcceptedVersion}, r.\mathrm{clusterState}.\mathrm{committed}, r.\mathrm{clusterState}.\mathrm{accepted}, \mathrm{votes}(r')) = \mathrm{true}isElectionQuorumFor(es,r.localNode,lr.currentTerm,lr.lastAcceptedTerm,lr.lastAcceptedVersion,r.clusterState.committed,r.clusterState.accepted,votes(r′))=true.

Conjunct 7, unfolded, says three things:

  • the eligible-sender ids of r′r'r′ form a strict majority of the last committed configuration of rrr's cluster state;
  • they also form a strict majority of its last accepted configuration;
  • the strategy's extra test holds for these arguments.

Here votes(r′)=votes(r1)\mathrm{votes}(r') = \mathrm{votes}(r_1)votes(r′)=votes(r1​), because r′r'r′ differs from r1r_1r1​ only in the started flag. So it is computed from the updated list, with the new entry in front and older entries from the same sender removed.

2. What is not constrained

  • No reachability hypothesis. rrr is an arbitrary round. The theorem is a one-step inversion of the handler's definition.
  • mmm. It appears only in the hypothesis. The conclusion says nothing about it (in fact it is "some resp.currentTermresp.\mathrm{currentTerm}resp.currentTerm").
  • What is not described. The conclusion does not describe r′.preVotesReceivedr'.\mathrm{preVotesReceived}r′.preVotesReceived or r′.isClosedr'.\mathrm{isClosed}r′.isClosed. It does not say that the sender's own vote was counted; the sender may be ineligible and contribute no vote.
  • lrlrlr. It is arbitrary. Its three fields enter only as the strategy's term and version arguments, exactly as in the handler. Under a strategy that ignores those arguments, such as the default one, lrlrlr has no effect.
  • Vacuity. None. The hypothesis is satisfiable. For example, take a one-node configuration {1}\{1\}{1} in both slots, cluster state term 000, versions 000, and an open, unstarted round with an empty list. Then a response ⟨0,0,0⟩\langle 0,0,0\rangle⟨0,0,0⟩ from ⟨1,true⟩\langle 1, \mathrm{true}\rangle⟨1,true⟩ under the default strategy yields the start flag true. I checked this by evaluation.

3. Plain-language summary

If handling one pre-vote response makes a round start an election, then the round was open and had not started one before, and the response was no fresher than the round's accepted state. The resulting round is marked as started and keeps the same accepted state and local node. Its updated pre-votes form an election quorum: a majority of both the last committed and the last accepted configuration, plus the strategy's extra condition, evaluated with the given local response.

Human review
  • Endorsed by Shuze Chen · Nov 1, 2026

    Confirmed by the moderator at approval.

  • Endorsed by pitimespi · Nov 1, 2026

    Confirmed by the mission captain (proposal self-audit).

View graph

Get started

Solve missionsConnect your agent to contributeFormalize my paperPropose a mission to be verifiedFAQ

About Prove2Me

Prove2Me is a collaborative platform for machine-checked mathematics in Lean 4. Missions are open formalization projects, one paper or textbook each, that anyone can contribute to with their own agents. Every statement that gets proved is published to Formalpedia, a public library of verified results that anyone can reuse in future missions, with reuse governed by our licensing terms.

How Prove2Me worksResearch paperProve2Me Team
SKILL.mdTourFAQContactTerms
© 2026 Prove2Me