For every payload type V, cluster parameters P with P.InitCompatible and P.strategy = ElectionStrategy.DEFAULT_INSTANCE, every cluster W with Reachable P W and every node n with P.eligible n = true: (W.node n).javaInvariant P.strategy.
javaInvariant is the conjunction of the five assertions of invariant() (CoordinationState.java:601-611): the last accepted term is at most the current term; electionWon equals isElectionQuorum of the join votes; if the election is won, the last published version is at least the last accepted version, otherwise it is 0; a won election implies startedJoinSinceLastReboot; the publish votes are empty or the election is won. Not every handler preserves these assertions on its own: handlePublishRequest can break them (OpenSearchCoordination.javaInvariant_not_local), so they hold because of what the rest of the cluster did. A node that is not cluster-manager-eligible may restart with any persisted state, so the first assertion can fail there.
Preamble
import Definitions.Def_OpenSearchSafety
open OpenSearchCoordination OpenSearchSafety
Sources and conventions. Rendered from Definitions/Def_OpenSearchCoordination.lean, Definitions/Def_OpenSearchSafety.lean and the stub Theorems/Thm_OpenSearchSafety_javaInvariant_reachable.lean; Java line numbers refer to OpenSearch b2a54ba5c0defba3d1d20423e911d4bf920bab19 (CS = CoordinationState.java, CM = CoordinationMetadata.java, ES = ElectionStrategy.java, CO = Coordinator.java, CBS = ClusterBootstrapService.java), checked against the local checkout. Throughout, V is an arbitrary payload type, P a parameter record and W a cluster; csn abbreviates W.node(n), the coordination state of node n in W, and "m∈sent" means W.sent(m). Booleans are read as truth values: "won(cs)" means electionWon = true, "¬won(cs)" means electionWon = false.
Nodes, configurations, quorums. Node ids are natural numbers. A discovery noded is a pair (d.id,d.cm) of an id and a Boolean (isClusterManagerNode). A voting configurationc is a strictly increasing list of node ids, hence duplicate-free; two configurations are equal exactly when they have the same members. ∅ denotes the empty configuration EMPTY_CONFIG; "c is empty" means its list is empty. For a configuration c and any list q of ids,
hasQuorum(c,q)⟺2⋅{x∈c:x∈q}>∣c∣,
so the empty configuration has no quorum, and ids of q outside c as well as repetitions in q are irrelevant. Transcription check: matches VotingConfiguration.hasQuorum (CM:405-409).
A vote collection is a list of node ids; the empty collection is []; isQuorum(v,c):=hasQuorum(c,v); addVote(v,d) is (d.id::v,added) if d.cm is true and d.id∈/v, and (v,not added) otherwise. Transcription check:VoteCollection (CS:750-814) keys a map by node id; addVote (CS:755-757) changes the key set exactly in the same cases.
Cluster states. A cluster state s has a term s.term∈N, a version s.version∈Z, a metadata version s.mv∈Z, a last committed and a last accepted configuration s.lcc and s.lac, an optional natural number s.cmNode (the cluster-manager node), two Booleans s.uuidKnown and s.uuidCommitted, and a payload s.value∈V. Two cluster states are equal when all nine fields are. vomv(s) (versionOrMetadataVersion) is s.mv if s.term=0 and s.version otherwise. markCommitted(s) is s with lcc replaced by s.lac and with uuidCommitted set to true when s.uuidKnown is true; nothing else changes (transcribes CS:703-740; the Java assertion at CS:708-710 is not checked).
Message contents. A joinj has a source and a target discovery node j.src, j.tgt, a term j.term∈N, a last accepted term j.lat∈N and a last accepted version j.lav∈Z; two joins are equal when all five fields are. A publish response is a pair ⟨t,v⟩∈N×Z. An apply-commit request is a triple ⟨d,t,v⟩ of a discovery node, a term and a version. A start-join request is a pair of a discovery node and a term; it is not a cluster message (see the steps).
Coordination states. A coordination state cs has a local node cs.localNode (a discovery node), a current term ct(cs)∈N, a last accepted state cs.las (a cluster state), two vote collections joinVotes(cs) and pubVotes(cs), Booleans started(cs) (startedJoinSinceLastReboot) and won(cs) (electionWon), a last published version lpv(cs)∈Z and a last published configuration lpc(cs). lat(cs), lav(cs), lcc(cs), lac(cs) are the term, version, last committed and last accepted configuration of cs.las. init(d,t,s) is the coordination state with local node d, current term t, last accepted state s, both vote collections empty, started and won false, lpv=0 and lpc=s.lac (the constructor, CS:84-106).
Election strategies. An election strategy is a Boolean function σ(d,t,at,av,c1,c2,v) of a discovery node, two naturals, an integer, two configurations and a vote collection; DEFAULT_INSTANCE is the constant-true function. isElectionQuorumσ(cs,v):=isQuorum(v,lcc(cs))∧isQuorum(v,lac(cs))∧σ(cs.localNode,ct(cs),lat(cs),lav(cs),lcc(cs),lac(cs),v); for the default strategy the third conjunct is true. isPublishQuorum(cs,v):=isQuorum(v,lcc(cs))∧isQuorum(v,lpc(cs)). Transcription check: ES:68-88 with CS:144-154, and CS:156-158.
Parameters. A parameter record P consists of elig:N→Bool (eligible: whether a node is cluster-manager-eligible), a predicate initConfs on configurations (an arbitrary set of "bootstrap configurations": possibly empty, possibly containing ∅, possibly infinite), and an election strategy σP (strategy). dn(n):=(n,elig(n)); "x is eligible" means elig(x)= true. InitCompatible(P) holds when for all configurations X,Y with initConfs(X) and initConfs(Y) and all lists q1,q2 of eligible ids,
hasQuorum(X,q1)∧hasQuorum(Y,q2)⟹∃x,x∈q1∧x∈q2
(for X=Y this is automatic: two majorities of one configuration meet).
Messages and clusters. A cluster message is one of: join(j); pubReq(a,s,pt,pv), a publish request from node a carrying a cluster state s and a ghost parent(pt,pv)∈N×Z; pubResp(a,d,r), a publish response r from node a to node d; commit(c), an apply-commit request c. A cluster W is a function W.node from node ids to coordination states together with a predicate W.sent on messages (the messages sent so far). W.set(n,cs) replaces node n's state by cs and changes nothing else; W.send(m) adds m to the sent set and changes nothing else. A key is a pair (t,v)∈N×Z; the key of a publication pubReq(a,s,pt,pv) is (s.term,s.version).
The handlers. Each handler returns either an error (the corresponding step is then impossible) or a result. Guards are tested in the order listed; the first that applies gives the error.
setInitialState(cs,s): error if lac(cs) is non-empty; otherwise cs with cs.las:=s (nothing else changes). Its contractinitialStateContract(cs,s) is the conjunction: lat(cs)=0; lcc(cs) empty; lpv(cs)=0; lpc(cs) empty; ¬won(cs); joinVotes(cs) and pubVotes(cs) empty; s.term=0; s.version=lav(cs); s.lac and s.lcc non-empty. Transcription check: CS:180-204; the contract is the eleven assertions CS:190-201.
handleStartJoin(cs,(d,t)): error if t≤ct(cs); otherwise the pair of cs with ct:=t, lpv:=0, lpc:=lac(cs), started:= true, won:= false and both vote collections emptied (last accepted state unchanged), and the join ⟨cs.localNode,d,t,lat(cs),vomv(cs.las)⟩ (source, target, term, last accepted term, last accepted version — the metadata version when lat(cs)=0). Transcription check: CS:213-255.
handleJoinσ(cs,j): errors, in order, if j.term=ct(cs); if ¬started(cs); if j.lat>lat(cs); if j.lat=lat(cs) and j.lav>vomv(cs.las); if lac(cs) is empty. Otherwise, with (v′,b)=addVote(joinVotes(cs),j.src) and w=isElectionQuorumσ(cs,v′), the result is cs with joinVotes:=v′ and won:=w, and moreover lpv:=lav(cs) if w holds and won(cs) did not; together with the Boolean b ("vote added"). The target j.tgt is not examined. Transcription check: CS:264-339; the Java assertions CS:265 (target is the local node) and CS:323-324 (a won election is not lost) are not checked by the handler.
handleClientValue(cs,s): errors, in order, if ¬won(cs); if lpv(cs)=lav(cs); if s.term=ct(cs); if s.version≤lpv(cs); if s.lac=lac(cs) and lcc(cs)=lac(cs); if ¬isQuorum(joinVotes(cs),s.lac). Otherwise cs with lpv:=s.version, lpc:=s.lac, pubVotes:=[], together with the publish request carrying s. Its contractclientValueContract(cs,s) is s.lcc=lcc(cs). Transcription check: CS:348-407; the contract is the assertion CS:397-398.
handlePublishRequest(cs,s): error if s.term=ct(cs). Otherwise, if s.term=lat(cs) and s.version≤lav(cs): when s.term=0, error; when s.term=0 (the "Zen1" branch), error if s.cmNode is none, accept if s.cmNode=somem with somem=cs.las.cmNode, error otherwise. In all remaining cases, accept. To accept is to return cs with cs.las:=s and the response ⟨s.term,s.version⟩. Transcription check: CS:416-460 (a null incoming cluster-manager in the Zen1 branch throws in Java).
handlePublishResponse(cs,d,r): errors, in order, if ¬won(cs); if r.term=ct(cs); if r.version=lpv(cs). Otherwise let v′ be the collection returned by addVote(pubVotes(cs),d); the result is cs with pubVotes:=v′, together with the optional apply-commit request some⟨cs.localNode,r.term,r.version⟩ if isQuorum(v′,lcc(cs))∧isQuorum(v′,lpc(cs)), and none otherwise. Transcription check: CS:471-515.
handleCommit(cs,⟨d,t,v⟩): errors, in order, if t=ct(cs); if t=lat(cs); if v=lav(cs). Otherwise cs with cs.las:=markCommitted(cs.las). The source d is not examined. Transcription check: CS:523-573.
Restart states.RestartState(s,s′) holds when either (i) s′ agrees with s on term, version, lcc, lac, uuidKnown and uuidCommitted (its metadata version, cluster-manager node and payload are free); or (ii) s.term=0, s.lcc=s.lac=∅, s.uuidKnown= false, and s′.term=0, s′.lcc=s′.lac=∅ (every other field of s′, including its version, is free). Citation check: in GatewayMetaState.java (141-207) an eligible node reloads its persisted current term, version and metadata (the metadata includes the coordination metadata and the cluster-UUID flags). The remote-store path (169-186) is taken by an eligible node with remote cluster state enabled when its local cluster UUID is unknown and the remote store knows one; the restored state is built on Metadata.EMPTY_METADATA (line 296), to which RemoteStoreRestoreService.restore adds index metadata, global settings, templates, customs and the remote version, but no coordination metadata and no cluster UUID — so its term is 0 and its configurations are empty, as in (ii). Java also persists the rest of the metadata (payload, metadata version), which (i) leaves free: more behaviours than Java.
Initial clusters.Initial(P,W) holds when every node n has csn=init(dn(n),0,sn) for some cluster state sn with sn.term=0 and sn.lcc=sn.lac=∅ (the version, metadata version, cluster-manager node, cluster-UUID flags and payload of sn are arbitrary and may differ from node to node), and no message is sent.
Steps.StepP(W,W′) holds exactly in the following nine cases (all objects arbitrary unless constrained):
bootstrap: a node n, a configuration X with initConfs(X), a cluster state s with initialStateContract(csn,s) and s.lcc=s.lac=X, and setInitialState(csn,s)=okcs′; then W′=W.set(n,cs′). Node n need not be eligible (the Coordinator rejects non-eligible nodes and imposes further guards, CO:1110-1141; omitting them only adds behaviours).
startJoin: nodes n, src with src eligible, a term t, and handleStartJoin(csn,(dn(src),t))=ok(cs′,j); then W′=W.set(n,cs′).send(join(j)). No message from src is required.
join: a node n and a join j with join(j)∈sent, j.tgt.id=n and handleJoinσP(csn,j)=ok(cs′,b); then W′=W.set(n,cs′).
clientValue: a node n and a cluster state s with clientValueContract(csn,s) and handleClientValue(csn,s)=ok(cs′,p); then W′=W.set(n,cs′).send(pubReq(n,s,lat(csn),lav(csn))): the request carries the state of p, which is s, and as ghost parent the key of n's last accepted state (unchanged by the handler).
publish: nodes n, src and pubReq(src,s,pt,pv)∈sent with handlePublishRequest(csn,s)=ok(cs′,r); then W′=W.set(n,cs′).send(pubResp(n,src,r)). Any node may handle any sent publish request.
respond: nodes n, src and pubResp(src,n,r)∈sent with handlePublishResponse(csn,dn(src),r)=ok(cs′,o); then W′=W.set(n,cs′) if o=none and W′=W.set(n,cs′).send(commit(c)) if o=somec.
commit: a node n and commit(c)∈sent with handleCommit(csn,c)=okcs′; then W′=W.set(n,cs′). Any node may handle any sent commit.
restart: an eligible node n and a cluster state s′ with RestartState(csn.las,s′); then W′=W.set(n,init(csn.localNode,ct(csn),s′)).
restartAny: a node n that is not eligible, any term t and any cluster state s′; then W′=W.set(n,init(csn.localNode,t,s′)).
Messages are never removed from the sent set; a sent message may be handled any number of times, or never.
Reachability.ReachableP is the least predicate on clusters that holds of every W with Initial(P,W) and holds of W′ whenever it holds of W and StepP(W,W′).
The Java invariant.javaInvariant(σ,cs) is the conjunction of: (a) lat(cs)≤ct(cs); (b) the Boolean won(cs) equals the Boolean isElectionQuorumσ(cs,joinVotes(cs)); (c) if won(cs) then lpv(cs)≥lav(cs), otherwise lpv(cs)=0; (d) won(cs)⇒started(cs); (e) pubVotes(cs) is empty or won(cs). Transcription check: exactly the five assertions of CoordinationState.invariant() (CS:601-611), in order.
1. Declaration
Binders and hypotheses, in order:
an implicit payload type V and a parameter record P;
hP : InitCompatible(P);
hes : σP=DEFAULT_INSTANCE;
a cluster W and hW : ReachableP(W);
a node n with hn : n is eligible.
Conclusion.javaInvariant(σP,csn), which by hes reads:
(a) lat(csn)≤ct(csn);
(b) won(csn) holds exactly when isQuorum(joinVotes(csn),lcc(csn))∧isQuorum(joinVotes(csn),lac(csn));
(c) if won(csn) then lpv(csn)≥lav(csn), otherwise lpv(csn)=0;
(d) won(csn)⇒started(csn);
(e) pubVotes(csn)=[] or won(csn).
2. What is not constrained
elig and initConfs are arbitrary apart from InitCompatible(P); the payload type V is arbitrary. Only the default election strategy is covered.
Reachable clusters include every interleaving of the nine steps: messages lost, duplicated, delayed and reordered; crashes and restarts at any time; start-joins from any eligible node with any higher term; any client value the handlers accept.
Only eligible nodes are covered. For a non-eligible node, the restartAny step can make lat>ct, falsifying (a); the theorem says nothing about such nodes.
Other Java assertions (those inside the handlers, e.g. CS:190-201, CS:323-324, CS:572) are not part of javaInvariant and not covered.
Not vacuous (the conclusion is non-trivial already at the leader of the one-node scenario in the read-back of commit_reachable).
3. Plain-language summary
In every reachable cluster, every cluster-manager-eligible node satisfies the five assertions of OpenSearch's CoordinationState.invariant() (with the default election strategy).
Confirmed by the moderator at approval.