Close Menu

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Honda CR-V Hybrid Lineup Expanded in Malaysia From RM178,200

    vivo V70 – Top 7 Flagship Features You Will Love

    Apple iPad Air with M4 Officially Launches in Malaysia From RM2,799

    Facebook X (Twitter) Instagram
    • Artificial Intelligence
    • Business Technology
    • Cryptocurrency
    • Gadgets
    • Gaming
    • Health
    • Software and Apps
    • Technology
    Facebook X (Twitter) Instagram Pinterest Vimeo
    Tech AI Verse
    • Home
    • Artificial Intelligence

      What the polls say about how Americans are using AI

      February 27, 2026

      Tensions between the Pentagon and AI giant Anthropic reach a boiling point

      February 21, 2026

      Read the extended transcript: President Donald Trump interviewed by ‘NBC Nightly News’ anchor Tom Llamas

      February 6, 2026

      Stocks and bitcoin sink as investors dump software company shares

      February 4, 2026

      AI, crypto and Trump super PACs stash millions to spend on the midterms

      February 2, 2026
    • Business

      Weighing up the enterprise risks of neocloud providers

      March 3, 2026

      A stolen Gemini API key turned a $180 bill into $82,000 in two days

      March 3, 2026

      These ultra-budget laptops “include” 1.2TB storage, but most of it is OneDrive trial space

      March 1, 2026

      FCC approves the merger of cable giants Cox and Charter

      February 28, 2026

      Finding value with AI and Industry 5.0 transformation

      February 28, 2026
    • Crypto

      Strait of Hormuz Shutdown Shakes Asian Energy Markets

      March 3, 2026

      Wall Street’s Inflation Alarm From Iran — What It Means for Crypto

      March 3, 2026

      Ethereum Price Prediction: What To Expect From ETH In March 2026

      March 3, 2026

      Was Bitcoin Hijacked? How Institutional Interests Shaped Its Narrative Since 2015

      March 3, 2026

      XRP Whales Now Hold 83.7% of All Supply – What’s Next For Price?

      March 3, 2026
    • Technology

      Spotify’s new feature makes it easier to find popular audiobooks

      March 3, 2026

      This portable JBL Grip Bluetooth speaker is so good at 20% off

      March 3, 2026

      ‘AI’ could dox your anonymous posts

      March 3, 2026

      Microsoft says new Teams location feature isn’t for ’employee tracking’

      March 3, 2026

      OpenAI got ‘sloppy’ about the wrong thing

      March 3, 2026
    • Others
      • Gadgets
      • Gaming
      • Health
      • Software and Apps
    Check BMI
    Tech AI Verse
    You are at:Home»Technology»Proving Liveness with TLA
    Technology

    Proving Liveness with TLA

    TechAiVerseBy TechAiVerseJanuary 3, 2026No Comments27 Mins Read4 Views
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr Email Reddit
    Proving Liveness with TLA
    Share
    Facebook Twitter LinkedIn Pinterest WhatsApp Email

    Proving Liveness with TLA

    The TLA Toolbox now has support for proving liveness properties (i.e. that something will eventually happen).
    I try it out on the Xen vchan protocol.

    Working on a liveness proof with the TLA+ Toolbox.

    Table of Contents

    • Background
    • A simple channel specification
      • Specification in terms of actions
      • Invariants
    • Temporal logic
      • Proving temporal claims with TLAPS
      • Generalising
      • Hiding definitions
    • Proving liveness (simple case)
    • Multi-step liveness
    • The real protocol
      • Proving ReadLimit
      • Proving Availability
      • Proving WriteLimit
      • End-to-end liveness
    • Work-arounds for bugs
      • SUFFICES doesn’t always generalise
      • CASE with a temporal goal
      • PTL and primes
      • Syntax matters
      • Other bugs
    • Conclusions

    Background

    The vchan protocol is used for efficient communication between Xen virtual machines.
    In 2018, I made a TLA+ specification of vchan:
    I created a specification of the protocol from the C code,
    used the model checker (TLC) to test that the protocol worked on small models,
    and wrote a machine-verified proof of Integrity (that the data received always matched what was sent).
    I also outlined a proof of Availability (that data sent will eventually arrive, rather than the system deadlocking or going around in circles). But:

    Disappointingly, we can’t actually prove Availability using TLAPS, because
    currently it understands very little temporal logic

    However, newer versions of TLAPS (the TLA Proof System) have added proper support for temporal logic,
    so I decided to take another look.
    In this post, I’ll start with a simple example of proving a liveness property,
    and then look at the real protocol.
    If you’re not familiar with TLA, you might want to read the earlier post first,
    though I’ll briefly introduce concepts as we meet them here.

    A simple channel specification

    We’ll start with a simple model of a one-way channel.
    There is a sender and a receiver, and they have access to a shared buffer of size BufferSize
    (which is a non-zero natural number):

    1
    2
    
    CONSTANT BufferSize 
    ASSUME BufferSizeType == BufferSize in Nat  {0}
    

    The model just keeps track of the total number of bytes sent and received (not the actual data):

    1
    2
    
    VARIABLES Sent, Got
    vars == << Sent, Got >>     * For referring to both variables together
    

    The amount of data currently in the buffer (BufferUsed) is the difference between these counters,
    and the free space (BufferFree) is the difference between that and the total buffer size:

    1
    2
    
    BufferUsed == Sent - Got
    BufferFree == BufferSize - BufferUsed
    

    The property we are interested in (Liveness) is that data sent eventually arrives:

    1
    2
    3
    4
    
    Liveness ==
      A n in Nat :    * For all natural numbers n:
        Sent = n ~>     * if we have sent exactly n bytes, then eventually
          Got >= n      * we'll have received at least that many.
    

    Specification in terms of actions

    Liveness is the property we want, but we also need to say how we plan to achieve this.
    In TLA, this is done by describing the initial state and the actions that can be performed
    at each step.

    Initially, no bytes have been sent or received:

    1
    2
    3
    
    Init ==
      / Sent = 0
      / Got = 0
    

    An action only looks at a single atomic step of the algorithm, and relates the values of the variables
    at the start of the step (e.g. Sent) to their values at the end (written primed, e.g. Sent').

    The Send action is true when the sender increases Sent
    (by some amount n, limited by the free space in the buffer):

    1
    2
    3
    4
    
    Send ==
      E n in 1..BufferFree :
        / Sent' = Sent + n
        / UNCHANGED Got
    

    The Recv action is true when the receiver reads all of the data currently in the buffer:

    1
    2
    3
    4
    
    Recv ==
      / BufferUsed > 0             * Buffer must be non-empty
      / Got' = Got + BufferUsed
      / UNCHANGED Sent
    

    Every action of our system is either a Send or a Recv:

    1
    2
    3
    
    Next ==
      / Send
      / Recv
    

    Finally, Spec describes the behaviours of the whole system in these terms:

    1
    2
    3
    4
    
    Spec ==
      / Init              * Init is true initially
      / [][Next]_vars     * Each step is a Next step, or leaves vars unchanged
      / WF_vars(Recv)     * The receiver must eventually read, if it can
    

    Invariants

    It’s always useful to prove some basic invariants about the system
    (in particular, the types of the variables):

    1
    2
    3
    4
    5
    
    I ==
      / Sent in Nat
      / Got in Nat
      / Sent >= Got                    * We can't receive more than was sent
      / BufferUsed in 0..BufferSize   * The buffer doesn't overflow
    

    Before trying to prove anything, it’s good to use the model checker (TLC) to test it first.
    You’ll need to put some limits on the number of bytes sent and checked in the model,
    so it isn’t a full proof, but it’s likely to spot most problems.
    In this example, it passes easily.

    The general idea for proving things in TLA is to get away from temporal logic as quickly as possible
    and do most of the proof with simple actions.
    The following pattern should work for proving any invariant:

    1
    2
    3
    4
    
    THEOREM AlwaysI == Spec => []I
    <1> Init => I BY BufferSizeType DEF Init, I
    <1> I / [Next]_vars => I' BY NextPreservesI DEF vars, I
    <1> QED BY PTL DEF Spec
    

    This says that Spec implies that I is always true because:

    • I is true initially.
    • Next steps preserve I (and so does leaving vars unchanged).
    • Therefore Spec ensures I will always be true.

    Note: The <1> at the start of each step is the indentation level,
    which for some reason TLAPS can’t work out by itself.

    The final QED BY PTL (“by Propositional Temporal Logic”) is the only step that uses temporal logic.
    Everything else is just regular logic.

    Now all we need to do is prove that the Next action preserves I, which is straight-forward:

    1
    2
    3
    4
    5
    6
    
    LEMMA NextPreservesI ==
      ASSUME I, Next
      PROVE  I'
    <1> CASE Send BY BufferSizeType DEF I, Send
    <1> CASE Recv BY BufferSizeType DEF I, Recv
    <1> QED BY DEF Next
    

    So for proving invariants like I we can mostly ignore temporal logic.
    By for proving liveness, we’ll need to understand more about it…

    Temporal logic

    Temporal logic is a type of modal logic.
    In a modal logic we imagine there are many worlds, and we are in one of them.
    Ordinary mathematical statements refer to our world,
    so e.g. Sent = 4 refers to the value of Sent in our world.
    However, by using the modal operators, you can refer to other worlds.
    For example, [](Sent = 4) means that Sent = 4 is true in all worlds “reachable” from the current one.

    In TLA, each world corresponds to a moment in time,
    and current and future times are reachable.
    So []X means “X will always be true”.
    <>X means “X will eventually be true”.
    For example:

    • [](Sent = 4) means that Sent = 4 is true now and always will be.
    • <>(Sent = 4) means that Sent = 4 is either true now or will be true at some point in the future.

    Modal operators nest, so you can say things like <>[](Sent = 4)
    (eventually Sent will equal 4 and will remain so from then onwards),
    or [](F => <>G) (F always leads to G, which can also be written as F ~> G).

    Proving temporal claims with TLAPS

    In the proof of Spec => []I above, only one step required temporal logic (BY PTL).
    Having few such steps is good because these steps can be tricky.

    The main problem is that TLA propositions are written in First Order Modal Logic,
    but TLAPS doesn’t have any solvers that understand this!
    Instead, it has a number of solvers that understand regular non-modal logic,
    plus the PTL decision procedure that handles modal logic but not much else.

    For example, consider this apparently simple claim:

    1
    
    [](Sent = 4) => Sent > 0
    

    This says that if Sent is always equal to 4 then it is currently greater than 0.
    TLAPS can’t prove this in a single step:

    • The regular solvers don’t understand the [] bit.
    • The PTL procedure doesn’t understand numbers.

    Coalescing for Reasoning in First-Order Modal Logics explains in detail what’s going on,
    but my rough understanding is that TLAPS replaces things a solver won’t understand with
    new fresh variables.

    For example, [](Sent = 4) => Sent > 0 is received by the solver as something like:

    • Blob1 => Sent > 0 (for a regular solver), or
    • []Blob2 => Blob3 (for PTL)

    Instead, you have to break it down into separate steps:

    1
    2
    3
    
    THEOREM [](Sent = 4) => Sent > 0
    <1> Sent = 4 => Sent > 0 OBVIOUS
    <1> QED BY PTL
    

    In the second step, the PTL solver will receive something like this, which it can prove:

    1
    2
    
    ASSUME Blob2 => Blob3
    PROVE  []Blob2 => Blob3
    

    Annoyingly, TLAPS doesn’t have an easy way to show you what it replaced in this way.
    If it did, I think it would be much easier to learn how to use it.
    You can use tlapm spec.tla --debug tempfiles and look at the files generated for each
    solver backend, but they’re quite hard to read.

    Generalising

    Things that can be proved without assumptions specific to the current time are always true.
    In the above example, PTL converted Blob2 => Blob3 from the regular solver
    to [](Blob2 => Blob3).
    This was only possible because the proof of Blob2 => Blob3 didn’t depend on the current world/time.

    For example:

    1
    2
    3
    4
    5
    
    THEOREM
      ASSUME Spec
      PROVE  [](Sent = 4) => [](Sent > 0)
    <1> Sent = 4 => Sent > 0 OBVIOUS
    <1> QED BY PTL (* Fails *)
    

    We’re trying to prove that if Sent is always 4 then it’s always greater than 0.
    The problem is that we proved Sent = 4 => Sent > 0 in a context that assumed Spec,
    so TLAPS won’t let us generalise it (even though it looks identical to what we proved before,
    and didn’t actually use anything from Spec).

    When PTL fails, the Interesting Obligations view shows e.g.

    ASSUME ...
           Sent = 4 => Sent > 0 (* non-[] *),
           PTL 
    PROVE  [](Sent = 4) => [](Sent > 0)
    

    The (* non-[] *) indicates that this can’t be generalised to all times; we only know it’s true now.

    Instead, we can do:

    1
    2
    3
    4
    5
    6
    7
    
    THEOREM
      Spec => ([](Sent = 4) => [](Sent > 0))
    <1> Sent = 4 => Sent > 0 OBVIOUS
    <1> SUFFICES ASSUME Spec
                 PROVE  [](Sent = 4) => [](Sent > 0)
        OBVIOUS
    <1> QED BY PTL DEF Spec
    

    Doing the Sent = 4 => Sent > 0 step before introducing Spec as an assumption allowed it to be generalised.
    We could also have proved it as a separate lemma.

    Here’s an example where we make use of a non-[] assumption:

    1
    2
    3
    4
    5
    
    THEOREM
      ASSUME Spec
      PROVE  <>(Sent = 0)
    <1> Sent = 0 BY DEF Spec, Init
    <1> QED BY PTL
    

    If Spec is true (we’re at the start of the algorithm) then Sent = 0.
    We can’t generalise to saying that Sent is always 0,
    but we can still use it to prove that Sent is (trivially) eventually zero.
    But having non-[] assumptions usually indicates that something has gone wrong.

    Some assumptions are safe to have in the context however.
    You can assume constants (e.g NEW n in Nat), and also things that will always be true (e.g. []I).
    This is fine:

    1
    2
    3
    4
    5
    
    THEOREM
      ASSUME NEW n in Nat, []I
      PROVE  [](Sent = 4) => [](Sent > 0)
    <1> Sent = 4 => Sent > 0 OBVIOUS
    <1> QED BY PTL
    

    Hiding definitions

    Here’s a simplified version of a problem I had:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    LEMMA L1 ==
      ASSUME NEW n in Nat
      PROVE  Sent >= n => [](Sent >= n)
    PROOF OMITTED
    
    THEOREM
      ASSUME NEW m in Nat
      PROVE  Sent >= m + 1 => [](Sent >= m + 1)
    BY L1  (* Fails! *)
    

    As usual, the regular solvers can’t handle the [] and PTL can’t handle the numbers or the for-all.
    We could create a function (F) for the temporal formula and then hide its definition:

    1
    2
    3
    4
    5
    6
    7
    8
    
    THEOREM
      ASSUME NEW m in Nat
      PROVE  Sent >= m + 1 => [](Sent >= m + 1)
    <1> DEFINE F(i) == Sent >= i => [](Sent >= i)
    <1> A n in Nat : F(n) BY L1
    <1> SUFFICES F(m + 1) OBVIOUS
    <1> HIDE DEF F
    <1> QED OBVIOUS
    

    However, due to a bug in TLAPS, this is unreliable.
    Often a proof fails, then you rename things a bit and it passes, then you rename them back and it continues to pass!
    I reported this at https://github.com/tlaplus/tlapm/issues/247.
    Here’s a screenshot of an extreme case, where TLAPS is failing to prove a conclusion that matches an assumption
    character for character!

    TLAPS failing to prove something very obvious.

    Until that’s fixed, I found that the following pattern works well as a work-around:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    L1_prop(n) == Sent >= n => [](Sent >= n)
    LEMMA L1 ==
      ASSUME NEW n in Nat
      PROVE  L1_prop(n)
    <1> USE DEF L1_prop
    <1> QED PROOF OMITTED
    
    THEOREM
      ASSUME NEW m in Nat
      PROVE  Sent >= m + 1 => [](Sent >= m + 1)
    <1> L1_prop(m+1) BY L1
    <1> QED BY DEF L1_prop
    

    Even when this bug is fixed, I think it would be really nice if
    you could say e.g. BY L1(m+1) to explicitly use L1 with a particular value of n.

    Proving liveness (simple case)

    OK, let’s use some temporal logic to prove Liveness:

    1
    2
    3
    4
    
    Liveness ==
      A n in Nat :    * For all natural numbers n:
        Sent = n ~>     * if we have sent exactly n bytes, then eventually
          Got >= n      * we'll have received at least that many.
    

    One way to prove A ~> B (A always eventually leads to B)
    is from an action A => B' (A leads to B in one step), plus a few side conditions.
    We’ll do that here because we conveniently have a Recv action that immediately gets us to our goal.
    To prove A ~> B a generally-useful pattern is:

    1
    2
    3
    4
    5
    6
    7
    
    THEOREM A ~> B
    <1> A / Recv => B'
    <1> A => ENABLED <<Recv>>_vars
    <1> WF_vars(Recv)
    <1> A / [Next]_vars => (A / B)'
    <1> [][Next]_vars
    <1> QED BY PTL
    
    1. If we’re at A and perform our useful action Recv then afterwards we’ll be at B.
    2. When we’re at A we can perform the useful action.
    3. If the useful action is continually possible then eventually it will happen.
    4. If we’re at A and perform any action Next, we’ll either stay at A or get to B.
    5. The system always performs Next steps.

    Some examples of why these are necessary to prove Liveness:

    • Without 2: Recv requires at least 2 bytes in the buffer; sending a single byte fails.
    • Without 3: someone else has to perform Recv and we can’t be sure they’ll actually do it.
    • Without 4: the sender can Send some data and then Retract it before it is read.
    • Without 5: 4 wouldn’t be useful.

    (3) and (5) come directly from the definition of Spec.
    We need to write proofs for the others, but they’re all non-temporal proofs and fairly simple.
    The only slightly tricky one is proving ENABLED:

    1
    2
    3
    4
    5
    
    LEMMA EnabledRecv ==
      ASSUME NEW n in Nat, I,
             Sent >= n, ~(Got >= n)
      PROVE  ENABLED <<Recv>>_vars
    BY AutoUSE, ExpandENABLED DEF Recv, I
    

    The magic ExpandENABLED causes the definition of ENABLED to be expanded in the version given to the solver.
    This is only useful if the solver also has access to the definitions of all the actions used;
    AutoUSEprovides them automatically so you don’t have to list them manually.

    However, the solver has to work quite hard to prove this and I found it times out on more realistic examples.
    The following trick is more generally useful:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    LEMMA EnabledRecv ==
      ASSUME NEW n in Nat, I,
             Sent >= n, ~(Got >= n)
      PROVE  ENABLED <<Recv>>_vars
    <1> <<Recv>>_vars <=> Recv BY DEF vars, Recv, I
    <1> ENABLED <<Recv>>_vars <=> ENABLED Recv BY ENABLEDaxioms
    <1> SUFFICES ENABLED Recv OBVIOUS
    <1> BufferUsed > 0 BY DEF I
    <1> QED BY AutoUSE, ExpandENABLED DEF Recv
    
    • Recv always modifies vars, so <>_vars is the same as Recv.
    • Therefore, ENABLED <>_vars is the same as ENABLED Recv.
    • Therefore, we just need to prove ENABLED Recv, which is easier for the solver.

    The above uses the magic BY ENABLEDaxioms.
    This doesn’t seem to be documented; I just found it in one of the examples.
    It’s really fussy about the syntax used.
    e.g. it works if you ask to prove it in both directions with <=>,
    but if you ask for e.g. just => then it fails!
    Also, it requires <>_vars <=> Recv rather than <>_vars = Recv,
    though you can prove the first from the second.
    The ENABLEDaxioms_test.tla test file is useful for getting the syntax right.

    See example.tla for the full proof.

    Multi-step liveness

    The above proof was easy because the Recv step gets us to our goal immediately.
    It’s more complicated if we only say that each Recv step reads some of the buffer:

    1
    2
    3
    4
    
    Recv ==
      E n in 1..BufferUsed :
        / Got' = Got + n
        / UNCHANGED Sent
    

    The trick is to define a distance metric:

    1
    
    Dist(n, i) == Sent >= n / n - Got <= i
    

    Dist(n, i) says we are within i bytes of having received the first n bytes.
    Then a simple inductive argument will do:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <1> DEFINE R(j) == Dist(n, j) ~> Dist(n, 0)
    <1>1 R(0) BY PTL
    <1>2 ASSUME NEW i in Nat, R(i) PROVE R(i + 1)
        <2> Dist(n, i+1) ~> Dist(n, i) BY PTL, Progress
        <2> Dist(n, i) ~> Dist(n, 0) BY <1>2
        <2> QED BY PTL
    <1> A i in Nat : R(i)
        <2> HIDE DEF R
        <2> QED BY NatInduction, Isa, <1>1, <1>2
    

    Here, R(i) says that the algorithm works if we start at most i bytes away from our goal.
    R(0) is obviously true, and we show that R(i) => R(i+1).
    By induction, R is true for any distance.
    Notice again the use of HIDE to avoid confusing the solvers with temporal logic.

    Then we just need to show Progress: we eventually get closer to our goal (Dist(n, i+1) ~> Dist(n, i)).
    That can be proved from Recv in a similar way to before. See example.tla for the full proof.

    The real protocol

    I’ve now updated vchan.tla to prove liveness for the real system.
    It was more complicated than the example above (because it also looks at the data being sent,
    has many steps, models sending interrupts, etc), but the basic approach is the same.

    Note: In the real system, Sent and Got are the actual messages, so the
    property talks about their lengths. Sent is what the sending application has
    asked the vchan library to transmit (even if it hasn’t been written to the shared
    buffer yet), and Got is what the receiving application has received from its
    vchan library.

    Ideally, we’d like to prove something like this:

    1
    2
    3
    
    Availability ==
      A x in Nat :
        Len(Sent) = x ~> Len(Got) >= x
    

    However, that doesn’t work because the sender (and receiver) can decide to shut down the connection at any moment.
    If the sender closes the connection before transmitting all the data then obviously it might not be received.
    So it’s actually defined like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    (* Any data that the write function reports has been sent successfully
       (i.e. data in Sent when it is back at "sender_ready") will eventually
       be received (if the receiver doesn't close the connection).
       In particular, this says that it's OK for the sender to close its
       end immediately after sending some data.
       Note that this is not currently true of the C implementation. *)
    Availability ==
      A x in Nat :
        x = Len(Sent) / SenderLive / pc[SenderWriteID] = "sender_ready"
          ~> / Len(Got) >= x
             / ~ReceiverLive
    

    Note: SenderLive and ReceiverLive are badly named.
    SenderLive = FALSE means that the sender has written all data successfully and there is nothing more to come.
    ReceiverLive = FALSE means that the receiver has decided to terminate the connection early,
    and probably indicates an error (think EPIPE).

    Although TLAPS couldn’t prove liveness properties back in 2018, I had got most of the way there:

    I’d defined ReadLimit as:

    The number of bytes that the receiver will eventually get without further action
    from the sender (assuming the receiver doesn’t decide to close the connection).

    and likewise WriteLimit as:

    The number of bytes that the sender will eventually send without further
    action from the other processes or the client application, assuming the
    connection isn’t closed by either end.

    I’d proved some useful invariants about them. For example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    (* Whenever the sender is blocked or idle, the receiver can read everything in
       the buffer without further action from any other process. *)
    THEOREM ReadAllIfSenderBlocked ==
      ASSUME I, SenderLive, ReceiverLive,
             / pc[SenderWriteID] = "sender_ready"
             / pc[SenderWriteID] = "sender_blocked" / ~SpaceAvailableInt
      PROVE  ReadLimit = Len(Got) + Len(Buffer)
    
    (* Whenever the receiver is blocked, the sender will fill the buffer (or write everything
       it wants to write) without further action from any other process. *)
    THEOREM WriteAllIfReceiverBlocked ==
      ASSUME I, SenderLive, ReceiverLive,
             pc[ReceiverReadID] = "recv_await_data", ~DataReadyInt
      PROVE  WriteLimit = Len(Got) + Min(BufferSize, Len(Buffer) + Len(msg))
    

    If ReadLimit and WriteLimit accurately predict what will happen then these properties
    are good evidence that the protocol works:

    • WriteLimit predicts that the sender will be able to fill the buffer initially.
    • If the receiver doesn’t read it then the buffer will become full and the sender will block,
      at which point ReadAllIfSenderBlocked predicts all the data will be read.
    • If the receiver runs out of data then it will then eventually block,
      at which point, WriteAllIfReceiverBlocked predicts more data can be written.

    I’d used TLC to check that both predictions were correct (on small models),
    but I hadn’t been able to prove them with the old version of TLAPS.

    Proving ReadLimit

    The actual proof followed the same outline as the example above.
    The main extra bit was showing that if we haven’t yet read up to the ReadLimit
    then the receiver process would eventually reach the code that did the read,
    and with the belief that there was at least one byte available.
    Mixing BY PTL and regular solvers was particularly annoying here and required stating
    lots of obvious facts like PC in {"recv_got_len"} => PC = "recv_got_len" because PTL
    doesn’t understand sets, etc.

    It seems a bit strange to have to prove that each line of code leads to the next.
    That’s something you can mostly just assume when writing software, but in TLA you need to be explicit.
    The only interesting bits were the usual things about showing code terminates:

    • For loops, show that they will make progress each iteration.
    • Show the code doesn’t crash (e.g. no buffer overflow).
    • Show that any explicit await will eventually resume.

    To help with the proofs I defined RSpec as a variant of Spec:

    1
    2
    3
    4
    5
    
    RSpec ==
      / []I
      / [][Next]_vars
      / []ReceiverLive
      / WF_vars(ReceiverRead)
    

    Instead of being true only at the start (like Spec, which uses Init),
    this only requires our invariant ([]I) to be true.
    Things proved using RSpec are therefore true from any point in the protocol,
    which lets them be reused in other proofs.

    I was able to show that ReadLimit‘s prediction will come true:

    1
    2
    3
    4
    
    ReadLimitEventually_prop(n) == RSpec / ReadLimit >= n ~> Len(Got) >= n
    LEMMA ReadLimitEventually ==
      ASSUME NEW n in Nat
      PROVE  ReadLimitEventually_prop(n)
    

    and from that, rather magically:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    (* When the sender is waiting, the receiver will get everything sent. *)
    ReaderLiveness_prop(n) ==
         / RSpec
         / SenderLive
         / / pc[SenderWriteID] = "sender_ready"
            / pc[SenderWriteID] = "sender_blocked"
         / BytesTransmitted >= n * If we've sent n bytes in total...
         ~> Len(Got) >= n         * then at least n bytes will eventually be received.
    LEMMA ReaderLiveness ==
      ASSUME NEW n in Nat
      PROVE ReaderLiveness_prop(n)
    

    That eliminates all mention of ReadLimit, and expresses the property in terms meaningful to the sender.

    Proving Availability

    I was expecting to have to prove WriteLimit was correct too.
    But it turned out I didn’t.
    Availability only said that the receiver would receive data that had been transmitted;
    it never claimed that the sending process would succeed in doing that!
    It’s right there in the comment even:

    1
    2
    3
    4
    5
    6
    
    (* Any data that the write function reports has been sent successfully ... *)
    Availability ==
      A x in Nat :
        x = Len(Sent) / SenderLive / pc[SenderWriteID] = "sender_ready"
          ~> / Len(Got) >= x
             / ~ReceiverLive
    

    Well, this is pretty bad. The point of proofs is that instead of having to check hundreds of lines of C,
    or dozens of lines of TLA actions, we only have to check that the definition of Availability is what we want.
    My definition of Availability is still true if the sender blocks forever waiting for more space in the buffer.
    In fact, if I change the definition of the sender process so that
    the application asking for data to be sent is the only possible action,
    then TLC reports that Availability still holds (which is true).
    The original blog post appeared on Reddit, Hacker News and Lobsters,
    and nobody else seems to have spotted this either.

    Though in fairness to my past self, I did mention checking an earlier version of Availability
    that did require that it works end-to-end (but didn’t make any claims about what happens on shutdown),
    and I had also tested WriteLimit, so I’m not really worried that the system doesn’t work.

    And so, having learnt how to do liveness proofs, which was my real goal, I decided to stop here.

    Proving WriteLimit

    But proof assistants are addictive, and I found myself continuing anyway.
    The proof of the correctness of WriteLimit was very similar to the one for ReadLimit and I got it done much faster
    now I knew more about how to do these proofs.

    However, I also wanted to fix Availability to something useful and updated it to this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    (* We can't ensure Availability if the sender shuts down
       before it has finishing writing the message to the buffer,
       or if it tries to send another message after shutting down. *)
    CleanShutdownOnly == Len(msg) = 0 / SenderLive
    
    Availability ==
      / []CleanShutdownOnly
      / []ReceiverLive
      => A n in Nat :
            Len(Sent) = n ~>
              Len(Got) >= n
    

    (msg is the data yet to be transmitted)

    That required strengthening some of the existing proofs to talk about clean shutdowns,
    not just the connection never being closed, but it was fairly straight-forward.

    End-to-end liveness

    I used the ReadLimit and WriteLimit lemmas to prove some things about the system as a whole, e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    (* If WriteLimit says we'll transmit some data, it will also be received:
       1. If WriteLimit says we'll transmit some data, then we will.
       2. Eventually the sender will be at a safe point after that.
       3. Once the sender is at a safepoint, the receiver will get all the data. *)
    EndToEndLive_prop(n) == WriteLimit >= n ~> Len(Got) >= n
    LEMMA EndToEndLive ==
      ASSUME NEW n in Nat, []WSpec, []RSpec, []CleanShutdownI
      PROVE  EndToEndLive_prop(n)
    
    (* If we've received n bytes but wanted to send more, then eventually
       there will be space to send more. *)
    LEMMA EventuallySpace ==
      ASSUME NEW n in Nat,
             []WSpec, []RSpec, []CleanShutdownI,
             [](Len(Got) >= n), [](Len(Sent) > n)
      PROVE  <>(WriteLimit > n)
    
    (* If the application wants to send n bytes then eventually WriteLimit will
       predict that we will send them. *)
    LEMMA SufficientSpace ==
      ASSUME NEW n in Nat, []WSpec, []RSpec, []CleanShutdownI
      PROVE  Len(Sent) >= n ~> WriteLimit >= n
    

    and was then finally able to prove my improved version of Availability:

    1
    
    THEOREM Spec => Availability
    

    See vchan.tla for the final version with the proofs.

    Work-arounds for bugs

    The biggest problem I had was the bug when mixing forall and temporal formulas,
    until I found the work-around above.
    Here are some other problems I hit and their solutions:

    SUFFICES doesn’t always generalise

    This doesn’t work:

    1
    2
    3
    4
    5
    6
    
    Foo == [](Sent = 4)
    THEOREM Foo => [](Sent >= 0)
    <1> SUFFICES ASSUME Foo PROVE [](Sent >= 0)
        BY PTL DEF Foo
    <1> Sent = 4 => Sent >= 0 BY DEF Foo
    <1> QED BY PTL DEF Foo  * Fails!
    

    Even though the SUFFICES used BY DEF Foo, Foo still counts as a time-specific assumption.
    A work-around is to ASSUME []Foo and then assert Foo in a separate step:

    1
    2
    3
    4
    5
    6
    7
    
    Foo == [](Sent = 4)
    THEOREM Foo => [](Sent >= 0)
    <1> SUFFICES ASSUME []Foo PROVE [](Sent >= 0)
        BY PTL DEF Foo
    <1> Foo BY PTL
    <1> Sent = 4 => Sent >= 0 BY DEF Foo
    <1> QED BY PTL DEF Foo
    

    CASE with a temporal goal

    This doesn’t work:

    1
    2
    3
    4
    5
    
    THEOREM ASSUME [](Sent = 0) / [](Sent = 4)
            PROVE  [](Sent = 0 / Sent = 4)
    <1> CASE [](Sent = 0) BY PTL
    <1> CASE [](Sent = 4) BY PTL
    <1> QED OBVIOUS
    

    It fails to parse, with Non-constant CASE for temporal goal.
    However, if you replace CASE with the expanded form then it works:

    1
    2
    3
    4
    5
    
    THEOREM ASSUME [](Sent = 0) / [](Sent = 4)
            PROVE  [](Sent = 0 / Sent = 4)
    <1> ASSUME [](Sent = 0) PROVE [](Sent = 0 / Sent = 4) BY PTL
    <1> ASSUME [](Sent = 4) PROVE [](Sent = 0 / Sent = 4) BY PTL
    <1> QED OBVIOUS
    

    PTL and primes

    This doesn’t work:

    1
    2
    
    THEOREM ASSUME [](Sent = 0) PROVE Sent' = 0
    <1> QED BY PTL
    

    A work-around:

    1
    2
    3
    
    THEOREM ASSUME [](Sent = 0) PROVE Sent' = 0
    <1> SUFFICES (Sent = 0)' OBVIOUS
    <1> QED BY PTL
    

    Syntax matters

    Things you might expect to be the same after parsing are treated differently.
    This fails:

    1
    2
    3
    4
    5
    
    THEOREM
      ASSUME [](Sent = 0 / Sent = 4)
      PROVE  [](/ Sent = 0
                / Sent = 4)
    OBVIOUS
    

    But this passes:

    1
    2
    3
    4
    5
    
    THEOREM
      ASSUME [](Sent = 0 / Sent = 4)
      PROVE  [](Sent = 0
                / Sent = 4)
    OBVIOUS
    

    Other bugs

    • If keyboard short-cuts stop working, move the focus out of the window and back in again.
      Sometimes some keys stop working (e.g. Delete) while others continue (e.g. Backspace).

    • If the toolbox GUI fails to launch the prover due to NullPointerException,
      try closing the specification and reopening it.

    • The solver doesn’t actually tell you if everything passed; you have to look in the scrollbar for little red regions.
      If a theorem is folded in the editor, the top-level bit will still show as red if any sub-step fails, but it will
      NOT appear in the scrollbar!
      Either expand everything and check by eye, or run tlapm from the command-line as a final check.
      Note that the bright green Spec Status indicator in the status bar only means that parsing the spec succeeded,
      not that the proof-checking passed.

    Conclusions

    Checking the specification with TLC was quick and easy and immediately found the shutdown bug.
    Actually proving the specification correct using TLAPS was much more difficult,
    took way longer, and didn’t uncovered any additional bugs.
    It did reveal a mistake in my definition of Availability, but there are surely quicker ways of finding that.
    So, in terms of value for time spent, I guess it’s only worth it for very high-value code
    (or if, like me, you just like proving stuff for fun).

    However, I think it’s good to know how to verify things, even if you don’t usually do it.
    It has certainly made me think more carefully about exactly what you need to check to verify liveness.

    Share. Facebook Twitter Pinterest LinkedIn Reddit WhatsApp Telegram Email
    Previous Article2026 will be my year of the Linux desktop
    Next Article A Basic Just-In-Time Compiler (2015)
    TechAiVerse
    • Website

    Jonathan is a tech enthusiast and the mind behind Tech AI Verse. With a passion for artificial intelligence, consumer tech, and emerging innovations, he deliver clear, insightful content to keep readers informed. From cutting-edge gadgets to AI advancements and cryptocurrency trends, Jonathan breaks down complex topics to make technology accessible to all.

    Related Posts

    Spotify’s new feature makes it easier to find popular audiobooks

    March 3, 2026

    This portable JBL Grip Bluetooth speaker is so good at 20% off

    March 3, 2026

    ‘AI’ could dox your anonymous posts

    March 3, 2026
    Leave A Reply Cancel Reply

    Top Posts

    Ping, You’ve Got Whale: AI detection system alerts ships of whales in their path

    April 22, 2025703 Views

    Lumo vs. Duck AI: Which AI is Better for Your Privacy?

    July 31, 2025286 Views

    6.7 Cummins Lifter Failure: What Years Are Affected (And Possible Fixes)

    April 14, 2025164 Views

    6 Best MagSafe Phone Grips (2025), Tested and Reviewed

    April 6, 2025124 Views
    Don't Miss
    Gadgets March 4, 2026

    Honda CR-V Hybrid Lineup Expanded in Malaysia From RM178,200

    Honda CR-V Hybrid Lineup Expanded in Malaysia From RM178,200 Honda Malaysia has officially launched the…

    vivo V70 – Top 7 Flagship Features You Will Love

    Apple iPad Air with M4 Officially Launches in Malaysia From RM2,799

    Apple Launches iPhone 17e in Malaysia from RM2,999

    Stay In Touch
    • Facebook
    • Twitter
    • Pinterest
    • Instagram
    • YouTube
    • Vimeo

    Subscribe to Updates

    Get the latest creative news from SmartMag about art & design.

    About Us
    About Us

    Welcome to Tech AI Verse, your go-to destination for everything technology! We bring you the latest news, trends, and insights from the ever-evolving world of tech. Our coverage spans across global technology industry updates, artificial intelligence advancements, machine learning ethics, and automation innovations. Stay connected with us as we explore the limitless possibilities of technology!

    Facebook X (Twitter) Pinterest YouTube WhatsApp
    Our Picks

    Honda CR-V Hybrid Lineup Expanded in Malaysia From RM178,200

    March 4, 20262 Views

    vivo V70 – Top 7 Flagship Features You Will Love

    March 4, 20262 Views

    Apple iPad Air with M4 Officially Launches in Malaysia From RM2,799

    March 4, 20262 Views
    Most Popular

    7 Best Kids Bikes (2025): Mountain, Balance, Pedal, Coaster

    March 13, 20250 Views

    VTOMAN FlashSpeed 1500: Plenty Of Power For All Your Gear

    March 13, 20250 Views

    Best TV Antenna of 2025

    March 13, 20250 Views
    © 2026 TechAiVerse. Designed by Divya Tech.
    • Home
    • About Us
    • Contact Us
    • Privacy Policy
    • Terms & Conditions

    Type above and press Enter to search. Press Esc to cancel.