A pre-voting round that a quorum refuses never starts an election
ProvedOpenSearchPreVote.no_disruptionFix an election strategy es, the health healthy n and collector state st n of every node n (as they stand while the round runs), a requester c whose pre-vote requests carry the term t, a cluster state s, and a list of node ids L that is a quorum of the last committed or of the last accepted configuration of s. Suppose every node whose id is in L is unhealthy or knows a leader other than c: for every n with n.id ∈ L, healthy n = false, or (st n).leader = some l with l ≠ c. Let r be a round over s (r.clusterState = s) that is reachable when the deliverable responses are exactly the answers to c's request, handlePreVoteRequest (healthy n) (st n) ⟨c, t⟩ = .ok resp. Then r.electionStarted = false, and handling any further such answer, with any requester response lr, does not call startElection.
Whatever answers arrive, in whatever order and number: only nodes outside L answer, and a set of voters disjoint from a quorum of a configuration is not a quorum of it, so the election-quorum test, which needs quorums of both configurations, never passes. A node cut off from a cluster whose majority follows a leader cannot depose that leader by starting elections when it reconnects. The theorem does not cover the requests' side effect: each request raises the receiver's largest seen term to at least t, and a leader whose current term is below t then moves to that larger term and starts an election itself, immediately or after its publication in progress (Coordinator.updateMaxTermSeen, CO:560-581, and CO:1784).
import Definitions.Def_OpenSearchPreVote open OpenSearchCoordination OpenSearchPreVote
theorem OpenSearchPreVote.no_disruption {V : Type} (es : ElectionStrategy) (healthy : DiscoveryNode → Bool)
(st : DiscoveryNode → CollectorState) (c : DiscoveryNode) (t : Nat) (s : ClusterState V) (L : List NodeId)
(hL : s.lastCommittedConfiguration.hasQuorum L = true ∨ s.lastAcceptedConfiguration.hasQuorum L = true)
(hfollow : ∀ n : DiscoveryNode, n.id ∈ L → healthy n = false ∨ ∃ l, (st n).leader = some l ∧ l ≠ c)
(r : PreVotingRound V)
(hr : RoundReachable es (fun n resp => handlePreVoteRequest (healthy n) (st n) ⟨c, t⟩ = .ok resp) r)
(hs : r.clusterState = s) :
r.electionStarted = false ∧
∀ lr resp sender, handlePreVoteRequest (healthy sender) (st sender) ⟨c, t⟩ = .ok resp →
(r.handlePreVoteResponse es lr resp sender).2.1 = false := by sorryRead-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.no_disruption
Notions used
Discovery nodes. A discovery node is a pair: a node id and a Boolean flag ("cluster-manager-eligible"). Two discovery nodes are equal iff both components are equal.
Configurations and quorums. A voting configuration is a strictly increasing (hence duplicate-free) list of node ids . For a list of node ids,
Cluster states. A cluster state has, among other fields, a last committed configuration and a last accepted configuration .
Requests, collector states, and the request handler.
- A pre-vote request consists of a source node and a term .
- A collector state has a known leader ("none", or "some ") and a stored response .
- returns success with iff is true and ( or ). Otherwise it returns an error. The term is never read.
Rounds. A round has a cluster state , a local node , a list of (sender, response) pairs, and Booleans and . is the set of ids of the cluster-manager-eligible senders in the list.
The response handler. returns (new round, start flag, max-term argument). Write .
-
If is closed, or is fresher than , the round is unchanged and the flag is false.
-
Otherwise the list becomes followed by the old entries from senders ; call this round . The flag is true iff both of these hold:
- had not started an election;
- , and the strategy's arbitrary extra test all hold.
When the flag is true, becomes true.
Reachability. is the least predicate on rounds closed under three rules:
- every is reachable;
- handling, with any local response, of any with ;
- closing.
1. Declaration
Binders and hypotheses, in order.
- : an implicit type.
- : an election strategy, arbitrary.
- : a function from discovery nodes to Booleans (each node's health).
- : a function from discovery nodes to collector states (each node's known leader and stored response).
- : a discovery node (the requester named in the pre-vote request).
- : the request's term.
- : a cluster state over .
- : a list of node ids.
- : . So strictly more than half of the ids of 's last committed configuration lie in , or strictly more than half of the ids of its last accepted configuration do.
- : for every discovery node (with either flag) such that ,
Every node whose id is in is unhealthy or knows a leader different from . "Different" means different id or different flag. By the handler's definition, this is the same as saying every such node refuses the request .
- : a round over .
- : , where
Unfolded, holds iff all of these hold:
- ;
- ;
- .
So the only deliveries are of the stored responses of the nodes willing to answer .
- : .
Conclusion.
- (a) The round has never started an election.
- (b) Handling one more response that some sender would genuinely give to does not start an election either, for any local response .
2. What is not constrained
-
The strategy. is arbitrary, even non-monotone. The conclusion can hold for every strategy because the election quorum requires both configuration majorities, and blocks one of them. Here is my reading of why the hypotheses fit together (not a proof):
- No node with an id in can ever deliver.
- So stays disjoint from .
- A set disjoint from a strict majority of a duplicate-free configuration cannot itself be a strict majority of it.
-
The term . It never matters, because the request handler does not read it.
-
The local node. is unconstrained, and it is not required to equal . The node named as requester in the requests and the round's own local node may differ.
-
Nodes outside . They are unconstrained. They may be healthy and leaderless, or follow , and then they deliver and their votes count.
-
The list . may contain duplicates or ids outside both configurations. Only one of the two configurations needs to have its majority inside .
-
The requester itself. If , then also constrains 's own health and collector state: must be unhealthy or follow another leader. If , 's own answer is unconstrained.
-
Static model. Health and collector states are fixed functions for the entire round. They do not change while the round runs. All deliveries answer the same request .
-
Scope. The statement concerns only the round's start flag.
- It says nothing about the responders'
updateMaxTermSeen. In Java,handlePreVoteRequestraises every responder'smaxTermSeento (PreVoteCollector.java:134). That includes a current leader, which then moves to a higher term and starts an election itself when exceeds its term (Coordinator.java:564–573). The module docstring places this outside the functions. - It says nothing about the effect of the round's own
updateMaxTermSeencalls.
- It says nothing about the responders'
-
Vacuity. None. The hypotheses are jointly satisfiable in a non-trivial way, which I checked by evaluation:
- take configuration in both slots and ;
- nodes with ids and follow leader ;
- is healthy with no leader;
- answers its own request, giving votes : no quorum and no start.
Also, and hold for for any .
3. Plain-language summary
Suppose a majority of either the last committed or the last accepted configuration of state consists of nodes that are unhealthy or already follow a leader other than . Then no pre-voting round over that collects only genuine answers to 's request ever starts an election, and one more genuine answer would not start one either. The statement is about the round's quorum count with every node's state frozen. It does not cover Java's term-raising side effects of pre-vote requests.
Confirmed by the mission captain (proposal self-audit).
Confirmed by the moderator at approval.