For all P, every cluster W with Inv P W and keys (t₁, v₁), (t₂, v₂), (t, v) with Descends W t₁ v₁ t v and Descends W t₂ v₂ t v: Descends W t₁ v₁ t₂ v₂ ∨ Descends W t₂ v₂ t₁ v₁. Since every key has at most one parent, the published states form a forest and two ancestors of one key are comparable.
Preamble
import Definitions.Def_OpenSearchSafety
open OpenSearchCoordination OpenSearchSafety
Formal statement
theorem OpenSearchSafety.inv_descends_linear {V : Type} (P : Params) (W : Cluster V) (hinv : Inv P W)
(t₁ t₂ t : Nat) (v₁ v₂ v : Int) (h₁ : Descends W t₁ v₁ t v) (h₂ : Descends W t₂ v₂ t v) :
Descends W t₁ v₁ t₂ v₂ ∨ Descends W t₂ v₂ t₁ v₁ := by sorry
Source
OpenSearch b2a54ba5c0defba3d1d20423e911d4bf920bab19: CoordinationState (the per-node handlers as transcribed by the OpenSearchCoordination mission; handleCommit lines 523-573, isPublishQuorum 156-158, invariant 601-611): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/CoordinationState.java; Coordinator (the calls into CoordinationState: handleApplyCommit lines 419-452, handlePublishRequest 454-526, startElection 583-602, abdicateTo 604-620, ensureTermAtLeast 635-641, joinLeaderInTerm 643-657, setInitialConfiguration 1101-1161, handleJoin 1267-1299, publish 1350 with the handleClientValue call at 1404, CoordinatorPublication.handlePublishResponse 1871-1876): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/Coordinator.java; ClusterBootstrapService (requirements 146-157, onFoundPeersUpdated 176-215, startBootstrap 259-273, checkRequirements 306-335): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/ClusterBootstrapService.java#L146-L335; GatewayMetaState.start (persisted state loaded and rebuilt 141-231, remote restore 169-188, synchronous and asynchronous persistence 199-207, nodes without the cluster-manager and data roles 232-255): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/gateway/GatewayMetaState.java#L126-L257; PersistedStateRegistry.addPersistedState (lines 38-41): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/PersistedStateRegistry.java#L38-L41; ElectionStrategy (DEFAULT_INSTANCE lines 46-59): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/main/java/org/opensearch/cluster/coordination/ElectionStrategy.java#L44-L110; the randomized safety test CoordinationStateTestCluster (reboot 156-193, runRandomly 279-329, invariant 331-370): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/test/framework/src/main/java/org/opensearch/cluster/coordination/CoordinationStateTestCluster.java and CoordinationStateTests.testSafety (lines 917-924): https://github.com/opensearch-project/OpenSearch/blob/b2a54ba5c0defba3d1d20423e911d4bf920bab19/server/src/test/java/org/opensearch/cluster/coordination/CoordinationStateTests.java#L917-L924; for comparison, Elastic's TLA+ model ZenWithTerms (HandleCommit 241-250, CommittedValuesDescendantsFromInitialValue 338-347, CommitHasQuorumVsPreviousCommittedConfiguration 349-358): https://github.com/elastic/elasticsearch-formal-models/blob/ca30663506a7e18de9d23fabbc70a369903d7c98/ZenWithTerms/tla/ZenWithTerms.tla
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: OpenSearchSafety.inv_descends_linear
Notions used
Model and notation. Node ids are natural numbers. The parameters P consist of an eligibility function P.eligible:N→Bool (write E(x) for P.eligible(x)=true, "x is cluster-manager-eligible"), a predicate P.initConfs on voting configurations (the bootstrap configurations; it may be empty or have any number of elements), and an election strategy P.strategy. P.dn(n) is the discovery node with id n and cluster-manager flag P.eligible(n).
A voting configurationc is a strictly increasing list c.nodeIds of node ids, so two configurations are equal exactly when they contain the same ids; EMPTY is the empty one. For a list q of node ids, "q is a quorum of c", Lean c.hasQuorum q = true, means
2⋅{x∈c.nodeIds:x∈q}>∣c.nodeIds∣,
i.e. strictly more than half of the members of c occur in q; the order of q, repetitions in q and members of q outside c are irrelevant, and EMPTY has no quorum at all.
A cluster states has a term s.term∈N, a version s.version∈Z, a metadata version, a last committed configuration s.lcc, a last accepted configuration s.lac, an optional cluster-manager id, two Boolean cluster-UUID flags, and a payload of the arbitrary type V. A key is a pair (t,v)∈N×Z; the key of s is (s.term,s.version).
A clusterW consists of a coordination state W.node(n) for every natural number n and a predicate W.sent on messages ("has been sent"); for an arbitrary W the sent messages form an arbitrary, possibly infinite, set. Messages are of four kinds:
join(j), where a join j consists of a source discovery node j.src and a target discovery node j.tgt (each an id and a flag), a term j.term∈N, a last accepted term j.lat∈N and a last accepted version j.lav∈Z;
PR(src,s,pt,pv) (Lean .publishRequest src s pt pv), a publish request from node src carrying the cluster state s and the ghost parent key(pt,pv)∈N×Z;
RESP(a,d,(t,v)), a publish response from node a to node d for the key (t,v);
AC(σ,t,v), an apply-commit request with source discovery node σ and key (t,v).
A publication is a sent message PR(src,s,pt,pv); its key is the key of s and its parent is (pt,pv). In the model's steps, a node publishes PR(n,s,lat(n),lav(n)), so the ghost parent is the publisher's accepted key at publication time; but the statements below are about arbitrary clusters satisfying the invariant, where only the invariant constrains W.sent.
For a node n, read off its coordination state W.node(n): ln(n) its local discovery node; T(n) its current term; lat(n), lav(n), lcc(n), lac(n) the term, version, last committed and last accepted configuration of its last accepted state; won(n) its election-won flag; started(n) its started-join-since-last-reboot flag; lpv(n) and lpc(n) its last published version and configuration; JV(n) and PV(n) the node-id lists of its join votes and publish votes.
Committed keys.Committed(t,v) (Lean Committed W t v) holds iff some message AC(σ,t,v) has been sent, for some source discovery node σ. The definition alone does not require (t,v) to be the key of a publication.
Parent and descent.Parent((t,v),(t′,v′)) holds iff some publication PR(src,s,t,v) has been sent with (s.term,s.version)=(t′,v′): a publication of key (t′,v′) whose ghost parent is (t,v). Descent, Lean Descends W t v t' v', written here (t,v)⪯(t′,v′) and read "(t′,v′) descends from (t,v)" — the first key is the ancestor — is the least relation that is reflexive on every key and closed under appending a Parent step: (t,v)⪯(t′,v′) iff there is a chain (t,v)=k0,k1,…,km=(t′,v′) with m≥0 and Parent(ki,ki+1) for each i<m. It is reflexive on every key, published or not; a key that is not the key of any publication has no ancestor but itself. "Proper" descent means ⪯ between two different keys. For a publication PR(src,s,pt,pv) write key(s)=(s.term,s.version).
Auxiliary relations (over P and W.sent):
KeyLE(t,v;t′,v′): t<t′∨(t=t′∧v≤v′) — the lexicographic order on keys.
Below(lt,lv;bt,bv): lt<bt∨(lt=bt∧(bt=0∨lv≤bv)) — lexicographic, except that versions are not compared when both terms are 0.
Realizes(t,v,x,y), for configurations x (in the role of a last committed configuration) and y (last accepted): either (i) t=0, x=y and P.initConfs(x), with v unconstrained; or (ii) some publication PR(src,s,pt,pv) has key (t,v), y=s.lac, and either x=s.lcc or (x=s.lac and Committed(t,v)).
JoinVoters(L,t,J), for a list J of node ids: every x∈J has E(x) and there is a sent join(j) with j.src.id=x, j.tgt.id=L and j.term=t.
BaseVoters(L,t,bt,bv,J): the same, and moreover that join satisfies Below(j.lat,j.lav;bt,bv).
RespVoters(t,v,A): every x∈A has E(x) and RESP(x,d,(t,v)) has been sent for some node d (any destination).
ElectionRecord(L,t,bt,bv,x,y,J): bt<t, Realizes(bt,bv,x,y), BaseVoters(L,t,bt,bv,J), J is a quorum of x and J is a quorum of y. The key (bt,bv) is the base. (Since J is a quorum of x, x and J are non-empty; nothing requires L to have won or published, nor the base to be published.)
Won(b,t): E(b), and either (won(b) and T(b)=t) or some publication PR(b,s,pt,pv) from b has s.term=t.
The voter lists may be empty (the voter conditions then hold vacuously) and may contain repetitions.
The invariant Inv(P,W) is the conjunction of the following 30 fields (node variables range over all of N; "sent" means in W.sent; ⪯ is descent as above).
Every node n:
localNode_eq: ln(n)=P.dn(n).
idle_of_not_won: ¬won(n)⇒lpv(n)=0∧PV(n)=[].
won_started: won(n)⇒started(n).
votes_started: JV(n)=[]⇒started(n).
started_term_pos: started(n)⇒T(n)≥1.
won_quorum: won(n)⇒JV(n) is a quorum of lcc(n) and of lac(n).
not_won_no_quorum: ¬won(n)⇒¬(JV(n) is a quorum of lcc(n) and of lac(n)).
joinVotes_sent: every m∈JV(n) has E(m), and some sent join(j) has j.src.id=m, j.tgt.id=n, j.term=T(n) and Below(j.lat,j.lav;lat(n),lav(n)).
publishVotes_sent: every m∈PV(n) has E(m), and RESP(m,n,(T(n),lpv(n))) has been sent.
Every eligible node n (E(n)):
term_le: lat(n)≤T(n).
accepted_realizes: (lat(n)=0∧lcc(n)=lac(n)=EMPTY) or Realizes(lat(n),lav(n),lcc(n),lac(n)).
Joins and publish responses:
join_wf: every sent join(j) has j.src=P.dn(j.src.id) and E(j.tgt.id).
join_term_le: every sent join(j) with E(j.src.id) has j.term≤T(j.src.id).
join_unique: two sent joins with the same source id, that source eligible, and the same term are equal (all five fields, so in particular the same target).
join_after_response: for a sent join(j) and a sent RESP(a,d,(t,v)) with j.src.id=a, E(a) and t<j.term: KeyLE(t,v;j.lat,j.lav).
response_le: for a sent RESP(a,d,(t,v)) with E(a): t≤T(a) and KeyLE(t,v;lat(a),lav(a)).
response_published: for every sent RESP(a,d,(t,v)) there is a sent publication PR(d,s,pt,pv) from d with key (t,v).
Publications — for every sent PR(src,s,pt,pv):
pub_wf: E(src), s.term≥1, pt≤s.term, pv<s.version, and pt=0⇒P.initConfs(s.lcc).
pub_parent: if pt≥1, there is a sent PR(src′,s′,pt′,pv′) with key (pt,pv) such that (a) s.lcc=s′.lcc, or (s.lcc=s′.lac and Committed(pt,pv)); and (b) s.lac=s′.lac⇒s.lcc=s′.lac.
pub_unique: any two sent publications with the same key are the same message (same source, same cluster state in all fields, same parent).
pub_rooted: (0,v0)⪯key(s) for some v0∈Z.
pub_same_term: for every sent PR(src′,s′,pt′,pv′) with s′.term=s.term: key(s)⪯key(s′) or key(s′)⪯key(s).
pub_voters: some list J with JoinVoters(src,s.term,J) is a quorum of s.lcc and of s.lac.
pub_election: if pt<s.term, then ElectionRecord(src,s.term,pt,pv,s.lcc,y,J) for some configuration y and list J.
Commits:
commit_quorum: for every sent AC(σ,t,v) there are a sent PR(src,s,pt,pv) with key (t,v) and a list A with RespVoters(t,v,A), A a quorum of s.lac, and either A a quorum of s.lcc, or (Committed(pt,pv) and some sent publication of key (pt,pv) has last accepted configuration s.lac).
Leaders:
won_unique: Won(a,t)∧Won(b,t)⇒a=b.
pub_source_term: for every sent PR(src,s,pt,pv): s.term<T(src), or s.term=T(src) and (won(src) or ¬started(src)).
leader_pubs: if won(n), every sent PR(src,s,pt,pv) with s.term=T(n) (any source) has s.version≤lpv(n), key(s)⪯(T(n),lpv(n)), and JV(n) a quorum of s.lcc and of s.lac.
leader_inflight: if won(n) and lpv(n)=lav(n), then for some state s: PR(n,s,lat(n),lav(n)) is sent, s.term=T(n), s.version=lpv(n), lpc(n)=s.lac, and either lcc(n)=s.lcc, or (lcc(n)=s.lac, Committed(lat(n),lav(n)) and lac(n)=s.lac).
leader_settled: if won(n) and lpv(n)=lav(n), then either (lat(n)=T(n) and lpc(n)=lac(n)), or (lat(n)<T(n) and no sent publication has term T(n)).
1. Declaration
Binders and hypotheses, in order:
A type V (implicit), parameters P, a cluster W over V, and hinv : Inv(P,W).
t1,t2,t∈N and v1,v2,v∈Z.
h₁ : (t1,v1)⪯(t,v).
h₂ : (t2,v2)⪯(t,v).
Conclusion.
(t1,v1)⪯(t2,v2)∨(t2,v2)⪯(t1,v1).
Any two ancestors of the same key are related by descent: the ancestors of every key (the key itself included) are totally ordered by ⪯, i.e. lie on a single line. Lines of descent may branch but never merge.
2. What is not constrained
(t,v) is arbitrary. If it is not the key of any publication, both hypotheses can only be reflexivity and both ancestors equal (t,v).
Either ancestor may be (t,v) itself; when the two ancestors are equal both disjuncts hold.
Nothing is said about descendants: two keys with a common ancestor need not be comparable.
Nothing is said about whether the ancestors are published or committed.
The hypothesis Inv(P,W). The statement is about every cluster W satisfying the 30 fields, for arbitrary parameters P (any eligibility function, any bootstrap-configuration predicate, any election strategy) and any payload type V. Reachability is not assumed, and P.InitCompatible is not assumed. The hypothesis is satisfiable, so the statement is not vacuous on that account: every cluster in which no message has been sent and every node n is in the state init(P.dn(n),0,sn) for some state sn of term 0 with empty configurations (the model's Initial clusters) satisfies all 30 fields — fields 1, 2, 10 and 11 by direct check, field 7 because EMPTY has no quorum, the rest vacuously. On such clusters the conclusion is trivial. That clusters with publications and commits satisfy the invariant is the subject of other stubs (reachable_inv, which assumes P.InitCompatible and the default election strategy, and commit_reachable), not of this statement. Neither the statement nor Inv mentions P.strategy.
3. Plain-language summary
In any cluster satisfying the invariant, if a key descends from two keys, then one of those two descends from the other. The ancestry of every key is a single line.
Confirmed by the moderator at approval.