Skip to content

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
masterfrom
mku-tlaips_tcp
Open

Opus 4.7 uses Apalache to search for inductive invariant candidates, then proves main theorem with TLAPS#212
lemmy wants to merge 21 commits into
masterfrom
mku-tlaips_tcp

Conversation

@lemmy
Copy link
Copy Markdown
Member

@lemmy lemmy commented May 12, 2026

Full TLAPS safety proof for tcp.tla

This 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

Top-level theorem THEOREM SpecImpliesInv == Spec => []Inv
Strengthening THEOREM SpecImpliesIndInv == Spec => []IndInv
Proof obligations 4540 (all proved by TLAPS, 0 OMITTED)
tcp_proof.tla size 5665 lines
Backends used Z3, Zenon, Isabelle/TLA+, PTL

Inv is the existing two-peer "both queues empty implies same EST status" invariant from tcp.tla; IndInv is a 6-clause auxiliary strengthening discovered with Apalache and ported back to TLAPS.

Files added

  • specifications/tcp/tcp_proof.tla — full TLAPS proof
  • specifications/tcp/IndInv_apa.tla — Apalache module used to discover IndInv
  • specifications/tcp/IndInv_apa.cfg, IndInv_apa_init.cfg — Apalache
    configurations for Init => IndInv and IndInv /\ [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 a
    reusable TypeOKInductive lemma to keep the top-level theorem tractable.
  • InvInit == Init => Inv — direct from the initial-state predicate.
  • Added two generic helper lemmas PrefixOneNonEmpty and PrefixTwoNonEmpty,
    derived from the unfolded IsPrefix definition, used throughout the rest of
    the proof to chain Tail/Append through Seq(Msgs).
  • Added an explicit ASSUME PeersFinite == IsFiniteSet(Peers) (the spec's bare
    Cardinality(Peers) = 2 only implies cardinality, not finiteness, in TLA+).
  • Proved PeersAB and InvIsAB to enable explicit two-peer reasoning.

After phase 1, the remaining obstacle was the inductive stepInv /\ [Next]_vars => Inv', which is not inductive on its own — the spec needs auxiliary invariants correlating connection states with queue contents.

Phase 2 — Discovering IndInv with Apalache (commit b47fe66c)

Built a dedicated Apalache module IndInv_apa.tla (extends tcp via
INSTANCE):

  • Replaced the unbounded TypeOK with TypeOKBounded + BoundedNetwork
    (Len(network[p]) <= MaxLen with MaxLen = 4).
  • Defined IndInit (havoc state subject to IndInv) and NextBounded.
  • Iterated:
    apalache-mc check --length=0 --init=Init    --inv=IndInv  IndInv_apa.tla   # base
    apalache-mc check --length=1 --init=IndInit --inv=IndInv  IndInv_apa.tla   # step
  • Each counterexample (~26 in total) produced a refinement of one auxiliary
    clause. Several intermediate candidates (Aux_RST_head, Aux_q_PostEst,
    Aux_FIN_handshake_predecessor, Aux_SR_q_EST_has_ACK) had to be discarded
    as over-restrictive: they excluded reachable Inv-respecting states where
    Inv was only vacuously true.

The converged invariant has 6 auxiliary clauses on top of TypeOK /\ Inv:

  • Aux_singleton_RST — if n[p] = <<"RST">> and n[q] = <<>>, then connstate[q] # EST.
  • Aux_singleton_ACK — if n[p] = <<"ACK">>, n[q] = <<>> and connstate[p] = SR, then connstate[q] = EST.
  • Aux_singleton_ACKofFIN — if n[p] = <<"ACKofFIN">>, n[q] = <<>> and connstate[p] in {FW1, CLOSING, LA}, then connstate[q] # EST.
  • Aux_EST_evidence — if connstate[p] = EST, then either connstate[q] has reached a post-EST state or some handshake message (SYN, ACK, SYN,ACK, FIN,
    ACKofFIN, RST) is still in n[p] or n[q].
  • Aux_LastMsg — for each connstate[q] in {SR, FW1, CW, LA, CLOSING, SS}, LastMsg(n[p]) is pinned to the message q appended on entry to that state (e.g. q = SR pins it to "SYN,ACK").
  • Aux_RST_at_end — if LastMsg(n[p]) = "RST", then connstate[q] in {TIME-WAIT, CLOSED, LISTEN}.
    Apalache established (with MaxLen = 4, Cardinality(Peers) = 2):
  • Init => IndInv in ~5 s
  • IndInv /\ [Next]_vars => IndInv' in ~70 s

Apalache established (for MaxLen = 4):

  • Init => IndInv in ~5 s
  • IndInv /\ [Next]_vars => IndInv' in ~70 s

Phase 3 — Unbounded TLAPS proof of IndInv (commits 5c821be201a14f)

Translated the bounded Apalache result into a fully unbounded TLAPS proof,
proceeding one action at a time so each commit ran TLAPS to completion:

Action(s) Commit Pattern
IndInvInit, IndInvStutter 5c821be direct
5 user actions (no-network-change + append-only) cb290c8 append m to n[remote]
8 user actions (all of User) 9e201fd same
TimeWait, Closing 006cbff TimeWait: no network change; Closing: tail-only
LastAck 881d857 tail-only
Note3 send (Reset) 2e27b5a append-only
Listen ac749ed tail + append
Established 246e0f6 tail + append, post CW ∈ PostEst
FinWait2 61d2b21 tail + append, post TW (vacuous on covered states)
Note3 RST receive 1ed3985 tail-only, q=local case-split on n[local] = <<RST>> vs Len ≥ 2
Note2 e1cdcb1 SubSeq(_, 3, _) consume 2 + append
FinWait1 (FIN, ACKofFIN) 374cafb two sub-cases mirroring Established and Note3 RST
SynSent (SYN, SYN,ACK) 0c7cba3 two sub-cases — sub-case (b) transitions into EST
SynReceived (RST, ACK) 0a1ae9b two sub-cases — sub-case (b) transitions into EST; ~700 lines
SpecImpliesIndInv, SpecImpliesInv 201a14f PTL closing

The "transitions into EST" subcases in SynSent and SynReceived were the most subtle — Aux_EST_evidence' requires a case-split on Len(n[local]) and on whether n[remote] is empty to dispatch the disjunction.

Notes / lessons

  • A reproducible internal tlapm crash (src/expr/e_levels.ml: Assertion failed) is triggered by WITNESS Len(network'[p]) \in 1..Len(network'[p]). Workaround: replace the witness with the literal value Len(network[p]) + 1 (which TLAPS can prove lies in 1..Len(network'[p]) from the Append length identity).
  • Many "obviously true" structural invariants were rejected by Apalache as over-restrictive. The minimal six-clause IndInv is meaningfully smaller than the first plausible candidates.
  • Each per-action sub-proof follows the same skeleton (USE/SUFFICES/CASE on q, then on p), which makes future maintenance and review tractable despite the file's overall size.
  • TLAPS runtime per action ranges from ~6 min (no-network actions) to ~25 min (SynReceived); the cumulative full-rebuild time is ~3 hours.

lemmy and others added 19 commits May 11, 2026 20:41
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>
@lemmy lemmy self-assigned this May 12, 2026
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>
@lemmy lemmy requested a review from muenchnerkindl May 12, 2026 04:07
@lemmy
Copy link
Copy Markdown
Member Author

lemmy commented May 12, 2026

@muenchnerkindl This might interest you as well

Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
@lemmy lemmy marked this pull request as ready for review May 12, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant