Opus 4.7 uses Apalache to search for inductive invariant candidates, then proves main theorem with TLAPS#212
Open
lemmy wants to merge 21 commits into
Open
Opus 4.7 uses Apalache to search for inductive invariant candidates, then proves main theorem with TLAPS#212lemmy wants to merge 21 commits into
lemmy wants to merge 21 commits into
Conversation
Add specifications/tcp/tcp_proof.tla discharging the static
obligations of the RFC 9293 abstract TCP FSM:
- TypeCorrect : Spec => []TypeOK (~268 obligations). The
inductive proof covers every action of Next (User, System and
Reset, plus stuttering); two new generic helper lemmas
PrefixOneNonEmpty and PrefixTwoNonEmpty are derived from the
IsPrefix definition so the per-action sub-proofs can chain Tail
and Append into Seq(Msgs).
- InvInit : Init => Inv. Direct from the initial-state predicate.
- PeersAB : explicit witnesses A, B with A # B and Peers = {A, B}
proven from the spec's `ASSUME Cardinality(Peers) = 2` plus an
added `ASSUME PeersFinite == IsFiniteSet(Peers)` (the spec's
bare cardinality assumption is intended to assert finiteness; in
TLA+ Cardinality is otherwise CHOOSE-based).
- InvIsAB : the convenient two-peer reformulation of Inv.
The full inductive step Inv /\ [Next]_vars => Inv' is left as future
work; it is documented at the bottom of the file as the substantive
remaining obligation.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add specifications/tcp/IndInv_apa.tla together with two cfg files:
- IndInv_apa_init.cfg drives the Init => IndInv check
(apalache-mc check --length=0 --init=Init --inv=IndInv)
- IndInv_apa.cfg drives the inductive-step check
(apalache-mc check --length=1 --init=IndInit --inv=IndInv)
After ~26 counterexample-driven iterations both obligations pass
(~5s and ~70s respectively) for the bounded model with MaxLen=4 and
Cardinality(Peers)=2, so
Spec => []IndInv => []Inv
holds for executions in which every queue stays bounded by MaxLen.
The strengthened invariant is
IndInv ==
TypeOKBounded
/\ BoundedNetwork
/\ Inv
/\ Aux_singleton_RST \* (i)
/\ Aux_singleton_ACK \* (ii)
/\ Aux_singleton_ACKofFIN \* (iii)
/\ Aux_EST_evidence \* (iv)
/\ Aux_LastMsg \* (v)
/\ Aux_RST_at_end \* (vi)
(i)-(iii) the three "singleton" trigger conditions that pin down
connstate[q] for each consume-only action whose post-
state can leave both networks empty (SynReceived RST/
ACK, FW1/Closing/LastAck on ACKofFIN, Note3 RST recv).
(iv) p in EST forces some TCP-message witness in either
queue (or q in some PostEst state) -- otherwise p has
no trace of the handshake.
(v) for q in {SR, FW1, CW, LA, CLOSING, SS} the LAST element
of n[p] equals the message q appended on entry to that
state. Reflects "while q stays in X, q does not append
to n[p]".
(vi) LastMsg(p)=RST forces q to be in {TIME-WAIT, CLOSED,
LISTEN} -- after Note3 send q can only transit through
those without re-touching n[p].
The file documents the iteration history. Several intermediate
candidates I tried (Aux_RST_head, Aux_q_PostEst,
Aux_FIN_handshake_predecessor with a "p in PostEst" disjunct,
Aux_SR_q_EST_has_ACK) were OVER-RESTRICTIVE: they were violated by
reachable post-states where Inv is vacuously true on a singleton
empty-queue case. The successful invariant is built from the
singleton triggers + a careful evidence clause for EST + per-state
LastMsg + the Note3-RST anchor.
The tcp.tla spec is unchanged; all bookkeeping (Apalache type
annotations, the symbolic IndInit havoc, the bounded NextBounded
transition) lives in the IndInv_apa module via INSTANCE tcp.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Translate the Apalache-discovered inductive strengthening into TLA+ definitions (HasMsg, LastMsg, PostEst, Aux_singleton_RST, Aux_singleton_ACK, Aux_singleton_ACKofFIN, Aux_EST_evidence, Aux_LastMsg, Aux_RST_at_end, IndInv) and discharge the static half: - IndInvInit : Init => IndInv (clause-by-clause). - IndInvStutter : IndInv /\ UNCHANGED vars => IndInv' (trivial). Refactor the existing TypeCorrect proof to expose TypeOKInductive as a re-usable lemma so the IndInv inductive step does not have to re-derive type preservation. Begin the action-based per-clause analysis for IndInvIsInductive in a sub-lemma IndInvUser: dispatch on the eight user actions, and fully discharge the PASSIVE_OPEN case (no network change, local -> LISTEN). The interesting sub-step is Aux_EST_evidence with q=local: when local transitions CLOSED -> LISTEN the "q in PostEst" disjunct is lost, but Inv (which excludes both networks empty when EST disagrees) forces a HasMsg disjunct via TypeOK and the Msgs alphabet. The remaining 7 user actions, the System and Reset cases, and the top-level IndInvIsInductive QED are all marked OMITTED as work-in-progress placeholders to be filled in incrementally. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Extend IndInvUser with the inductive-step proof for:
- CLOSE_SYN_SENT (no network change, SS -> CLOSED)
- CLOSE_LISTEN (no network change, LISTEN -> CLOSED)
- ACTIVE_OPEN (append "SYN" to n[remote], CLOSED -> SS)
- SEND (append "SYN" to n[remote], LISTEN -> SS)
The two no-network actions follow the PASSIVE_OPEN template:
- the singleton-aux's are preserved by IH except possibly when
q = local, in which case the post connstate is non-EST and
contradicts the IH's required q = EST;
- Aux_EST_evidence uses Inv to derive a HasMsg disjunct when the
"q in PostEst" disjunct is lost (only an issue for PASSIVE_OPEN);
- Aux_LastMsg and Aux_RST_at_end propagate via "connstate'[q] =
connstate[q] when q # local".
The two append-SYN actions exploit the 2-peer cardinality (PeersAB)
to force {p, q} = {local, remote} and derive contradictions in the
singleton-aux cases where the appended SYN is forced to equal RST,
ACK, or ACKofFIN. Aux_EST_evidence uses HasMsg("SYN", remote) as
the witness when q = local (post SS is not in PostEst).
Three CLOSE_* cases (CLOSE_SYN_RECEIVED, CLOSE_ESTABLISHED,
CLOSE_CLOSE_WAIT), all System actions, both Reset cases and the
top-level QED of IndInvIsInductive remain OMITTED placeholders.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Complete the User-action half of IndInvIsInductive by adding the
three remaining CLOSE_* sub-cases (CLOSE_SYN_RECEIVED,
CLOSE_ESTABLISHED, CLOSE_CLOSE_WAIT) to IndInvUser. All eight
user actions are now discharged:
PASSIVE_OPEN, ACTIVE_OPEN, SEND
CLOSE_SYN_SENT, CLOSE_LISTEN
CLOSE_SYN_RECEIVED, CLOSE_ESTABLISHED, CLOSE_CLOSE_WAIT
CLOSE_SYN_RECEIVED, CLOSE_ESTABLISHED both transition local into
FW1 (in PostEst) and CLOSE_CLOSE_WAIT into LA (also in PostEst),
so the Aux_EST_evidence q=local case discharges via the
"q in PostEst" disjunct directly. Aux_LastMsg with q=local matches
because the appended "FIN" satisfies both LastMsg(p) in {FIN, RST}
(for q=FW1) and LastMsg(p) = FIN (for q=LA).
Wire the User case into IndInvIsInductive via PICK + IndInvUser.
The System (10 actions) and Reset (Note3 send / RST receive) cases
remain OMITTED placeholders.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Open the System case of IndInvIsInductive by introducing the
helper lemma IndInvSystem and discharging the first two of its ten
sub-cases:
- TimeWait : TW -> CLOSED, no network change. Mirrors the
no-network user actions.
- Closing : CLOSING -> TIME-WAIT, consumes "ACKofFIN" from
n[local] (Tail), no append. This is the first action that
modifies a queue via Tail rather than Append. The proof
distinguishes Len(n[local]) = 1 (post n'[local] = <<>>) from
Len >= 2 (LastMsg(p)' = LastMsg(p), preserving Aux_LastMsg
and Aux_RST_at_end for p = local). The Inv preservation case
where l or r equals local relies on Aux_singleton_ACKofFIN
pre-state to force connstate[other] # ESTABLISHED.
Eight further system actions (SynSent SYN/SYN,ACK, SynReceived
RST/ACK, Listen, Established, FW1, FW2, LastAck, Note2) and the
two Reset cases remain OMITTED placeholders.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the LastAck system action sub-case (LA -> CLOSED, consume "ACKofFIN" via Tail). Identical structure to Closing apart from the post-state value (CLOSED instead of TIME-WAIT); both are non- ESTABLISHED and trigger the same disjunctive-Inv reasoning. 7 system actions remain OMITTED (SynSent SYN/SYN,ACK, SynReceived RST/ACK, Listen, Established, FW1, FW2, Note2) plus the 2 Reset sub-cases. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the Reset case to IndInvIsInductive via a new helper lemma
IndInvReset. The Note3 send sub-case (tcb[local], append "RST" to
n[remote], local -> TIME-WAIT) follows the append-only template:
- All singleton-aux's are vacuous because the appended "RST"
cannot equal "ACK" or "ACKofFIN" (and Aux_singleton_RST is
handled by post local = TW # EST).
- Aux_EST_evidence with q = local is satisfied by post connstate
= TW \in PostEst.
- Aux_LastMsg with q = local is vacuous (TW not in covered states
SR/FW1/CW/LA/CLOSING/SS).
- Aux_RST_at_end is satisfied by post connstate'[q] = TW in {TW,
CLOSED, LISTEN}.
The Note3 RST receive sub-case (Tail-only consume of "RST",
local -> LISTEN/CLOSED) remains OMITTED, as do the 7 remaining
System actions.
(In passing: an attempt at SynReceived using ASSUME ... PROVE
inside CASE triggered a tlapm internal assertion failure
(File "src/expr/e_levels.ml", line 502, characters 12-18).
Reverted; will revisit with a different proof structure.)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the Listen system action sub-case to IndInvSystem (Tail "SYN"
from n[local], Append "SYN,ACK" to n[remote], LISTEN -> SR).
This is the first Tail+Append action discharged for IndInv and
follows a hybrid template:
- For Inv': both queues post are non-empty for r = remote (Append
never empty), so {p : n'[p] = <<>>} reduces to {local}.
- For Aux_singleton_RST/ACK/ACKofFIN: q = remote is impossible
(Append makes n'[remote] non-empty); q = local has post
connstate = SR, which is # EST and # SR-ACK (so all three
singleton-aux post-conditions hold/are vacuous). In the
Aux_singleton_ACK case where p = remote, the appended
"SYN,ACK" cannot equal the singleton "ACK", giving a
contradiction.
- For Aux_EST_evidence: q = local post = SR # EST means p must be
remote; HasMsg("SYN,ACK", p)' is satisfied by the just-appended
SYN,ACK at network'[p][Len(network'[p])].
- For Aux_LastMsg: q = local post = SR forces LastMsg(p)' =
"SYN,ACK", which holds exactly because p = remote and the
appended message is "SYN,ACK". q = remote uses the unchanged
connstate together with the Tail-LastMsg invariance for
Len >= 2.
- For Aux_RST_at_end: p = remote gives LastMsg(p)' = "SYN,ACK"
# "RST" (vacuous); p = local goes through the Tail-LastMsg
invariance pattern from the Closing/LastAck proofs.
6 system actions remain OMITTED (SynSent x 2, SynReceived x 2,
Established, FW1 x 2, FW2, Note2) plus Note3 RST receive.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the Established system action sub-case (Tail "FIN" from
n[local], Append "ACKofFIN" to n[remote], local EST -> CW).
Identical structure to Listen (Tail+Append) apart from:
- pre/post connstate values (LISTEN -> SR vs EST -> CW)
- the head being consumed ("SYN" vs "FIN")
- the message being appended ("SYN,ACK" vs "ACKofFIN").
Aux_LastMsg with q = local matches because the appended
"ACKofFIN" satisfies LastMsg(p) = "ACKofFIN" for q = CLOSE-WAIT.
Aux_EST_evidence with q = local is satisfied by post connstate
'[local] = CW \in PostEst.
5 system actions remain OMITTED (SynSent x 2, SynReceived x 2,
FW1 x 2, FW2, Note2) plus Note3 RST receive.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the FinWait2 system action sub-case (Tail "FIN" from
n[local], Append "ACKofFIN" to n[remote], local FW2 -> TW).
Identical structure to Listen/Established (Tail+Append). The
post connstate'[local] = TW differs: Aux_LastMsg with q = local
becomes vacuous (TW not in covered states) and Aux_RST_at_end
with q = local is satisfied directly via TW \in {TW, CLOSED,
LISTEN}.
4 system actions remain OMITTED (SynSent x 2, SynReceived x 2,
FW1 x 2, Note2) plus Note3 RST receive.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the Note3 RST receive sub-case (Tail "RST" from n[local],
local pre any -> post in {LISTEN, CLOSED}, post # ESTABLISHED).
Key observations:
- Inv': uses Aux_singleton_RST pre when r # local has empty
queue and pre n[local] = <<"RST">>.
- Aux_singleton_RST': split p = local (pre had two RSTs;
LastMsg = "RST" so Aux_RST_at_end gives connstate[q] in
{TW, CLOSED, LISTEN}, preserved post) vs p # local (pre
n[p] = <<"RST">>, n[q] = <<>>; pre Aux gives connstate[q]
# EST, preserved).
- Aux_singleton_ACK': p = local impossible (post # SR);
q = local case requires pre n[local] = <<"RST">> exactly so
that LastMsg(local) = "RST" contradicts Aux_LastMsg pre at
p = remote SR (which requires LastMsg(local) = "SYN,ACK").
- Aux_EST_evidence': pre p = remote in EST so q = local; case
on n[local] = <<"RST">> (then n[remote] # <<>> by
Aux_singleton_RST contrapositive, gives HasMsg evidence on
p = remote) vs Len(n[local]) >= 2 (then n'[local] # <<>>,
HasMsg(network'[local][1], q) holds).
Reset action fully discharged. 3 system actions remain OMITTED
(SynSent x 2, SynReceived x 2, FW1 x 2, Note2).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add the Note2 system action sub-case (consume <<"FIN", "ACKofFIN">> from head of n[local] via SubSeq(_, 3, _), Append "ACKofFIN" to n[remote], local FW1 -> TW). Same overall structure as Established/FW2, but consumes 2 head elements instead of 1. Length bookkeeping uses SubSeqProperties to derive Len(n'[local]) = Len(n[local]) - 2 and the index shift n'[local][i] = n[local][i + 2]. Aux_LastMsg with p = local: when n'[local] # <<>> we have Len(n[local]) >= 3, so the last element is preserved across SubSeq, giving LastMsg(p)' = LastMsg(p). Aux_RST_at_end follows the same pattern. All other auxs are vacuous on q = local with post TW. 3 system actions remain OMITTED (SynSent x 2, SynReceived x 2, FW1 x 2). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add both FinWait1 sub-cases:
(a) head = FIN, n'[remote] = Append "ACKofFIN", local FW1 ->
CLOSING. Same Tail+Append shape as Established/FW2. Post
CLOSING is in PostEst (Aux_EST_evidence) and in covered
states (Aux_LastMsg) where LastMsg(remote)' = "ACKofFIN"
from the appended message.
(b) head = ACKofFIN, no append, local FW1 -> FIN-WAIT-2. Same
Tail-only shape as Note3 RST receive. Inv preservation
uses Aux_singleton_ACKofFIN pre at p = local (n[local] =
<<"ACKofFIN">>, FW1 \in {FW1, CLOSING, LA}) to derive
connstate[r] # EST when r # local has empty queue. The
Aux_singleton_ACK case forces n'[local] = <<>> hence n[local]
= <<"ACKofFIN">>, then LastMsg(local) = "ACKofFIN" #
"SYN,ACK" contradicts Aux_LastMsg pre at q = remote SR.
2 system actions remain OMITTED: SynSent x 2, SynReceived x 2.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add both SynSent sub-cases (each Tail head + Append to remote):
(a) head = SYN, append SYN,ACK to n[remote], local SS -> SR.
Aux_singleton_RST/ACK/ACKofFIN with q = local satisfied
directly (post SR # EST and not in {FW1, CLOSING, LA}).
Aux_EST_evidence with q = local SR (not in PostEst) is
satisfied via HasMsg("SYN,ACK", p = remote)' which holds
because we just appended SYN,ACK; we use a literal index
of Len(network[p]) + 1 to avoid an internal tlapm bug
(e_levels.ml assertion) triggered by WITNESS expressions
involving Len(network'[p]).
(b) head = SYN,ACK, append ACK to n[remote], local SS -> EST.
Aux_singleton_ACK with q = local satisfied trivially
(post = EST). Aux_EST_evidence with p = local = EST, q =
remote uses HasMsg("ACK", q = remote)' (just appended).
Aux_RST_at_end uses LastMsg(p)' = ACK # RST when p = remote
and the inductive hypothesis when p = local.
1 system action remains OMITTED: SynReceived x 2.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Add both SynReceived sub-cases (each Tail head, no append):
(a) head = RST, local SR -> LISTEN. Mirror of Note3 RST receive
(post LISTEN \in {TW, CLOSED, LISTEN} so Aux_RST_at_end is
direct; Aux_singleton_ACK with q = local forces n[local] =
<<"RST">>, then LastMsg(local) = "RST" contradicts Aux_LastMsg
pre at q = remote SR; Aux_EST_evidence uses the case-split
n[local] = <<"RST">> (then Aux_singleton_RST contrapositive
forces n[remote] # <<>>, giving HasMsg evidence on p = remote)
vs Len(n[local]) >= 2 (giving HasMsg evidence on q = local).
(b) head = ACK, local SR -> ESTABLISHED. Mirror of SynSent SYN,ACK
sub-case but without the appended message.
- Aux_singleton_RST: split q = local (p = remote, n[remote]
= <<"RST">>; pre Aux_RST_at_end forces connstate[local] in
{TW, CLOSED, LISTEN}, contradicting SR) vs q = remote
(p = local, n[local] = <<"ACK", "RST">>; pre Aux_RST_at_end
gives connstate[remote] in {TW, CLOSED, LISTEN}, preserved).
- Aux_singleton_ACKofFIN with q = local forces n[local] =
<<"ACK">>, then LastMsg(local) = "ACK" contradicts Aux_LastMsg
pre at q = remote in {FW1, CLOSING, LA} (which respectively
require LastMsg = FIN/RST, ACKofFIN, FIN).
- Aux_EST_evidence: p = remote case is trivial (q = local
post EST \in PostEst). p = local case splits on
Len(n[local]): >= 2 gives HasMsg(n'[local][1], p)' direct;
= 1 (i.e., n[local] = <<"ACK">>) further splits on
n[remote]: empty gives connstate[remote] = EST via
Aux_singleton_ACK pre, hence q = remote post in PostEst;
non-empty gives LastMsg(remote) = "SYN,ACK" via Aux_LastMsg
pre, hence HasMsg("SYN,ACK", q = remote)' (unchanged).
ALL 19 actions are now discharged. Total: 4525 obligations.
The IndInv inductive invariant for the tcp spec is fully proven.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
With IndInvIsInductive fully proven, the standard PTL liveness
argument yields:
THEOREM SpecImpliesIndInv == Spec => []IndInv
via IndInvInit, IndInvIsInductive, PTL.
THEOREM SpecImpliesInv == Spec => []Inv
via IndInv => Inv (by definition) and SpecImpliesIndInv.
This completes the full TLAPS safety proof for the tcp spec
without modifying any original spec files. Total: 4540
obligations across 5665 lines.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
The previous commits added the TLAPS proof companion tcp_proof.tla but omitted it from specifications/tcp/manifest.json, so CI's manifest-files check was failing and the proof was never run by .github/workflows/CI.yml's "Check proofs" step (which iterates over modules carrying a "proof" field). Add a module entry for tcp_proof.tla with an empty models array and a proof.runtime of 00:51:00 (the wall-clock time of the most recent successful tlapm run that discharged all 4540 obligations on this machine). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
IndInv_apa.tla is the Apalache-based inductive-invariant search whose final invariant is now discharged in TLAPS by tcp_proof.tla. There is no value in re-running its two configurations (IndInv_apa_init.cfg checks Init => IndInv in ~5s, IndInv_apa.cfg checks IndInv preservation in ~70s) on every CI build, especially given that .github/workflows/CI.yml overrides --length=5 uniformly, which doesn't match either configuration's intended semantics. Add a module entry for IndInv_apa.tla with an empty models array (mirroring the existing APDistributedReplicatedLog.tla pattern in specifications/FiniteMonotonic/manifest.json). This makes SANY parse the file in CI's "Parse all modules" step but no model checker ever runs it. Add the two .cfg files to .ciignore so check_manifest_files.py does not flag them as missing from any manifest. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
CI's check_markdown_table.py compares the proof column in the README's
spec table against the presence of any module with a "proof" field in
specifications/<spec>/manifest.json. After listing tcp_proof.tla in the
manifest, the table was out of sync:
ERROR: Proof flags in README.md table differ from features in
manifest.json:
Spec specifications/tcp is missing a proof flag in README.md table
Add the checkmark to the TLAPS Proof column for the tcp row.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
Member
Author
|
@muenchnerkindl This might interest you as well |
Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full TLAPS safety proof for
tcp.tlaThis PR adds a complete, machine-checked safety proof of the abstract RFC 9293 TCP state machine in
specifications/tcp/tcp.tla. The original spec is not modified — every obligation is discharged in two new files.Result
THEOREM SpecImpliesInv == Spec => []InvTHEOREM SpecImpliesIndInv == Spec => []IndInvtcp_proof.tlasizeInvis the existing two-peer "both queues empty implies same EST status" invariant fromtcp.tla;IndInvis a 6-clause auxiliary strengthening discovered with Apalache and ported back to TLAPS.Files added
specifications/tcp/tcp_proof.tla— full TLAPS proofspecifications/tcp/IndInv_apa.tla— Apalache module used to discoverIndInvspecifications/tcp/IndInv_apa.cfg,IndInv_apa_init.cfg— Apalacheconfigurations for
Init => IndInvandIndInv /\ [Next] => IndInv'Approach (3 phases)
Phase 1 — Static obligations (commit
1758f209)Started with the obligations that don't require an inductive strengthening:
TypeCorrect == Spec => []TypeOK(~268 obligations) — refactored into areusable
TypeOKInductivelemma to keep the top-level theorem tractable.InvInit == Init => Inv— direct from the initial-state predicate.PrefixOneNonEmptyandPrefixTwoNonEmpty,derived from the unfolded
IsPrefixdefinition, used throughout the rest ofthe proof to chain
Tail/AppendthroughSeq(Msgs).ASSUME PeersFinite == IsFiniteSet(Peers)(the spec's bareCardinality(Peers) = 2only implies cardinality, not finiteness, in TLA+).PeersABandInvIsABto enable explicit two-peer reasoning.After phase 1, the remaining obstacle was the inductive step
Inv /\ [Next]_vars => Inv', which is not inductive on its own — the spec needs auxiliary invariants correlating connection states with queue contents.Phase 2 — Discovering
IndInvwith Apalache (commitb47fe66c)Built a dedicated Apalache module
IndInv_apa.tla(extendstcpviaINSTANCE):TypeOKwithTypeOKBounded + BoundedNetwork(
Len(network[p]) <= MaxLenwithMaxLen = 4).IndInit(havoc state subject toIndInv) andNextBounded.clause. Several intermediate candidates (
Aux_RST_head,Aux_q_PostEst,Aux_FIN_handshake_predecessor,Aux_SR_q_EST_has_ACK) had to be discardedas over-restrictive: they excluded reachable Inv-respecting states where
Invwas only vacuously true.The converged invariant has 6 auxiliary clauses on top of
TypeOK /\ Inv:Aux_singleton_RST— ifn[p] = <<"RST">>andn[q] = <<>>, thenconnstate[q] # EST.Aux_singleton_ACK— ifn[p] = <<"ACK">>,n[q] = <<>>andconnstate[p] = SR, thenconnstate[q] = EST.Aux_singleton_ACKofFIN— ifn[p] = <<"ACKofFIN">>,n[q] = <<>>andconnstate[p] in {FW1, CLOSING, LA}, thenconnstate[q] # EST.Aux_EST_evidence— ifconnstate[p] = EST, then eitherconnstate[q]has reached a post-EST state or some handshake message (SYN, ACK, SYN,ACK, FIN,ACKofFIN, RST) is still in
n[p]orn[q].Aux_LastMsg— for eachconnstate[q] in {SR, FW1, CW, LA, CLOSING, SS},LastMsg(n[p])is pinned to the messageqappended on entry to that state (e.g.q = SRpins it to"SYN,ACK").Aux_RST_at_end— ifLastMsg(n[p]) = "RST", thenconnstate[q] in {TIME-WAIT, CLOSED, LISTEN}.Apalache established (with
MaxLen = 4,Cardinality(Peers) = 2):Init => IndInvin ~5 sIndInv /\ [Next]_vars => IndInv'in ~70 sApalache established (for
MaxLen = 4):Init => IndInvin ~5 sIndInv /\ [Next]_vars => IndInv'in ~70 sPhase 3 — Unbounded TLAPS proof of
IndInv(commits5c821be…201a14f)Translated the bounded Apalache result into a fully unbounded TLAPS proof,
proceeding one action at a time so each commit ran TLAPS to completion:
IndInvInit,IndInvStutter5c821becb290c8mton[remote]User)9e201fdTimeWait,Closing006cbffLastAck881d857Note3 send(Reset)2e27b5aListenac749edEstablished246e0f6CW ∈ PostEstFinWait261d2b21TW(vacuous on covered states)Note3 RST receive1ed3985n[local] = <<RST>>vsLen ≥ 2Note2e1cdcb1SubSeq(_, 3, _)consume 2 + appendFinWait1(FIN, ACKofFIN)374cafbSynSent(SYN, SYN,ACK)0c7cba3SynReceived(RST, ACK)0a1ae9bSpecImpliesIndInv,SpecImpliesInv201a14fThe "transitions into EST" subcases in
SynSentandSynReceivedwere the most subtle —Aux_EST_evidence'requires a case-split onLen(n[local])and on whethern[remote]is empty to dispatch the disjunction.Notes / lessons
tlapmcrash (src/expr/e_levels.ml: Assertion failed) is triggered byWITNESS Len(network'[p]) \in 1..Len(network'[p]). Workaround: replace the witness with the literal valueLen(network[p]) + 1(which TLAPS can prove lies in1..Len(network'[p])from theAppendlength identity).IndInvis meaningfully smaller than the first plausible candidates.q, then onp), which makes future maintenance and review tractable despite the file's overall size.SynReceived); the cumulative full-rebuild time is ~3 hours.